Add experimental multiuserroom

This commit is contained in:
kwaroran
2024-09-10 05:44:59 +09:00
parent 2f5f136089
commit 8c47298512
10 changed files with 322 additions and 49 deletions

View File

@@ -708,4 +708,10 @@ export const languageEnglish = {
customCSS: "Custom CSS",
betaMobileGUI: "Beta Mobile GUI",
menu: "Menu",
connectionOpen: "Connection Open",
connectionOpenInfo: "Multiuser room is opened. you can share the room code to other users. others can join the room by using the code inside playground > Join Multiuser Room.",
createMultiuserRoom: "Create Multiuser Room",
connectionHost: "You are the host of the room.",
connectionGuest: "You are the guest of the room.",
otherUserRequesting: "Other user is already requesting. try again later.",
}

View File

@@ -15,6 +15,7 @@
import { capitalize } from "src/ts/util";
import { longpress } from "src/ts/gui/longtouch";
import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme";
import { ConnectionOpenStore } from "src/ts/sync/multiuser";
export let message = ''
export let name = ''
export let largePortrait = false
@@ -202,20 +203,22 @@
</button>
{/if}
<button class={"ml-2 hover:text-green-500 transition-colors "+(editMode?'text-green-400':'')} on:click={() => {
if(!editMode){
editMode = true
}
else{
editMode = false
edit()
}
}}>
<PencilIcon size={20}/>
</button>
<button class="ml-2 hover:text-green-500 transition-colors" on:click={(e) => rm(e, false)} use:longpress={(e) => rm(e, true)}>
<TrashIcon size={20}/>
</button>
{#if !$ConnectionOpenStore}
<button class={"ml-2 hover:text-green-500 transition-colors "+(editMode?'text-green-400':'')} on:click={() => {
if(!editMode){
editMode = true
}
else{
editMode = false
edit()
}
}}>
<PencilIcon size={20}/>
</button>
<button class="ml-2 hover:text-green-500 transition-colors" on:click={(e) => rm(e, false)} use:longpress={(e) => rm(e, true)}>
<TrashIcon size={20}/>
</button>
{/if}
{/if}
{#if $DataBase.translator !== '' && !blankMessage}
<button class={"ml-2 cursor-pointer hover:text-green-500 transition-colors " + (translated ? 'text-green-400':'')} class:translating={translating} on:click={async () => {

View File

@@ -25,6 +25,7 @@
import { postChatFile } from 'src/ts/process/files/multisend';
import { getInlayImage } from 'src/ts/process/files/image';
import PlaygroundMenu from '../Playground/PlaygroundMenu.svelte';
import { ConnectionOpenStore } from 'src/ts/sync/multiuser';
let messageInput:string = ''
let messageInputTranslate:string = ''
@@ -81,7 +82,8 @@
if($DataBase.useSayNothing){
cha.push({
role: 'user',
data: '*says nothing*'
data: '*says nothing*',
name: $ConnectionOpenStore ? $CurrentUsername : null
})
}
}
@@ -98,14 +100,16 @@
cha.push({
role: 'user',
data: await processScript(char,messageInput,'editinput'),
time: Date.now()
time: Date.now(),
name: $ConnectionOpenStore ? $CurrentUsername : null
})
}
else{
cha.push({
role: 'user',
data: messageInput,
time: Date.now()
time: Date.now(),
name: $ConnectionOpenStore ? $CurrentUsername : null
})
}
}
@@ -583,9 +587,9 @@
<Chat
character={$CurrentSimpleCharacter}
idx={chat.index}
name={$CurrentUsername}
name={chat.name ?? $CurrentUsername}
message={chat.data}
img={getCharImage($CurrentUserIcon, 'css')}
img={$ConnectionOpenStore ? '' : getCharImage($CurrentUserIcon, 'css')}
isLastMemory={$CurrentChat.lastMemory === (chat.chatId ?? 'none') && $CurrentShowMemoryLimit}
largePortrait={$UserIconProtrait}
MessageGenerationInfo={chat.generationInfo}

View File

@@ -19,6 +19,7 @@
import TextAreaInput from "../UI/GUI/TextAreaInput.svelte";
import ModuleChatMenu from "../Setting/Pages/Module/ModuleChatMenu.svelte";
import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme";
import Help from "./Help.svelte";
let btn
let input = ''
let cardExportType = 'realm'
@@ -399,6 +400,21 @@
<ChevronRightIcon />
</div>
</button>
{#if $DataBase.useExperimental}
<button class="border-darkborderc border py-2 px-8 flex rounded-md hover:ring-2 items-center mt-2" on:click={() => {
alertStore.set({
type: 'none',
msg: '2'
})
}}>
<div class="flex flex-col justify-start items-start">
<span>{language.createMultiuserRoom} <Help key="experimental"/></span>
</div>
<div class="ml-9 float-right flex-1 flex justify-end">
<ChevronRightIcon />
</div>
</button>
{/if}
<button class="border-darkborderc border py-2 px-8 flex rounded-md hover:ring-2 items-center mt-2" on:click={() => {
alertStore.set({
type: 'none',

View File

@@ -13,6 +13,7 @@
import PlaygroundImageGen from "./PlaygroundImageGen.svelte";
import PlaygroundParser from "./PlaygroundParser.svelte";
import ToolConvertion from "./ToolConvertion.svelte";
import { joinMultiuserRoom } from "src/ts/sync/multiuser";
let easterEggTouch = 0
@@ -89,6 +90,11 @@
}}>
<h1 class="text-2xl font-bold text-start">{language.promptConvertion}</h1>
</button>
<button class="bg-darkbg rounded-md p-6 flex flex-col transition-shadow hover:ring-1" on:click={() => {
joinMultiuserRoom()
}}>
<h1 class="text-2xl font-bold text-start">{language.joinMultiUserRoom}</h1>
</button>
<button class="bg-darkbg rounded-md p-6 flex flex-col transition-shadow hover:ring-1" on:click={() => {
easterEggTouch += 1
}}>

View File

@@ -127,6 +127,10 @@
}
}
}
case 2:{
chara.chatPage = i
createMultiuserRoom()
}
}
}}>
<MenuIcon size={18}/>

View File

@@ -51,7 +51,7 @@
import Button from "../UI/GUI/Button.svelte";
import { alertAddCharacter, alertInput, alertSelect } from "src/ts/alert";
import SideChatList from "./SideChatList.svelte";
import { joinMultiuserRoom } from "src/ts/sync/multiuser";
import { ConnectionIsHost, ConnectionOpenStore, joinMultiuserRoom, RoomIdStore } from "src/ts/sync/multiuser";
import { sideBarSize } from "src/ts/gui/guisize";
import DevTool from "./DevTool.svelte";
let sideBarMode = 0;
@@ -659,6 +659,22 @@
</div>
{:else if $CurrentCharacter?.chaId === '§playground'}
<SideChatList bind:chara={ $CurrentCharacter} />
{:else if $ConnectionOpenStore}
<div class="flex flex-col">
<h1 class="text-xl">{language.connectionOpen}</h1>
<span class="text-textcolor2 mb-4">{language.connectionOpenInfo}</span>
<div class="flex">
<span>ID: </span>
<span class="text-blue-600">{$RoomIdStore}</span>
</div>
<div>
{#if $ConnectionIsHost}
<span class="text-emerald-600">{language.connectionHost}</span>
{:else}
<span class="text-gray-500">{language.connectionGuest}</span>
{/if}
</div>
</div>
{:else}
<div class="w-full h-8 min-h-8 border-l border-b border-r border-selected relative bottom-6 rounded-b-md flex">
<button on:click={() => {

View File

@@ -20,7 +20,7 @@ import { additionalInformations } from "./embedding/addinfo";
import { cipherChat, decipherChat } from "./cipherChat";
import { getInlayImage, supportsInlayImage } from "./files/image";
import { getGenerationModelString } from "./models/modelString";
import { sendPeerChar } from "../sync/multiuser";
import { connectionOpen, peerRevertChat, peerSafeCheck, peerSync } from "../sync/multiuser";
import { runInlayScreen } from "./inlayScreen";
import { runCharacterJS } from "../plugins/embedscript";
import { addRerolls } from "./prereroll";
@@ -108,6 +108,19 @@ export async function sendChat(chatProcessIndex = -1,arg:{
}
doingChat.set(true)
if(connectionOpen){
chatProcessStage.set(4)
const peerSafe = await peerSafeCheck()
if(!peerSafe){
peerRevertChat()
doingChat.set(false)
alertError(language.otherUserRequesting)
return false
}
await peerSync()
chatProcessStage.set(0)
}
let db = get(DataBase)
let selectedChar = get(selectedCharID)
const nowChatroom = db.characters[selectedChar]
@@ -1307,7 +1320,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{
chatProcessStage.set(4)
sendPeerChar()
peerSync()
if(req.special){
if(req.special.emotion){

View File

@@ -1062,6 +1062,8 @@ export interface Message{
chatId?:string
time?: number
generationInfo?: MessageGenerationInfo
name?:string
otherUser?:boolean
}
export interface MessageGenerationInfo{

View File

@@ -1,23 +1,65 @@
import { v4 } from 'uuid';
import { alertError, alertInput, alertNormal, alertWait } from '../alert';
import { get } from 'svelte/store';
import { DataBase, setDatabase, type character, saveImage } from '../storage/database';
import { selectedCharID } from '../stores';
import { alertError, alertInput, alertNormal, alertStore, alertWait } from '../alert';
import { get, writable } from 'svelte/store';
import { DataBase, setDatabase, type character, saveImage, type Chat } from '../storage/database';
import { CurrentChat, selectedCharID } from '../stores';
import { findCharacterIndexbyId, sleep } from '../util';
import type { DataConnection, Peer } from 'peerjs';
import { readImage } from '../storage/globalApi';
import { doingChat } from '../process';
async function importPeerJS(){
return await import('peerjs');
}
interface ReciveFirst{
type: 'receive-char',
data: character
}
interface RequestFirst{
type: 'request-char'
}
interface ReciveAsset{
type: 'receive-asset',
id: string,
data: Uint8Array
}
interface RequestSync{
type: 'request-chat-sync',
id: string,
data: Chat
}
interface ReciveSync{
type: 'receive-chat',
data: Chat
}
interface RequestChatSafe{
type: 'request-chat-safe',
id: string
}
interface ResponseChatSafe{
type: 'response-chat-safe'
data: boolean,
id: string
}
interface RequestChat{
type: 'request-chat'
}
type ReciveData = ReciveFirst|RequestFirst|ReciveAsset|RequestSync|ReciveSync|RequestChatSafe|ResponseChatSafe|RequestChat
let conn:DataConnection
let peer:Peer
let connections:DataConnection[] = []
let connectionOpen = false
export let connectionOpen = false
let requestChatSafeQueue = new Map<string, {remaining:number,safe:boolean,conn?:DataConnection}>()
export let ConnectionOpenStore = writable(false)
export let ConnectionIsHost = writable(false)
export let RoomIdStore = writable('')
export async function createMultiuserRoom(){
//create a room with webrtc
ConnectionIsHost.set(true)
alertWait("Loading...")
const peerJS = await importPeerJS();
@@ -93,7 +135,96 @@ export async function createMultiuserRoom(){
return
}
char.chats[char.chatPage] = recivedChar.chats[0]
sendPeerChar()
}
if(data.type === 'request-chat-sync'){
const db = get(DataBase)
const selectedCharId = get(selectedCharID)
const char = db.characters[selectedCharId]
char.chats[char.chatPage] = data.data
db.characters[selectedCharId] = char
latestSyncChat = data.data
setDatabase(db)
for(const connection of connections){
if(connection.connectionId === conn.connectionId){
continue
}
const rs:ReciveSync = {
type: 'receive-chat',
data: data.data
}
connection.send(rs)
}
}
if(data.type === 'request-chat'){
const db = get(DataBase)
const selectedCharId = get(selectedCharID)
const char = db.characters[selectedCharId]
const chat = char.chats[char.chatPage]
const rs:ReciveSync = {
type: 'receive-chat',
data: chat
}
conn.send(rs)
}
if(data.type === 'request-chat-safe'){
const queue = {
remaining: connections.length,
safe: true,
conn: conn
}
requestChatSafeQueue.set(data.id, queue)
for(const connection of connections){
if(connection.connectionId === conn.connectionId){
queue.remaining--
requestChatSafeQueue.set(data.id, queue)
continue
}
const rs:RequestChatSafe = {
type: 'request-chat-safe',
id: data.id
}
connection.send(rs)
}
if(queue.remaining === 0){
if(waitingMultiuserId === data.id){
waitingMultiuserId = ''
waitingMultiuserSafe = queue.safe
}
else if(queue.conn){
const rs:ResponseChatSafe = {
type: 'response-chat-safe',
data: queue.safe,
id: data.id
}
queue.conn.send(rs)
requestChatSafeQueue.delete(data.id)
}
}
}
if(data.type === 'response-chat-safe'){
const queue = requestChatSafeQueue.get(data.id)
if(queue){
queue.remaining--
if(!data.data){
queue.safe = false
}
if(queue.remaining === 0){
if(waitingMultiuserId === data.id){
waitingMultiuserId = ''
waitingMultiuserSafe = queue.safe
}
else if(queue.conn){
const rs:ResponseChatSafe = {
type: 'response-chat-safe',
data: queue.safe,
id: data.id
}
queue.conn.send(rs)
requestChatSafeQueue.delete(data.id)
}
}
}
}
});
@@ -111,28 +242,23 @@ export async function createMultiuserRoom(){
}
connectionOpen = true
alertNormal("Room ID: " + roomId)
ConnectionOpenStore.set(true)
RoomIdStore.set(roomId)
alertStore.set({
type: 'none',
msg: ''
})
return
}
interface ReciveFirst{
type: 'receive-char',
data: character
}
interface RequestFirst{
type: 'request-char'
}
interface ReciveAsset{
type: 'receive-asset',
id: string,
data: Uint8Array
}
type ReciveData = ReciveFirst|RequestFirst|ReciveAsset
let waitingMultiuserId = ''
let waitingMultiuserSafe = false
let latestSyncChat:Chat|null = null
export async function joinMultiuserRoom(){
//join a room with webrtc
ConnectionIsHost.set(false)
alertWait("Loading...")
const peerJS = await importPeerJS();
peer = new peerJS.Peer(
@@ -145,6 +271,7 @@ export async function joinMultiuserRoom(){
let open = false
conn = peer.connect(roomId);
RoomIdStore.set(roomId)
conn.on('open', function() {
alertWait("Waiting for host to accept connection")
@@ -179,6 +306,32 @@ export async function joinMultiuserRoom(){
}
case 'receive-asset':{
saveImage(data.data, data.id)
break
}
case 'receive-chat':{
const db = get(DataBase)
const selectedCharId = get(selectedCharID)
const char = structuredClone(db.characters[selectedCharId])
char.chats[char.chatPage] = data.data
db.characters[selectedCharId] = char
latestSyncChat = data.data
setDatabase(db)
break
}
case 'request-chat-safe':{
const rs:ResponseChatSafe = {
type: 'response-chat-safe',
data: !get(doingChat) || data.id === waitingMultiuserId,
id: data.id
}
conn.send(rs)
break
}
case 'response-chat-safe':{
if(data.id === waitingMultiuserId){
waitingMultiuserId = ''
waitingMultiuserSafe = data.data
}
}
}
});
@@ -186,6 +339,7 @@ export async function joinMultiuserRoom(){
conn.on('close', function() {
alertError("Connection closed")
connectionOpen = false
ConnectionOpenStore.set(false)
selectedCharID.set(-1)
})
@@ -199,29 +353,78 @@ export async function joinMultiuserRoom(){
}
}
connectionOpen = true
ConnectionOpenStore.set(true)
alertNormal("Connected")
});
}
export function sendPeerChar(){
export async function peerSync(){
if(!connectionOpen){
return
}
await sleep(1)
const chat = get(CurrentChat)
latestSyncChat = chat
if(!conn){
// host user
for(const connection of connections){
connection.send({
type: 'receive-char',
data: get(DataBase).characters[get(selectedCharID)]
type: 'receive-chat',
data: chat
});
}
}
else{
conn.send({
type: 'receive-char',
data: get(DataBase).characters[get(selectedCharID)]
});
type: 'request-chat-sync',
data: chat
} as RequestSync)
}
}
export async function peerSafeCheck() {
if(!connectionOpen){
return true
}
await sleep(500)
if(!conn){
waitingMultiuserId = v4()
requestChatSafeQueue.set(waitingMultiuserId, {
remaining: connections.length,
safe: true,
})
for(const connection of connections){
const rs:RequestChatSafe = {
type: 'request-chat-safe',
id: waitingMultiuserId
}
connection.send(rs)
}
while(waitingMultiuserId !== ''){
await sleep(100)
}
return waitingMultiuserSafe
}
else{
waitingMultiuserId = v4()
const rs:RequestChatSafe = {
type: 'request-chat-safe',
id: waitingMultiuserId
}
conn.send(rs)
while(waitingMultiuserId !== ''){
await sleep(100)
}
return waitingMultiuserSafe
}
}
export function peerRevertChat() {
if(!connectionOpen || !latestSyncChat){
return
}
CurrentChat.set(latestSyncChat)
}