+ {#if $selectedCharID >= 0}
+ {#if $DataBase.characters[$selectedCharID].viewScreen !== 'none'}
+
+ {/if}
+ {/if}
+ 2 ? 'background: rgba(0,0,0,0.8)': ''} bind:openChatList/>
+
+{:else if $DataBase.theme === 'waifu'}
+ {openChatList = false}}/>
+{/if}
+
+
\ No newline at end of file
diff --git a/src/lib/ChatScreens/DefaultChatScreen.svelte b/src/lib/ChatScreens/DefaultChatScreen.svelte
new file mode 100644
index 00000000..152e53fe
--- /dev/null
+++ b/src/lib/ChatScreens/DefaultChatScreen.svelte
@@ -0,0 +1,381 @@
+
+
+ {
+ openMenu = false
+}}>
+ {#if $selectedCharID < 0}
+
+
RisuAI
+ Version {appVer}
+
+ {:else}
+
{
+ //@ts-ignore
+ const scrolled = (e.target.scrollHeight - e.target.clientHeight + e.target.scrollTop)
+ if(scrolled < 100 && $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message.length > loadPages){
+ loadPages += 30
+ }
+ }}>
+
+ {#each messageForm($DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message, loadPages) as chat, i}
+ {#if chat.role === 'char'}
+ {#if $DataBase.characters[$selectedCharID].type !== 'group'}
+ {#await getCharImage($DataBase.characters[$selectedCharID].image, 'css')}
+
+ {:then im}
+
+ {/await}
+ {:else}
+ {#await getCharImage(findCharacterbyId(chat.saying).image, 'css')}
+
+ {:then im}
+
+ {/await}
+ {/if}
+ {:else}
+ {#await getCharImage($DataBase.userIcon, 'css')}
+
+ {:then im}
+
+ {/await}
+ {/if}
+ {/each}
+ {#if $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message.length <= loadPages}
+ {#if $DataBase.characters[$selectedCharID].type !== 'group'}
+ {#await getCharImage($DataBase.characters[$selectedCharID].image, 'css')}
+
+ {:then im}
+
+ {/await}
+ {/if}
+ {/if}
+ {#if openMenu}
+
{
+ e.stopPropagation()
+ }}>
+ {#if $DataBase.characters[$selectedCharID].type === 'group'}
+
+
+ {language.autoMode}
+
+ {/if}
+
+
{
+ openChatList = true
+ openMenu = false
+ }}>
+
+ {language.chatList}
+
+ {#if $DataBase.translator !== ''}
+
{
+ $doingChat = true
+ messageInput = (await translate(messageInput, true))
+ $doingChat = false
+ }}>
+
+ {language.translateInput}
+
+ {/if}
+
+
+ {language.reroll}
+
+
+
+ {/if}
+
+
+ {/if}
+
+
\ No newline at end of file
diff --git a/src/lib/ChatScreens/EmotionBox.svelte b/src/lib/ChatScreens/EmotionBox.svelte
new file mode 100644
index 00000000..f4045353
--- /dev/null
+++ b/src/lib/ChatScreens/EmotionBox.svelte
@@ -0,0 +1,21 @@
+
+
+{#await getEmotion($DataBase,$CharEmotion, 'contain') then images}
+ {#each images as image, i}
+
+
+ {/each}
+{/await}
+
+
\ No newline at end of file
diff --git a/src/lib/ChatScreens/ResizeBox.svelte b/src/lib/ChatScreens/ResizeBox.svelte
new file mode 100644
index 00000000..973f4ade
--- /dev/null
+++ b/src/lib/ChatScreens/ResizeBox.svelte
@@ -0,0 +1,96 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/ChatScreens/TransitionImage.svelte b/src/lib/ChatScreens/TransitionImage.svelte
new file mode 100644
index 00000000..be6b3914
--- /dev/null
+++ b/src/lib/ChatScreens/TransitionImage.svelte
@@ -0,0 +1,188 @@
+
+
+
+
+{#if currentSrc && currentSrc.length > 0}
+
+ {#if !showOldImage}
+ {#each currentSrc as img, i}
+ {#if styleType === 'normal'}
+
+ {:else if styleType === 'emp'}
+ {#if i <= 1}
+
+ {/if}
+ {/if}
+ {/each}
+ {:else}
+ {#if oldStyleType === 'normal'}
+ {#each oldSrc as img2, i}
+
+ {/each}
+ {:else if oldStyleType === 'emp'}
+
+ {#each oldSrc as img2, i}
+ {#if i <= 1}
+
+ {/if}
+ {/each}
+ {/if}
+ {#if styleType === 'normal'}
+ {#each currentSrc as img3, i}
+
+ {/each}
+ {:else if styleType === 'emp'}
+
+ {#each currentSrc as img3, i}
+ {#if i <= 1}
+
+ {/if}
+ {/each}
+ {/if}
+ {/if}
+
+{/if}
diff --git a/src/lib/Others/AlertComp.svelte b/src/lib/Others/AlertComp.svelte
new file mode 100644
index 00000000..f00a58f9
--- /dev/null
+++ b/src/lib/Others/AlertComp.svelte
@@ -0,0 +1,148 @@
+
+
+{#if $alertStore.type !== 'none' && $alertStore.type !== 'toast'}
+
+
+ {#if $alertStore.type === 'error'}
+
Error
+ {:else if $alertStore.type === 'ask'}
+
Confirm
+ {:else if $alertStore.type === 'selectChar'}
+
Select
+ {:else if $alertStore.type === 'input'}
+
Input
+ {/if}
+ {#if $alertStore.type === 'markdown'}
+
{@html ParseMarkdown($alertStore.msg)}
+ {:else if $alertStore.type !== 'select'}
+
{$alertStore.msg}
+ {/if}
+ {#if $alertStore.type === 'ask'}
+
+ {
+ alertStore.set({
+ type: 'none',
+ msg: 'yes'
+ })
+ }}>YES
+ {
+ alertStore.set({
+ type: 'none',
+ msg: 'no'
+ })
+ }}>NO
+
+ {:else if $alertStore.type === 'select'}
+ {#each $alertStore.msg.split('||') as n, i}
+
{
+ alertStore.set({
+ type: 'none',
+ msg: i.toString()
+ })
+ }}>{n}
+ {/each}
+ {:else if $alertStore.type === 'error' || $alertStore.type === 'normal' || $alertStore.type === 'markdown'}
+
{
+ alertStore.set({
+ type: 'none',
+ msg: ''
+ })
+ }}>OK
+ {:else if $alertStore.type === 'input'}
+
+
{
+ alertStore.set({
+ type: 'none',
+ msg: input
+ })
+ }}>OK
+ {:else if $alertStore.type === 'selectChar'}
+
+ {#each $DataBase.characters as char, i}
+ {#if char.type !== 'group'}
+ {#if char.image}
+ {#await getCharImage($DataBase.characters[i].image, 'css')}
+ {
+ //@ts-ignore
+ alertStore.set({type: 'none',msg: char.chaId})
+ }}>
+
+
+ {:then im}
+ {
+ //@ts-ignore
+ alertStore.set({type: 'none',msg: char.chaId})
+ }} additionalStyle={im} />
+
+ {/await}
+ {:else}
+ {
+ //@ts-ignore
+ alertStore.set({type: 'none',msg: char.chaId})
+ }}>
+
+
+ {/if}
+ {/if}
+ {/each}
+
+ {/if}
+
+
+{:else if $alertStore.type === 'toast'}
+ {
+ alertStore.set({
+ type: 'none',
+ msg: ''
+ })
+ }}
+ >{$alertStore.msg}
+{/if}
+
+
\ No newline at end of file
diff --git a/src/lib/Others/ChatList.svelte b/src/lib/Others/ChatList.svelte
new file mode 100644
index 00000000..21e1628b
--- /dev/null
+++ b/src/lib/Others/ChatList.svelte
@@ -0,0 +1,104 @@
+
+
+
+
+
+
{language.chatList}
+
+
+
+
+
+
+ {#each $DataBase.characters[$selectedCharID].chats as chat, i}
+
{
+ if(!editMode){
+ $DataBase.characters[$selectedCharID].chatPage = i
+ close()
+ }
+ }} class="flex items-center text-neutral-200 border-t-1 border-solid border-0 border-gray-600 p-2 cursor-pointer" class:bg-selected={i === $DataBase.characters[$selectedCharID].chatPage}>
+ {#if editMode}
+
+ {:else}
+ {chat.name}
+ {/if}
+
+ {
+ e.stopPropagation()
+ exportChat(i)
+ }}>
+
+
+ {
+ e.stopPropagation()
+ if($DataBase.characters[$selectedCharID].chats.length === 1){
+ alertError(language.errors.onlyOneChat)
+ return
+ }
+ const d = await alertConfirm(`${language.removeConfirm}${chat.name}`)
+ if(d){
+ $DataBase.characters[$selectedCharID].chatPage = 0
+ let chats = $DataBase.characters[$selectedCharID].chats
+ chats.splice(i, 1)
+ $DataBase.characters[$selectedCharID].chats = chats
+ }
+ }}>
+
+
+
+
+ {/each}
+
+
{
+ const cha = $DataBase.characters[$selectedCharID]
+ const len = $DataBase.characters[$selectedCharID].chats.length
+ let chats = $DataBase.characters[$selectedCharID].chats
+ chats.push({
+ message:[], note:'', name:`New Chat ${len + 1}`, localLore:[]
+ })
+ if(cha.type === 'group'){
+ cha.characters.map((c) => {
+ chats[len].message.push({
+ saying: c,
+ role: 'char',
+ data: findCharacterbyId(c).firstMessage
+ })
+ })
+ }
+ $DataBase.characters[$selectedCharID].chats = chats
+ $DataBase.characters[$selectedCharID].chatPage = len
+ close()
+ }}>
+
+
+
{
+ importChat()
+ }}>
+
+
+
{
+ editMode = !editMode
+ }}>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/Others/Check.svelte b/src/lib/Others/Check.svelte
new file mode 100644
index 00000000..e1cab220
--- /dev/null
+++ b/src/lib/Others/Check.svelte
@@ -0,0 +1,19 @@
+
+
+
+ {
+ onChange(check)
+ }}>
+ {#if check}
+
+
+
+ {:else}
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/lib/Others/GridCatalog.svelte b/src/lib/Others/GridCatalog.svelte
new file mode 100644
index 00000000..42a723fc
--- /dev/null
+++ b/src/lib/Others/GridCatalog.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
Catalog
+
+
+
+ {#each $DataBase.characters.filter((c) => {
+ return c.name.toLocaleLowerCase().includes(search.toLocaleLowerCase())
+ }) as char, i}
+
+ {#if char.image}
+ {changeChar(i)}} additionalStyle={getCharImage($DataBase.characters[i].image, 'css')}>
+ {:else}
+ {changeChar(i)}} additionalStyle={i === $selectedCharID ? 'background:#44475a' : ''}>
+ {#if char.type === 'group'}
+
+ {:else}
+
+ {/if}
+
+ {/if}
+
+ {/each}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/Others/Help.svelte b/src/lib/Others/Help.svelte
new file mode 100644
index 00000000..e8e91b1e
--- /dev/null
+++ b/src/lib/Others/Help.svelte
@@ -0,0 +1,19 @@
+
+
+ {
+ alertMd(language.help[key])
+}}>
+ {#if key === "experimental"}
+
+ {:else}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/src/lib/Others/WelcomeRisu.svelte b/src/lib/Others/WelcomeRisu.svelte
new file mode 100644
index 00000000..57d36a4a
--- /dev/null
+++ b/src/lib/Others/WelcomeRisu.svelte
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
Welcome to RisuAI!
+
+ {#if step === 0}
+ Choose the language
+
+ {
+ changeLanguage('en')
+ step = 1
+ }}>• English
+ {
+ changeLanguage('ko')
+ step = 1
+ }}>• 한국어
+
+
+ {:else if step === 1}
+ {language.setup.chooseProvider}
+
+ {
+ provider = 1
+ step += 1
+ }}>• {language.setup.openaikey}
+ {
+ provider = 2
+ step += 1
+ }}>• {language.setup.openaiProxy}
+ {
+ provider = 3
+ step += 1
+ }}>• {language.setup.setupmodelself}
+
+ {:else if step === 2}
+ {#if provider === 1}
+ {language.setup.openaikey}
+
+ API key
+
+
+ {language.setup.apiKeyhelp} https://platform.openai.com/account/api-keys
+
+ {
+ provider = 1
+ step += 1
+ }}>• {language.confirm}
+
+ {:else if provider === 2}
+ {language.setup.openaiProxy}
+
+ OpenAI Reverse Proxy URL
+
+
+
+ API key (Used for passwords)
+
+
+
+ {
+ provider = 1
+ step += 1
+ }}>• {language.confirm}
+
+ {:else}
+ {language.setup.setupmodelself}
+
+ {language.setup.setupSelfHelp}
+
+
+ {
+ provider = 1
+ step += 1
+ }}>• {language.confirm}
+
+ {/if}
+ {:else if step === 3}
+ {language.setup.theme}
+
+
{
+ $DataBase.theme = ''
+ step += 1
+ }}>• Standard Risu
+
+
{
+ $DataBase.theme = 'waifu'
+ step += 1
+ }}>• Waifulike (Not suitable for mobile)
+
+
+ {:else if step === 4}
+ {language.setup.theme}
+
+
{
+ $DataBase.theme = ''
+ step += 1
+ }}>• Standard Risu
+
+
{
+ $DataBase.theme = 'waifu'
+ step += 1
+ }}>• Waifulike
+
+
+ {:else if step === 4}
+ {language.setup.theme}
+
+
{
+ $DataBase.theme = ''
+ step += 1
+ }}>• Standard Risu
+
+
{
+ $DataBase.theme = 'waifu'
+ step += 1
+ }}>• Waifulike
+
+
+ {:else if step === 5}
+ {language.setup.texttheme}
+
+
{
+ $DataBase.theme = ''
+ step += 1
+ }}>• {language.classicRisu}
+
+
Normal Text
+
Italic Text
+
Bold Text
+
+
+
+
+
+
{
+ $DataBase.theme = ''
+ step += 1
+ }}>• {language.highcontrast}
+
+
Normal Text
+
Italic Text
+
Bold Text
+
+
+
+
+ {:else if step === 6}
+ {language.setup.inputName}
+
+
+
+
+ {
+ $DataBase.forceReplaceUrl2 = $DataBase.forceReplaceUrl
+ await addDefaultCharacters()
+ $DataBase.didFirstSetup = true
+ }}>• {language.confirm}
+
+ {/if}
+
+
+ {#if step > 0}
+
+ {
+ step = step - 1
+ }}>• Go Back
+ {/if}
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/Others/botpreset.svelte b/src/lib/Others/botpreset.svelte
new file mode 100644
index 00000000..80552432
--- /dev/null
+++ b/src/lib/Others/botpreset.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+
+
{language.presets}
+
+
+
+
+
+
+ {#each $DataBase.botPresets as presets, i}
+
{
+ if(!editMode){
+ changeToPreset(i)
+ close()
+ }
+ }} class="flex items-center text-neutral-200 border-t-1 border-solid border-0 border-gray-600 p-2 cursor-pointer" class:bg-selected={i === $DataBase.botPresetsId}>
+ {#if editMode}
+
+ {:else}
+ {#if i < 9}
+ {i + 1}
+ {/if}
+ {presets.name}
+ {/if}
+
+ {
+ e.stopPropagation()
+ if($DataBase.botPresets.length === 1){
+ alertError(language.errors.onlyOneChat)
+ return
+ }
+ const d = await alertConfirm(`${language.removeConfirm}${presets.name}`)
+ if(d){
+ changeToPreset(0)
+ let botPresets = $DataBase.botPresets
+ botPresets.splice(i, 1)
+ $DataBase.botPresets = botPresets
+ }
+ }}>
+
+
+
+
+ {/each}
+
+
{
+ let botPresets = $DataBase.botPresets
+ let newPreset = JSON.parse(JSON.stringify(presetTemplate))
+ newPreset.name = `New Preset`
+ botPresets.push(newPreset)
+
+ $DataBase.botPresets = botPresets
+ }}>
+
+
+
{
+ editMode = !editMode
+ }}>
+
+
+
+
{language.quickPreset}
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/BarIcon.svelte b/src/lib/SideBars/BarIcon.svelte
new file mode 100644
index 00000000..b9ea837b
--- /dev/null
+++ b/src/lib/SideBars/BarIcon.svelte
@@ -0,0 +1,36 @@
+
+{#await additionalStyle}
+
+{:then as}
+
+{/await}
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/CharConfig.svelte b/src/lib/SideBars/CharConfig.svelte
new file mode 100644
index 00000000..43649c8d
--- /dev/null
+++ b/src/lib/SideBars/CharConfig.svelte
@@ -0,0 +1,466 @@
+
+
+
+
+
+
+
+
+
+
+{#if subMenu === 0}
+ {#if currentChar.type !== 'group'}
+
+ {language.description}
+
+ {tokens.desc} {language.tokens}
+ {language.firstMessage}
+
+ {tokens.firstMsg} {language.tokens}
+ {:else}
+
+ {language.character}
+
+ {#if currentChar.data.characters.length === 0}
+ No Character
+ {:else}
+ {#each currentChar.data.characters as char, i}
+ {#await getCharImage(findCharacterbyId(char).image, 'css')}
+ {
+ rmCharFromGroup(i)
+ }}>
+
+
+ {:then im}
+ {
+ rmCharFromGroup(i)
+ }} additionalStyle={im} />
+ {/await}
+ {/each}
+ {/if}
+
+
+
+ {/if}
+ {language.authorNote}
+
+ {tokens.localNote} {language.tokens}
+
+
+
+ {language.jailbreakToggle}
+
+{:else if subMenu === 1}
+ {language.characterDisplay}
+ {currentChar.type !== 'group' ? language.charIcon : language.groupIcon}
+ {selectCharImg($selectedCharID)}}>
+ {#if currentChar.data.image === ''}
+
+ {:else}
+ {#await getCharImage(currentChar.data.image, 'css')}
+
+ {:then im}
+
+ {/await}
+ {/if}
+
+
+ {#if currentChar.type === 'group'}
+
+ {language.createGroupImg}
+
+ {/if}
+
+
+ {language.viewScreen}
+
+
+ {#if currentChar.type !== 'group'}
+
+ {language.none}
+ {language.emotionImage}
+ {language.imageGeneration}
+
+ {:else}
+
+ {language.none}
+ {language.singleView}
+ {language.SpacedView}
+ {language.emphasizedView}
+
+
+ {/if}
+
+ {#if currentChar.data.viewScreen === 'emotion'}
+ {language.emotionImage}
+ {language.emotionWarn}
+
+
+
+
+ {#if !$addingEmotion}
+
{addCharEmotion($selectedCharID)}}>
+
+
+ {:else}
+
Loading...
+ {/if}
+
+ {/if}
+ {#if currentChar.data.viewScreen === 'imggen'}
+ {language.imageGeneration}
+ {language.emotionWarn}
+
+
+
+
+ {#if !$addingEmotion}
+
{
+ let db = ($DataBase)
+ let charId = $selectedCharID
+ let dbChar = db.characters[charId]
+ if(dbChar.type !== 'group'){
+ dbChar.sdData.push(['', ''])
+ db.characters[charId] = dbChar
+ }
+ $DataBase = (db)
+ }}>
+
+
+ {:else}
+
Loading...
+ {/if}
+
+ {language.currentImageGeneration}
+ {#if currentChar.data.chats[currentChar.data.chatPage].sdData}
+
+ {:else}
+ {language.noData}
+ {/if}
+ {/if}
+{:else if subMenu === 3}
+ {language.loreBook}
+
+{:else if subMenu === 2}
+ {language.advancedSettings}
+ {#if currentChar.type !== 'group'}
+ Bias
+
+ {language.regexScript}
+
+ {#if currentChar.data.customscript.length === 0}
+ No Scripts
+ {/if}
+ {#each currentChar.data.customscript as customscript, i}
+ {
+ if(currentChar.type === 'character'){
+ let customscript = currentChar.data.customscript
+ customscript.splice(i, 1)
+ currentChar.data.customscript = customscript
+ }
+ }}/>
+ {/each}
+
+ {
+ if(currentChar.type === 'character'){
+ let script = currentChar.data.customscript
+ script.push({
+ comment: "",
+ in: "",
+ out: "",
+ type: "editinput"
+ })
+ currentChar.data.customscript = script
+ }
+ }}>
+
+
+ {language.utilityBot}
+
+ {
+ exportChar($selectedCharID)
+ }} class="text-neutral-200 mt-6 text-lg bg-transparent border-solid border-1 border-borderc p-4 hover:bg-green-500 transition-colors cursor-pointer">{language.exportCharacter}
+
+ {:else}
+
+
+
+ {language.useCharLorebook}
+
+
+ {/if}
+ {
+ const conf = await alertConfirm(language.removeConfirm + currentChar.data.name)
+ if(!conf){
+ return
+ }
+ const conf2 = await alertConfirm(language.removeConfirm2 + currentChar.data.name)
+ if(!conf2){
+ return
+ }
+ let chars = $DataBase.characters
+ chars.splice($selectedCharID, 1)
+ $selectedCharID = -1
+ $DataBase.characters = chars
+
+ }} class="text-neutral-200 mt-2 bg-transparent border-solid border-1 border-borderc p-2 hover:bg-draculared transition-colors cursor-pointer">{ currentChar.type === 'group' ? language.removeGroup : language.removeCharacter}
+{/if}
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/DropList.svelte b/src/lib/SideBars/DropList.svelte
new file mode 100644
index 00000000..6dc87060
--- /dev/null
+++ b/src/lib/SideBars/DropList.svelte
@@ -0,0 +1,58 @@
+
+
+
+ {#each list as n, i}
+
+ {language.formating[n]}
+ {
+ if(i !== 0){
+ let tempList = list
+ const temp = tempList[i]
+ tempList[i] = tempList[i-1]
+ tempList[i-1] = temp
+ list = tempList
+ }
+ else{
+ let tempList = list
+ const temp = tempList[i]
+ tempList[i] = tempList[i+1]
+ tempList[i+1] = temp
+ list = tempList
+ }
+ }}>
+ {
+ if(i !== (list.length - 1)){
+ let tempList = list
+ const temp = tempList[i]
+ tempList[i] = tempList[i+1]
+ tempList[i+1] = temp
+ list = tempList
+ }
+ else{
+ let tempList = list
+ const temp = tempList[i]
+ tempList[i] = tempList[i-1]
+ tempList[i-1] = temp
+ list = tempList
+ }
+ }}>
+
+ {#if i !== (list.length - 1)}
+
+ {/if}
+ {/each}
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/LoreBookData.svelte b/src/lib/SideBars/LoreBookData.svelte
new file mode 100644
index 00000000..fcd6c5aa
--- /dev/null
+++ b/src/lib/SideBars/LoreBookData.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+ {
+ open = !open
+ }}>
+ {value.comment.length === 0 ? 'Unnamed Lore' : value.comment}
+
+ {
+ const d = await alertConfirm(language.removeConfirm + value.comment)
+ if(d){
+ onRemove()
+ }
+ }}>
+
+
+
+ {#if open}
+
+
{language.name}
+
+ {#if !value.alwaysActive}
+
{language.activationKeys}
+
{language.activationKeysInfo}
+
+ {/if}
+
{language.insertOrder}
+
+
{language.prompt}
+
+
+
+ {language.alwaysActive}
+
+
+ {/if}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/LoreBookSetting.svelte b/src/lib/SideBars/LoreBookSetting.svelte
new file mode 100644
index 00000000..dc63536f
--- /dev/null
+++ b/src/lib/SideBars/LoreBookSetting.svelte
@@ -0,0 +1,82 @@
+
+
+
+ {
+ submenu = 0
+ }} class="flex-1 border-solid border-borderc border-1 p-2 flex justify-center cursor-pointer" class:bg-selected={submenu === 0}>
+ {$DataBase.characters[$selectedCharID].type === 'group' ? language.group : language.character}
+
+ {
+ submenu = 1
+ }} class="flex-1 border-solid border-borderc border-1 border-l-transparent p-2 flex justify-center cursor-pointer" class:bg-selected={submenu === 1}>
+ {language.Chat}
+
+
+{submenu === 0 ? $DataBase.characters[$selectedCharID].type === 'group' ? language.groupLoreInfo : language.globalLoreInfo : language.localLoreInfo}
+
+
+ {#if submenu === 0}
+ {#if $DataBase.characters[$selectedCharID].globalLore.length === 0}
+
No Lorebook
+ {:else}
+ {#each $DataBase.characters[$selectedCharID].globalLore as book, i}
+ {#if i !== 0}
+
+ {/if}
+
{
+ let lore = $DataBase.characters[$selectedCharID].globalLore
+ lore.splice(i, 1)
+ $DataBase.characters[$selectedCharID].globalLore = lore
+ }}/>
+ {/each}
+ {/if}
+ {:else}
+ {#if $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].localLore.length === 0}
+ No Lorebook
+ {:else}
+ {#each $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].localLore as book, i}
+ {#if i !== 0}
+
+ {/if}
+ {
+ let lore = $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].localLore
+ lore.splice(i, 1)
+ $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].localLore = lore
+ }}/>
+ {/each}
+ {/if}
+ {/if}
+
+
+
+
+
{addLorebook(submenu)}} class="hover:text-neutral-200 cursor-pointer">
+
+
+
{
+ exportLoreBook(submenu === 0 ? 'global' : 'local')
+ }} class="hover:text-neutral-200 ml-1 cursor-pointer">
+
+
+
{
+ importLoreBook(submenu === 0 ? 'global' : 'local')
+ }} class="hover:text-neutral-200 ml-2 cursor-pointer">
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/RegexData.svelte b/src/lib/SideBars/RegexData.svelte
new file mode 100644
index 00000000..de56752a
--- /dev/null
+++ b/src/lib/SideBars/RegexData.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/Settings.svelte b/src/lib/SideBars/Settings.svelte
new file mode 100644
index 00000000..b65e21ad
--- /dev/null
+++ b/src/lib/SideBars/Settings.svelte
@@ -0,0 +1,539 @@
+
+
+
+
+
+
+
+
+
+
+
+{#if subMenu === -1}
+ {language.userSetting}
+ {language.userIcon}
+ {selectUserImg()}}>
+ {#if $DataBase.userIcon === ''}
+
+ {:else}
+ {#await getCharImage($DataBase.userIcon, 'css')}
+
+ {:then im}
+
+ {/await}
+ {/if}
+
+ {language.username}
+
+
+{:else if subMenu === 0 && subSubMenu === 0}
+ {language.botSettings}
+
+ {
+ subSubMenu = 0
+ }} class="flex-1 border-solid border-borderc border-1 p-2 flex justify-center cursor-pointer" class:bg-selected={subSubMenu === 0}>
+ {language.Chat}
+
+ {
+ subSubMenu = 1
+ }} class="flex-1 border-solid border-borderc border-1 border-l-transparent p-2 flex justify-center cursor-pointer">
+ {language.others}
+
+
+ {language.model}
+
+ OpenAI GPT-3.5
+ OpenAI GPT-4
+ Text Generation WebUI
+ {#if $DataBase.plugins.length > 0}
+ Plugin
+ {/if}
+
+
+ {language.submodel}
+
+ OpenAI GPT-3.5
+ OpenAI GPT-4
+ Text Generation WebUI
+ {#if $customProviderStore.length > 0}
+ Plugin
+ {/if}
+
+
+ {#if $DataBase.aiModel === 'gpt35' || $DataBase.aiModel === 'gpt4' || $DataBase.subModel === 'gpt4' || $DataBase.subModel === 'gpt35'}
+ OpenAI {language.apiKey}
+
+ {/if}
+ {#if $DataBase.aiModel === 'custom'}
+ {language.plugin}
+
+ None
+ {#each $customProviderStore as plugin}
+ {plugin}
+ {/each}
+
+ {/if}
+ {#if $DataBase.aiModel === 'textgen_webui' || $DataBase.subModel === 'textgen_webui'}
+ TextGen {language.providerURL}
+
+ You must use WebUI without agpl license or use unmodified version with agpl license to observe the contents of the agpl license.
+ You must use textgen webui with --no-stream and without --cai-chat or --chat
+ {/if}
+ {language.mainPrompt}
+
+ {tokens.mainPrompt} {language.tokens}
+ {language.jailbreakPrompt}
+
+ {tokens.jailbreak} {language.tokens}
+ {language.globalNote}
+
+ {tokens.globalNote} {language.tokens}
+ {language.maxContextSize}
+ {#if $DataBase.aiModel === 'gpt35'}
+
+ {:else if $DataBase.aiModel === 'gpt4' || $DataBase.aiModel === 'textgen_webui'}
+
+ {:else if $DataBase.aiModel === 'custom'}
+
+ {/if}
+ {language.maxResponseSize}
+
+ {language.temperature}
+
+ {($DataBase.temperature / 100).toFixed(2)}
+ {language.frequencyPenalty}
+
+ {($DataBase.frequencyPenalty / 100).toFixed(2)}
+ {language.presensePenalty}
+
+ {($DataBase.PresensePenalty / 100).toFixed(2)}
+
+ {language.forceReplaceUrl}
+
+ {language.submodel} {language.forceReplaceUrl}
+
+
+
+
+
+ {language.advancedSettings}
+ {language.formatingOrder}
+
+ Bias
+
+
+
+
+ {language.promptPreprocess}
+
+
+
+ {openPresetList = true}} class="mt-4 drop-shadow-lg p-3 border-borderc border-solid flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected">{language.presets}
+
+
+{:else if subMenu === 0 && subSubMenu === 1}
+ {language.botSettings}
+
+ {
+ subSubMenu = 0
+ }} class="flex-1 border-solid border-borderc border-1 p-2 flex justify-center cursor-pointer">
+ {language.Chat}
+
+ {
+ subSubMenu = 1
+ }} class="flex-1 border-solid border-borderc border-1 border-l-transparent p-2 flex justify-center cursor-pointer" class:bg-selected={subSubMenu === 1}>
+ {language.others}
+
+
+ {language.imageGeneration}
+
+ {language.provider}
+
+ None
+ Stable Diffusion WebUI
+
+
+
+
+ {#if $DataBase.sdProvider === 'webui'}
+ You must use WebUI with --api flag
+ You must use WebUI without agpl license or use unmodified version with agpl license to observe the contents of the agpl license.
+ {#if !isTauri}
+ You are using web version. you must use ngrok or other tunnels to use your local webui.
+ {/if}
+ WebUI {language.providerURL}
+
+ {/if}
+
+
+ Steps
+
+
+ CFG Scale
+
+
+ Width
+
+ Height
+
+ Sampler
+
+
+{:else if subMenu === 3}
+ {language.display}
+ {language.UiLanguage}
+ {
+ await sleep(10)
+ changeLanguage($DataBase.language)
+ subMenu = -1
+ }}>
+ English
+ 한국어
+
+
+ {language.theme}
+
+ Standard Risu
+ Waifulike
+ WaifuCut
+
+
+
+
+
+ {#if $DataBase.theme === "waifu"}
+ {language.waifuWidth}
+
+ {($DataBase.waifuWidth)}%
+
+ {language.waifuWidth2}
+
+ {($DataBase.waifuWidth2)}%
+ {/if}
+
+ {language.textColor}
+
+ {language.classicRisu}
+ {language.highcontrast}
+ Custom
+
+
+ {#if $DataBase.textTheme === "custom"}
+
+
+ Normal Text
+
+
+
+ Italic Text
+
+
+
+ Bold Text
+
+
+
+ Italic Bold Text
+
+ {/if}
+
+
+ {#if isTauri}
+ {language.translator}
+
+ {language.disabled}
+ 한국어
+
+ {/if}
+ {language.UISize}
+
+ {($DataBase.zoomsize)}%
+
+ {language.iconSize}
+
+ {($DataBase.iconsize)}%
+
+ {#if isTauri}
+
+
+ {language.autoTranslation}
+
+ {/if}
+
+
+ {language.fullscreen}
+
+
+
+ {
+ if(check){
+ $DataBase.customBackground = '-'
+ const d = await selectSingleFile(['png', 'webp', 'gif'])
+ if(!d){
+ $DataBase.customBackground = ''
+ return
+ }
+ const img = await saveImage(d.data)
+ $DataBase.customBackground = img
+ }
+ else{
+ $DataBase.customBackground = ''
+ }
+ }}>
+ {language.useCustomBackground}
+
+
+
+
+ {language.playMessage}
+
+
+
+
+ {language.SwipeRegenerate}
+
+
+
+
+ {language.instantRemove}
+
+
+{:else if subMenu === 2}
+ {language.plugin}
+ {language.pluginWarn}
+
+
+
+ {#if $DataBase.plugins.length === 0}
+
No Plugins
+ {:else}
+ {#each $DataBase.plugins as plugin, i}
+ {#if i !== 0}
+
+ {/if}
+
+ {plugin.displayName ?? plugin.name}
+ {
+ const v = await alertConfirm(language.removeConfirm + (plugin.displayName ?? plugin.name))
+ if(v){
+ if($DataBase.currentPluginProvider === plugin.name){
+ $DataBase.currentPluginProvider = ''
+ }
+ let plugins = $DataBase.plugins
+ plugins.splice(i, 1)
+ $DataBase.plugins = plugins
+ }
+ }}>
+
+
+
+ {#if Object.keys(plugin.arguments).length > 0}
+
+ {#each Object.keys(plugin.arguments) as arg}
+ {arg}
+ {#if Array.isArray(plugin.arguments[arg])}
+
+ {#each plugin.arguments[arg] as a}
+ a
+ {/each}
+
+ {:else if plugin.arguments[arg] === 'string'}
+
+ {:else if plugin.arguments[arg] === 'int'}
+
+ {/if}
+ {/each}
+
+ {/if}
+ {/each}
+ {/if}
+
+
+
{
+ importPlugin()
+ }} class="hover:text-neutral-200 cursor-pointer">
+
+
+
+{:else if subMenu === 1}
+ {language.advancedSettings}
+ {language.advancedSettingsWarn}
+ {language.loreBookDepth}
+
+ {language.loreBookToken}
+
+
+ {language.additionalPrompt}
+
+
+ {language.descriptionPrefix}
+
+
+ {language.emotionPrompt}
+
+
+ {language.requestretrys}
+
+
+
+ {#if isTauri}
+ Request Lib
+
+ Reqwest
+ Tauri
+
+ {/if}
+
+
+
+ {language.sayNothing}
+
+
+ {
+ alertMd(getRequestLog())
+ }}
+ class="drop-shadow-lg p-3 border-borderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm">
+ {language.ShowLog}
+
+
+{:else if subMenu === 4}
+ {language.files}
+
+ {
+ if(await alertConfirm(language.backupConfirm)){
+ localStorage.setItem('backup', 'save')
+ if(isTauri){
+ checkDriver('savetauri')
+ }
+ else{
+ checkDriver('save')
+ }
+ }
+ }}
+ class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm">
+ {language.savebackup}
+
+
+ {
+ if((await alertConfirm(language.backupLoadConfirm)) && (await alertConfirm(language.backupLoadConfirm2))){
+ localStorage.setItem('backup', 'load')
+ if(isTauri){
+ checkDriver('loadtauri')
+ }
+ else{
+ checkDriver('load')
+ }
+ }
+ }}
+ class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm">
+ {language.loadbackup}
+
+
+{/if}
+
+
\ No newline at end of file
diff --git a/src/lib/SideBars/Sidebar.svelte b/src/lib/SideBars/Sidebar.svelte
new file mode 100644
index 00000000..d784e27c
--- /dev/null
+++ b/src/lib/SideBars/Sidebar.svelte
@@ -0,0 +1,190 @@
+
+
+
{
+ menuMode = 1 - menuMode
+ }}>
+
+ {#if menuMode === 0}
+ {#each charImages as charimg, i}
+
+ {#if charimg !== ''}
+
{changeChar(i)}} additionalStyle={getCharImage($DataBase.characters[i].image, 'css')}>
+
+ {:else}
+
{changeChar(i)}} additionalStyle={i === $selectedCharID ? 'background:#44475a' : ''}>
+
+
+ {/if}
+ {#if editMode}
+
+
{
+ let chars = $DataBase.characters
+ if(chars[i-1]){
+ const currentchar = chars[i]
+ chars[i] = chars[i-1]
+ chars[i-1] = currentchar
+ $DataBase.characters = chars
+ }
+ }}>
+
+
+
{
+ let chars = $DataBase.characters
+ if(chars[i+1]){
+ const currentchar = chars[i]
+ chars[i] = chars[i+1]
+ chars[i+1] = currentchar
+ $DataBase.characters = chars
+ }
+ }}>
+
+
+
+ {/if}
+
+ {/each}
+
{
+ if(sideBarMode === 1){
+ reseter();
+ sideBarMode = 0
+ }
+ else{
+ reseter();
+ sideBarMode = 1
+ }
+ }}>
+ {:else}
+
{
+ if($settingsOpen){
+ reseter();
+ settingsOpen.set(false)
+ }
+ else{
+ reseter();
+ settingsOpen.set(true)
+ }
+ }}>
+
{
+ reseter();
+ openGrid()
+ }}>
+ {/if}
+
+ 1000)}>
+ {sideBarStore.set(false)}}>
+
+
+ {#if sideBarMode === 0}
+ {#if $selectedCharID < 0 || $settingsOpen}
+
+ {:else}
+
+ {/if}
+ {:else if sideBarMode === 1}
+
Create
+
+ {language.createfromScratch}
+
+
+ {language.importCharacter}
+
+
+ {language.createGroup}
+
+ Edit
+ {editMode = !editMode;$selectedCharID = -1}}
+ class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected">
+ {language.editOrder}
+
+ {/if}
+
+
+
+
+{#if openPresetList}
+ {openPresetList = false}}/>
+{/if}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 00000000..02ce43c0
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,17 @@
+import "./styles.css";
+import App from "./App.svelte";
+import { loadData } from "./ts/globalApi";
+
+import { Buffer as BufferPolyfill } from 'buffer'
+import { initHotkey } from "./ts/hotkey";
+declare var Buffer: typeof BufferPolyfill;
+globalThis.Buffer = BufferPolyfill
+
+
+const app = new App({
+ target: document.getElementById("app"),
+});
+
+loadData()
+initHotkey()
+export default app;
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 00000000..aef65e9e
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,109 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body{
+ margin: 0;
+ padding: 0;
+ margin-top: 0px;
+ background-color: #282a36;
+ overflow-y: hidden;
+ overflow-x: hidden;
+}
+
+:root{
+ --FontColorStandard: #fafafa;
+ --FontColorBold : #fafafa;
+ --FontColorItalic : #8C8D93;
+ --FontColorItalicBold : #8C8D93;
+
+}
+
+html, body{
+ height: 100%
+}
+
+.chattext p{
+ color: var(--FontColorStandard);
+}
+
+.chattext2 pre{
+ background-color: #282a36;
+ padding: 0.5rem;
+ overflow-x: auto;
+}
+
+.chattext em{
+ color: var(--FontColorItalic);
+}
+
+.chattext strong{
+ color: var(--FontColorBold);
+}
+
+.chattext strong em{
+ color: var(--FontColorItalicBold);
+}
+
+::-webkit-scrollbar {
+ width: 5px;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+ background: #888;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+*{
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+.setting-area textarea{
+ height: 10rem;
+ min-height: 10rem;
+}
+
+.chattext p:first-child{
+ margin-top: 0.3rem;
+}
+
+.items-start {
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ -webkit-align-items: flex-start;
+ align-items: flex-start;
+}
+.items-start {
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ -webkit-align-items: flex-start;
+ align-items: flex-start;
+}
+
+.items-center {
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ -webkit-align-items: center;
+ align-items: center;
+}
+
+#app{
+ width: 100%;
+ height: 100%;
+}
+
+.input-text{
+ border: none;
+ outline: 0;
+ border-bottom: 1px solid #6272a4;
+}
\ No newline at end of file
diff --git a/src/ts/alert.ts b/src/ts/alert.ts
new file mode 100644
index 00000000..d29e3427
--- /dev/null
+++ b/src/ts/alert.ts
@@ -0,0 +1,114 @@
+import { get, writable } from "svelte/store"
+import { sleep } from "./util"
+import { language } from "../lang"
+
+interface alertData{
+ type: 'error'| 'normal'|'none'|'ask'|'wait'|'selectChar'|'input'|'toast'|'wait2'|'markdown'|'select'
+ msg: string
+}
+
+
+export const alertStore = writable({
+ type: 'none',
+ msg: 'n'
+} as alertData)
+
+export function alertError(msg:string){
+ console.error(msg)
+
+ alertStore.set({
+ 'type': 'error',
+ 'msg': msg
+ })
+}
+
+export function alertNormal(msg:string){
+ alertStore.set({
+ 'type': 'normal',
+ 'msg': msg
+ })
+}
+
+export async function alertSelect(msg:string[]){
+ alertStore.set({
+ 'type': 'select',
+ 'msg': msg.join('||')
+ })
+
+ while(true){
+ if (get(alertStore).type === 'none'){
+ break
+ }
+ await sleep(10)
+ }
+
+ return get(alertStore).msg
+}
+
+export function alertMd(msg:string){
+ alertStore.set({
+ 'type': 'markdown',
+ 'msg': msg
+ })
+}
+
+export function doingAlert(){
+ return get(alertStore).type !== 'none' && get(alertStore).type !== 'toast'
+}
+
+export function alertToast(msg:string){
+ alertStore.set({
+ 'type': 'toast',
+ 'msg': msg
+ })
+}
+
+export async function alertSelectChar(){
+ alertStore.set({
+ 'type': 'selectChar',
+ 'msg': ''
+ })
+
+ while(true){
+ if (get(alertStore).type === 'none'){
+ break
+ }
+ await sleep(10)
+ }
+
+ return get(alertStore).msg
+}
+
+export async function alertConfirm(msg:string){
+
+ alertStore.set({
+ 'type': 'ask',
+ 'msg': msg
+ })
+
+ while(true){
+ if (get(alertStore).type === 'none'){
+ break
+ }
+ await sleep(10)
+ }
+
+ return get(alertStore).msg === 'yes'
+}
+
+export async function alertInput(msg:string){
+
+ alertStore.set({
+ 'type': 'input',
+ 'msg': msg
+ })
+
+ while(true){
+ if (get(alertStore).type === 'none'){
+ break
+ }
+ await sleep(10)
+ }
+
+ return get(alertStore).msg
+}
\ No newline at end of file
diff --git a/src/ts/characters.ts b/src/ts/characters.ts
new file mode 100644
index 00000000..92bc5dcd
--- /dev/null
+++ b/src/ts/characters.ts
@@ -0,0 +1,686 @@
+import { get, writable } from "svelte/store";
+import { DataBase, saveImage, setDatabase, type character, type Chat, defaultSdDataFunc } from "./database";
+import exifr from 'exifr'
+import { alertConfirm, alertError, alertNormal, alertSelect, alertStore } from "./alert";
+import { language } from "../lang";
+import { PngMetadata } from "./exif";
+import { encode as encodeMsgpack, decode as decodeMsgpack } from "@msgpack/msgpack";
+import { checkNullish, findCharacterbyId, selectMultipleFile, selectSingleFile, sleep } from "./util";
+import { v4 as uuidv4 } from 'uuid';
+import { selectedCharID } from "./stores";
+import { downloadFile, getFileSrc, readImage } from "./globalApi";
+
+export function createNewCharacter() {
+ let db = get(DataBase)
+ db.characters.push(createBlankChar())
+ setDatabase(db)
+ return db.characters.length - 1
+}
+
+export function createNewGroup(){
+ let db = get(DataBase)
+ db.characters.push({
+ type: 'group',
+ name: "",
+ firstMessage: "",
+ chats: [{
+ message: [],
+ note: '',
+ name: 'Chat 1',
+ localLore: []
+ }], chatPage: 0,
+ viewScreen: 'none',
+ globalLore: [],
+ characters: [],
+ autoMode: false,
+ useCharacterLore: true,
+ emotionImages: [],
+ customscript: [],
+ chaId: uuidv4(),
+ })
+ setDatabase(db)
+ return db.characters.length - 1
+}
+
+export async function importCharacter() {
+ try {
+ const f = await selectSingleFile(['png', 'json'])
+ if(!f){
+ return
+ }
+ if(f.name.endsWith('json')){
+ const da = JSON.parse(Buffer.from(f.data).toString('utf-8'))
+ if((da.char_name || da.name) && (da.char_persona || da.description) && (da.char_greeting || da.first_mes)){
+ let db = get(DataBase)
+ db.characters.push({
+ name: da.char_name ?? da.name,
+ firstMessage: da.char_greeting ?? da.first_mes,
+ desc: da.char_persona ?? da.description,
+ notes: '',
+ chats: [{
+ message: [],
+ note: '',
+ name: 'Chat 1',
+ localLore: []
+ }],
+ chatPage: 0,
+ image: '',
+ emotionImages: [],
+ bias: [],
+ globalLore: [],
+ viewScreen: 'none',
+ chaId: uuidv4(),
+ sdData: defaultSdDataFunc(),
+ utilityBot: false,
+ customscript: [],
+ exampleMessage: ''
+ })
+ DataBase.set(db)
+ alertNormal(language.importedCharacter)
+ return
+ }
+ else{
+ alertError(language.errors.noData)
+ return
+ }
+ }
+ alertStore.set({
+ type: 'wait',
+ msg: 'Loading... (Reading)'
+ })
+ await sleep(10)
+ const img = f.data
+ const readed = (await exifr.parse(img, true))
+
+ console.log(readed)
+ if(readed.risuai){
+ await sleep(10)
+ const va = decodeMsgpack(Buffer.from(readed.risuai, 'base64')) as any
+ if(va.type !== 101){
+ alertError(language.errors.noData)
+ return
+ }
+
+
+ let char:character = va.data
+ let db = get(DataBase)
+ if(char.emotionImages && char.emotionImages.length > 0){
+ for(let i=0;i"
+ name: string
+ personality: ""
+ scenario: ""
+ talkativeness: "0.5"
+}
+
+export async function selectCharImg(charId:number) {
+ const selected = await selectSingleFile(['png'])
+ if(!selected){
+ return
+ }
+ const img = selected.data
+ let db = get(DataBase)
+ const imgp = await saveImage(img)
+ db.characters[charId].image = imgp
+ setDatabase(db)
+}
+
+export async function selectUserImg() {
+ const selected = await selectSingleFile(['png'])
+ if(!selected){
+ return
+ }
+ const img = selected.data
+ let db = get(DataBase)
+ const imgp = await saveImage(img)
+ db.userIcon = imgp
+ setDatabase(db)
+}
+
+export const addingEmotion = writable(false)
+
+export async function addCharEmotion(charId:number) {
+ addingEmotion.set(true)
+ const selected = await selectMultipleFile(['png', 'webp', 'gif'])
+ if(!selected){
+ addingEmotion.set(false)
+ return
+ }
+ let db = get(DataBase)
+ for(const f of selected){
+ console.log(f)
+ const img = f.data
+ const imgp = await saveImage(img)
+ const name = f.name.replace('.png','').replace('.webp','')
+ let dbChar = db.characters[charId]
+ if(dbChar.type !== 'group'){
+ dbChar.emotionImages.push([name,imgp])
+ db.characters[charId] = dbChar
+ }
+ setDatabase(db)
+ }
+ addingEmotion.set(false)
+}
+
+export async function rmCharEmotion(charId:number, emotionId:number) {
+ let db = get(DataBase)
+ let dbChar = db.characters[charId]
+ if(dbChar.type !== 'group'){
+ dbChar.emotionImages.splice(emotionId, 1)
+ db.characters[charId] = dbChar
+ }
+ setDatabase(db)
+}
+
+export async function exportChar(charaID:number) {
+ const db = get(DataBase)
+ let char:character = JSON.parse(JSON.stringify(db.characters[charaID]))
+
+ if(!char.image){
+ alertError('Image Required')
+ return
+ }
+ const conf = await alertConfirm(language.exportConfirm)
+ if(!conf){
+ return
+ }
+
+ alertStore.set({
+ type: 'wait',
+ msg: 'Loading...'
+ })
+
+ let img = await readImage(char.image)
+
+ try{
+ if(char.emotionImages && char.emotionImages.length > 0){
+ for(let i=0;i",
+ name: char.name,
+ personality: "",
+ scenario: "",
+ talkativeness: "0.5"
+ }
+
+ await sleep(10)
+ img = PngMetadata.write(img, {
+ 'chara': Buffer.from(JSON.stringify(tavernData)).toString('base64'),
+ 'risuai': data
+ })
+
+ alertStore.set({
+ type: 'wait',
+ msg: 'Loading... (Writing)'
+ })
+
+ char.image = ''
+ await sleep(10)
+ await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.png`, img)
+
+ alertNormal(language.successExport)
+
+ }
+ catch(e){
+ alertError(`${e}`)
+ }
+
+}
+
+
+export async function exportChat(page:number){
+ try {
+
+ const mode = await alertSelect(['Export as JSON', "Export as TXT"])
+ const selectedID = get(selectedCharID)
+ const db = get(DataBase)
+ const chat = db.characters[selectedID].chats[page]
+ const char = db.characters[selectedID]
+ const date = new Date().toJSON();
+ console.log(mode)
+ if(mode === '0'){
+ const stringl = Buffer.from(JSON.stringify({
+ type: 'risuChat',
+ ver: 1,
+ data: chat
+ }), 'utf-8')
+
+ await downloadFile(`${char.name}_${date}_chat`.replace(/[<>:"/\\|?*\.\,]/g, "") + '.json', stringl)
+
+ }
+ else{
+
+ let stringl = chat.message.map((v) => {
+ if(v.saying){
+ return `${findCharacterbyId(v.saying).name}\n${v.data}`
+ }
+ else{
+ return `${v.role === 'char' ? char.name : db.username}\n${v.data}`
+ }
+ }).join('\n\n')
+
+ if(char.type !== 'group'){
+ stringl = `${char.name}\n${char.firstMessage}\n\n` + stringl
+ }
+
+ await downloadFile(`${char.name}_${date}_chat`.replace(/[<>:"/\\|?*\.\,]/g, "") + '.txt', Buffer.from(stringl, 'utf-8'))
+
+ }
+ alertNormal(language.successExport)
+ } catch (error) {
+ alertError(`${error}`)
+ }
+}
+
+export async function importChat(){
+ const dat =await selectSingleFile(['json','jsonl'])
+ if(!dat){
+ return
+ }
+ try {
+ const selectedID = get(selectedCharID)
+ let db = get(DataBase)
+
+ if(dat.name.endsWith('jsonl')){
+ const lines = Buffer.from(dat.data).toString('utf-8').split('\n')
+ let newChat:Chat = {
+ message: [],
+ note: "",
+ name: "Imported Chat",
+ localLore: []
+ }
+
+ let isFirst = true
+ for(const line of lines){
+
+ const presedLine = JSON.parse(line)
+ if(presedLine.name && presedLine.is_user, presedLine.mes){
+ if(!isFirst){
+ newChat.message.push({
+ role: presedLine.is_user ? "user" : 'char',
+ data: formatTavernChat(presedLine.mes, db.characters[selectedID].name)
+ })
+ }
+ }
+
+ isFirst = false
+ }
+
+ if(newChat.message.length === 0){
+ alertError(language.errors.noData)
+ return
+ }
+
+ db.characters[selectedID].chats.push(newChat)
+ setDatabase(db)
+ alertNormal(language.successImport)
+ }
+ else{
+ const json = JSON.parse(Buffer.from(dat.data).toString('utf-8'))
+ if(json.type === 'risuChat' && json.ver === 1){
+ const das:Chat = json.data
+ if(!(checkNullish(das.message) || checkNullish(das.note) || checkNullish(das.name) || checkNullish(das.localLore))){
+ db.characters[selectedID].chats.push(das)
+ setDatabase(db)
+ alertNormal(language.successImport)
+ return
+ }
+ else{
+ alertError(language.errors.noData)
+ return
+ }
+ }
+ else{
+ alertError(language.errors.noData)
+ return
+ }
+ }
+
+ } catch (error) {
+ alertError(`${error}`)
+ }
+}
+
+function formatTavernChat(chat:string, charName:string){
+ const db = get(DataBase)
+ return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, db.username).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName)
+}
+
+export function characterFormatUpdate(index:number|character){
+ let db = get(DataBase)
+ let cha = typeof(index) === 'number' ? db.characters[index] : index
+ if(cha.chats.length === 0){
+ cha.chats = [{
+ message: [],
+ note: '',
+ name: 'Chat 1',
+ localLore: []
+ }]
+ }
+ if(!cha.chats[cha.chatPage]){
+ cha.chatPage = 0
+ }
+ if(!cha.chats[cha.chatPage].message){
+ cha.chats[cha.chatPage].message = []
+ }
+ if(!cha.type){
+ cha.type = 'character'
+ }
+ if(!cha.chaId){
+ cha.chaId = uuidv4()
+ }
+ if(cha.type !== 'group'){
+ if(checkNullish(cha.sdData)){
+ cha.sdData = defaultSdDataFunc()
+ }
+ if(checkNullish(cha.utilityBot)){
+ cha.utilityBot = false
+ }
+ }
+ if(checkNullish(cha.customscript)){
+ cha.customscript = []
+ }
+ if(typeof(index) === 'number'){
+ db.characters[index] = cha
+ setDatabase(db)
+ }
+ return cha
+}
+
+
+export function createBlankChar():character{
+ return {
+ name: '',
+ firstMessage: '',
+ desc: '',
+ notes: '',
+ chats: [{
+ message: [],
+ note: '',
+ name: 'Chat 1',
+ localLore: []
+ }],
+ chatPage: 0,
+ emotionImages: [],
+ bias: [],
+ viewScreen: 'none',
+ globalLore: [],
+ chaId: uuidv4(),
+ type: 'character',
+ sdData: defaultSdDataFunc(),
+ utilityBot: false,
+ customscript: [],
+ exampleMessage: ''
+ }
+}
+
+
+export async function makeGroupImage() {
+ try {
+ alertStore.set({
+ type: 'wait',
+ msg: `Loading..`
+ })
+ const db = get(DataBase)
+ const charID = get(selectedCharID)
+ const group = db.characters[charID]
+ if(group.type !== 'group'){
+ return
+ }
+
+ const imageUrls = await Promise.all(group.characters.map((v) => {
+ return getCharImage(findCharacterbyId(v).image, 'plain')
+ }))
+
+
+
+ const canvas = document.createElement("canvas");
+ canvas.width = 256
+ canvas.height = 256
+ const ctx = canvas.getContext("2d");
+
+ // Load the images
+ const images = [];
+ let loadedImages = 0;
+
+ await Promise.all(
+ imageUrls.map(
+ (url) =>
+ new Promise((resolve) => {
+ const img = new Image();
+ img.crossOrigin="anonymous"
+ img.onload = () => {
+ images.push(img);
+ resolve();
+ };
+ img.src = url;
+ })
+ )
+ );
+
+ // Calculate dimensions and draw the grid
+ const numImages = images.length;
+ const numCols = Math.ceil(Math.sqrt(images.length));
+ const numRows = Math.ceil(images.length / numCols);
+ const cellWidth = canvas.width / numCols;
+ const cellHeight = canvas.height / numRows;
+
+ for (let row = 0; row < numRows; row++) {
+ for (let col = 0; col < numCols; col++) {
+ const index = row * numCols + col;
+ if (index >= numImages) break;
+ ctx.drawImage(
+ images[index],
+ col * cellWidth,
+ row * cellHeight,
+ cellWidth,
+ cellHeight
+ );
+ }
+ }
+
+ // Return the image URI
+
+ const uri = canvas.toDataURL()
+ console.log(uri)
+ canvas.remove()
+ db.characters[charID].image = await saveImage(dataURLtoBuffer(uri));
+ setDatabase(db)
+ alertStore.set({
+ type: 'none',
+ msg: ''
+ })
+ } catch (error) {
+ alertError(`${error}`)
+ }
+}
+
+function dataURLtoBuffer(string:string){
+ const regex = /^data:.+\/(.+);base64,(.*)$/;
+
+ const matches = string.match(regex);
+ const ext = matches[1];
+ const data = matches[2];
+ return Buffer.from(data, 'base64');
+}
+
+export async function addDefaultCharacters() {
+ const imgs = [fetch('/sample/rika.png'),fetch('/sample/yuzu.png')]
+
+ alertStore.set({
+ type: 'wait',
+ msg: `Loading Sample bots...`
+ })
+
+ for(const img of imgs){
+ const imgBuffer = await (await img).arrayBuffer()
+ const readed = (await exifr.parse(imgBuffer, true))
+ await sleep(10)
+ const va = decodeMsgpack(Buffer.from(readed.risuai, 'base64')) as any
+ if(va.type !== 101){
+ alertError(language.errors.noData)
+ return
+ }
+ let char:character = va.data
+ let db = get(DataBase)
+ if(char.emotionImages && char.emotionImages.length > 0){
+ for(let i=0;i{
+ return JSON.parse(JSON.stringify(defaultSdData))
+}
+
+export function updateTextTheme(){
+ let db = get(DataBase)
+ const root = document.querySelector(':root') as HTMLElement;
+ if(!root){
+ return
+ }
+ switch(db.textTheme){
+ case "standard":{
+ root.style.setProperty('--FontColorStandard', '#fafafa');
+ root.style.setProperty('--FontColorItalic', '#8C8D93');
+ root.style.setProperty('--FontColorBold', '#fafafa');
+ root.style.setProperty('--FontColorItalicBold', '#8C8D93');
+ break
+ }
+ case "highcontrast":{
+ root.style.setProperty('--FontColorStandard', '#f8f8f2');
+ root.style.setProperty('--FontColorItalic', '#F1FA8C');
+ root.style.setProperty('--FontColorBold', '#8BE9FD');
+ root.style.setProperty('--FontColorItalicBold', '#FFB86C');
+ break
+ }
+ case "custom":{
+ root.style.setProperty('--FontColorStandard', db.customTextTheme.FontColorStandard);
+ root.style.setProperty('--FontColorItalic', db.customTextTheme.FontColorItalic);
+ root.style.setProperty('--FontColorBold', db.customTextTheme.FontColorBold);
+ root.style.setProperty('--FontColorItalicBold', db.customTextTheme.FontColorItalicBold);
+ break
+ }
+ }
+}
+
+export function changeToPreset(id =0){
+ let db = get(DataBase)
+ let pres = db.botPresets
+ pres[db.botPresetsId] = {
+ name: pres[db.botPresetsId].name,
+ apiType: db.apiType,
+ openAIKey: db.openAIKey,
+ mainPrompt:db.mainPrompt,
+ jailbreak: db.jailbreak,
+ globalNote: db.globalNote,
+ temperature: db.temperature,
+ maxContext: db.maxContext,
+ maxResponse: db.maxResponse,
+ frequencyPenalty: db.frequencyPenalty,
+ PresensePenalty: db.PresensePenalty,
+ formatingOrder: db.formatingOrder,
+ aiModel: db.aiModel,
+ subModel: db.subModel,
+ currentPluginProvider: db.currentPluginProvider,
+ textgenWebUIURL: db.textgenWebUIURL,
+ forceReplaceUrl: db.forceReplaceUrl,
+ forceReplaceUrl2: db.forceReplaceUrl2,
+ promptPreprocess: db.promptPreprocess,
+ bias: db.bias
+ }
+ db.botPresets = pres
+ const newPres = pres[id]
+ db.botPresetsId = id
+ db.apiType = newPres.apiType ?? db.apiType
+ db.openAIKey = newPres.openAIKey ?? db.openAIKey
+ db.mainPrompt = newPres.mainPrompt ?? db.mainPrompt
+ db.jailbreak = newPres.jailbreak ?? db.jailbreak
+ db.globalNote = newPres.globalNote ?? db.globalNote
+ db.temperature = newPres.temperature ?? db.temperature
+ db.maxContext = newPres.maxContext ?? db.maxContext
+ db.maxResponse = newPres.maxResponse ?? db.maxResponse
+ db.frequencyPenalty = newPres.frequencyPenalty ?? db.frequencyPenalty
+ db.PresensePenalty = newPres.PresensePenalty ?? db.PresensePenalty
+ db.formatingOrder = newPres.formatingOrder ?? db.formatingOrder
+ db.aiModel = newPres.aiModel ?? db.aiModel
+ db.subModel = newPres.subModel ?? db.subModel
+ db.currentPluginProvider = newPres.currentPluginProvider ?? db.currentPluginProvider
+ db.textgenWebUIURL = newPres.textgenWebUIURL ?? db.textgenWebUIURL
+ db.forceReplaceUrl = newPres.forceReplaceUrl ?? db.forceReplaceUrl
+ db.promptPreprocess = newPres.promptPreprocess ?? db.promptPreprocess
+ db.forceReplaceUrl2 = newPres.forceReplaceUrl2 ?? db.forceReplaceUrl2
+ db.bias = newPres.bias ?? db.bias
+ DataBase.set(db)
+}
\ No newline at end of file
diff --git a/src/ts/drive/drive.ts b/src/ts/drive/drive.ts
new file mode 100644
index 00000000..be2deda5
--- /dev/null
+++ b/src/ts/drive/drive.ts
@@ -0,0 +1,345 @@
+import { get } from "svelte/store";
+import { alertError, alertInput, alertNormal, alertStore } from "../alert";
+import { DataBase, setDatabase, type Database } from "../database";
+import { forageStorage, getUnpargeables, isTauri } from "../globalApi";
+import pako from "pako";
+import { BaseDirectory, readBinaryFile, readDir, writeBinaryFile } from "@tauri-apps/api/fs";
+import { language } from "../../lang";
+import { relaunch } from '@tauri-apps/api/process';
+import { open } from '@tauri-apps/api/shell';
+
+export async function checkDriver(type:'save'|'load'|'loadtauri'|'savetauri'){
+ const CLIENT_ID = '580075990041-l26k2d3c0nemmqiu3d3aag01npfrkn76.apps.googleusercontent.com';
+ const REDIRECT_URI = 'https://risu.pages.dev/';
+ const SCOPE = 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.appdata';
+ const encodedRedirectUri = encodeURIComponent(REDIRECT_URI);
+ const authorizationUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodedRedirectUri}&scope=${SCOPE}&response_type=code&state=${type}`;
+
+ if(type === 'save' || type === 'load'){
+ location.href = (authorizationUrl);
+ }
+ else{
+ try {
+ open(authorizationUrl)
+ let code = await alertInput(language.pasteAuthCode)
+ if(code.includes(' ')){
+ code = code.substring(code.lastIndexOf(' ')).trim()
+ }
+ if(type === 'loadtauri'){
+ await loadDrive(code)
+ }
+ else{
+ await backupDrive(code)
+ }
+ } catch (error) {
+ console.error(error)
+ alertError(`Backup Error: ${error}`)
+ }
+ }
+}
+
+
+export async function checkDriverInit() {
+ try {
+ const loc = new URLSearchParams(location.search)
+ const code = loc.get('code')
+
+ if(code){
+ const res = await fetch(`https://aichandict.xyz/api/drive/access?code=${encodeURIComponent(code)}`)
+ if(res.status >= 200 && res.status < 300){
+ const json:{
+ access_token:string,
+ expires_in:number
+ } = await res.json()
+ const da = loc.get('state')
+ if(da === 'save'){
+ await backupDrive(json.access_token)
+ }
+ else if(da === 'load'){
+ await loadDrive(json.access_token)
+ }
+ else if(da === 'savetauri' || da === 'loadtauri'){
+ alertStore.set({
+ type: 'wait2',
+ msg: `Copy and paste this Auth Code: ${json.access_token}`
+ })
+ }
+ }
+ else{
+ alertError(await res.text())
+ }
+ return true
+ }
+ else{
+ return false
+ }
+ } catch (error) {
+ console.error(error)
+ alertError(`Backup Error: ${error}`)
+ return true
+ }
+}
+
+
+
+
+async function backupDrive(ACCESS_TOKEN:string) {
+ alertStore.set({
+ type: "wait",
+ msg: "Uploading Backup..."
+ })
+
+ const files:DriveFile[] = await getFilesInFolder(ACCESS_TOKEN)
+
+ const fileNames = files.map((d) => {
+ return d.name
+ })
+
+ if(isTauri){
+ const assets = await readDir('assets', {dir: BaseDirectory.AppData})
+ let i = 0;
+ for(let asset of assets){
+ i += 1;
+ alertStore.set({
+ type: "wait",
+ msg: `Uploading Backup... (${i} / ${assets.length})`
+ })
+ const key = asset.name
+ if(!key || !key.endsWith('.png')){
+ continue
+ }
+ const formatedKey = formatKeys(key)
+ if(!fileNames.includes(formatedKey)){
+ await createFileInFolder(ACCESS_TOKEN, formatedKey, await readBinaryFile(asset.path))
+ }
+ }
+ }
+ else{
+ const keys = await forageStorage.keys()
+
+ for(let i=0;i {
+ return d.name
+ })
+
+ let latestDb:DriveFile = null
+ let latestDbDate = 0
+
+ for(const f of files){
+ if(f.name.endsWith("-database.risudat")){
+ const tm = parseInt(f.name.split('-')[0])
+ if(isNaN(tm)){
+ continue
+ }
+ else{
+ if(tm > latestDbDate){
+ latestDb = f
+ latestDbDate = tm
+ }
+ }
+ }
+ }
+ if(latestDbDate !== 0){
+ const db:Database = JSON.parse(Buffer.from(pako.inflate(await getFileData(ACCESS_TOKEN, latestDb.id))).toString('utf-8'))
+ const requiredImages = (getUnpargeables(db))
+ let ind = 0;
+ for(const images of requiredImages){
+ ind += 1
+ const formatedImage = formatKeys(images)
+ alertStore.set({
+ type: "wait",
+ msg: `Loading Backup... (${ind} / ${requiredImages.length})`
+ })
+ if(await checkImageExists(images)){
+ //skip process
+ }
+ else{
+ if(formatedImage.length >= 7){
+ if(fileNames.includes(formatedImage)){
+ for(const file of files){
+ if(file.name === formatedImage){
+ const fData = await getFileData(ACCESS_TOKEN, file.id)
+ if(isTauri){
+ await writeBinaryFile(`assets/` + images, fData ,{dir: BaseDirectory.AppData})
+
+ }
+ else{
+ await forageStorage.setItem('assets/' + images, fData)
+ }
+ }
+ }
+ }
+ else{
+ throw `cannot find file in drive: ${formatedImage}`
+ }
+ }
+ }
+ }
+ const dbjson = JSON.stringify(db)
+ const dbData = pako.deflate(
+ Buffer.from(dbjson, 'utf-8')
+ )
+ if(isTauri){
+ await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData})
+ relaunch()
+ alertStore.set({
+ type: "wait",
+ msg: "Success, Refresh your app."
+ })
+ }
+ else{
+ await forageStorage.setItem('database/database.bin', dbData)
+ location.search = ''
+ alertStore.set({
+ type: "wait",
+ msg: "Success, Refresh your app."
+ })
+ }
+ }
+}
+
+function checkImageExist(image:string){
+
+}
+
+
+function formatKeys(name:string) {
+ return getBasename(name).replace(/\_/g, '__').replace(/\./g,'_d').replace(/\//,'_s') + '.png'
+}
+
+async function getFilesInFolder(ACCESS_TOKEN:string, nextPageToken=''): Promise {
+ const url = `https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&pageSize=300` + nextPageToken;
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${ACCESS_TOKEN}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if(data.nextPageToken){
+ return (data.files as DriveFile[]).concat(await getFilesInFolder(ACCESS_TOKEN, `&pageToken=${data.nextPageToken}`))
+ }
+ return data.files as DriveFile[];
+ } else {
+ throw(`Error: ${response.status}`);
+ }
+}
+
+async function createFileInFolder(accessToken:string, fileName:string, content:Uint8Array, mimeType = 'application/octet-stream') {
+ const metadata = {
+ name: fileName,
+ mimeType: mimeType,
+ parents: ["appDataFolder"],
+ };
+
+ const body = new FormData();
+ body.append(
+ "metadata",
+ new Blob([JSON.stringify(metadata)], { type: "application/json" })
+ );
+ body.append("file", new Blob([content], { type: mimeType }));
+
+ const response = await fetch(
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: body,
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok) {
+ return result;
+ } else {
+ console.error("Error creating file:", result);
+ throw new Error(result.error.message);
+ }
+}
+
+const baseNameRegex = /\\/g
+function getBasename(data:string){
+ const splited = data.replace(baseNameRegex, '/').split('/')
+ const lasts = splited[splited.length-1]
+ return lasts
+}
+
+async function getFileData(ACCESS_TOKEN:string,fileId:string) {
+ const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
+
+ const request = {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${ACCESS_TOKEN}`
+ }
+ };
+
+ const response = await fetch(url, request);
+
+ if (response.ok) {
+ const data = new Uint8Array(await response.arrayBuffer());
+ return data;
+ } else {
+ throw "Error in response when reading files in folder"
+ }
+ }
\ No newline at end of file
diff --git a/src/ts/exif.ts b/src/ts/exif.ts
new file mode 100644
index 00000000..6d01035b
--- /dev/null
+++ b/src/ts/exif.ts
@@ -0,0 +1,36 @@
+import extract from 'png-chunks-extract';
+import encode from 'png-chunks-encode';
+import textKey from 'png-chunk-text'
+
+export const PngMetadata = {
+ write: (pngBuffer: Uint8Array, metadata: Record): Buffer => {
+ let chunks:{
+ name:string
+ data:Uint8Array
+ }[] = extract(Buffer.from(pngBuffer));
+
+ chunks = chunks.filter((v) => {
+ return v.name.toLocaleLowerCase() !== 'text'
+ })
+
+ for (const key in metadata) {
+ const value = metadata[key];
+ chunks.splice(-1, 0, textKey.encode(key, value))
+ }
+ const encoded = encode(chunks);
+ return encoded
+ },
+ filter: (pngBuffer: Uint8Array) => {
+ let chunks:{
+ name:string
+ data:Uint8Array
+ }[] = extract(Buffer.from(pngBuffer));
+
+ chunks = chunks.filter((v) => {
+ return v.name.toLocaleLowerCase() !== 'text'
+ })
+
+ const encoded = encode(chunks);
+ return encoded
+ }
+}
\ No newline at end of file
diff --git a/src/ts/globalApi.ts b/src/ts/globalApi.ts
new file mode 100644
index 00000000..73ea815a
--- /dev/null
+++ b/src/ts/globalApi.ts
@@ -0,0 +1,557 @@
+import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs"
+import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "./util"
+import localforage from 'localforage'
+import { convertFileSrc, invoke } from "@tauri-apps/api/tauri"
+import { v4 as uuidv4 } from 'uuid';
+import { appDataDir, join } from "@tauri-apps/api/path";
+import { get } from "svelte/store";
+import { DataBase, loadedStore, setDatabase, type Database, updateTextTheme, defaultSdDataFunc } from "./database";
+import pako from "pako";
+import { appWindow } from "@tauri-apps/api/window";
+import { checkUpdate } from "./update";
+import { selectedCharID } from "./stores";
+import { Body, ResponseType, fetch as TauriFetch } from "@tauri-apps/api/http";
+import { loadPlugins } from "./process/plugins";
+import { alertError, alertStore } from "./alert";
+import { checkDriverInit } from "./drive/drive";
+import { hasher } from "./parser";
+
+//@ts-ignore
+export const isTauri = !!window.__TAURI__
+export const forageStorage = localforage.createInstance({
+ name: "risuai"
+})
+
+interface fetchLog{
+ body:string
+ header:string
+ response:string
+ success:boolean,
+ date:string
+ url:string
+}
+
+let fetchLog:fetchLog[] = []
+
+export async function downloadFile(name:string, data:Uint8Array) {
+ const downloadURL = (data:string, fileName:string) => {
+ const a = document.createElement('a')
+ a.href = data
+ a.download = fileName
+ document.body.appendChild(a)
+ a.style.display = 'none'
+ a.click()
+ a.remove()
+ }
+
+ if(isTauri){
+ await writeBinaryFile(name, data, {dir: BaseDirectory.Download})
+ }
+ else{
+ downloadURL(`data:png/image;base64,${Buffer.from(data).toString('base64')}`, name)
+ }
+}
+
+let fileCache:{
+ origin: string[], res:(Uint8Array|'loading'|'done')[]
+} = {
+ origin: [],
+ res: []
+}
+
+let pathCache:{[key:string]:string} = {}
+let checkedPaths:string[] = []
+
+export async function getFileSrc(loc:string) {
+ if(isTauri){
+ if(loc.startsWith('assets')){
+ if(appDataDirPath === ''){
+ appDataDirPath = await appDataDir();
+ }
+ const cached = pathCache[loc]
+ if(cached){
+ return convertFileSrc(cached)
+ }
+ else{
+ const joined = await join(appDataDirPath,loc)
+ pathCache[loc] = joined
+ return convertFileSrc(joined)
+ }
+ }
+ return convertFileSrc(loc)
+ }
+ try {
+ if(usingSw){
+ const encoded = Buffer.from(loc,'utf-8').toString('hex')
+ let ind = fileCache.origin.indexOf(loc)
+ if(ind === -1){
+ ind = fileCache.origin.length
+ fileCache.origin.push(loc)
+ fileCache.res.push('loading')
+ try {
+ const hasCache:boolean = (await (await fetch("/sw/check/" + encoded)).json()).able
+ if(hasCache){
+ fileCache.res[ind] = 'done'
+ return "/sw/img/" + encoded
+ }
+ else{
+ const f:Uint8Array = await forageStorage.getItem(loc)
+ await fetch("/sw/register/" + encoded, {
+ method: "POST",
+ body: f
+ })
+ fileCache.res[ind] = 'done'
+ await sleep(10)
+ }
+ return "/sw/img/" + encoded
+ } catch (error) {
+ location.reload()
+ }
+ }
+ else{
+ const f = fileCache.res[ind]
+ if(f === 'loading'){
+ while(fileCache.res[ind] === 'loading'){
+ await sleep(10)
+ }
+ }
+ return "/sw/img/" + encoded
+ }
+ }
+ else{
+ let ind = fileCache.origin.indexOf(loc)
+ if(ind === -1){
+ ind = fileCache.origin.length
+ fileCache.origin.push(loc)
+ fileCache.res.push('loading')
+ const f:Uint8Array = await forageStorage.getItem(loc)
+ fileCache.res[ind] = f
+ return `data:image/png;base64,${Buffer.from(f).toString('base64')}`
+ }
+ else{
+ const f = fileCache.res[ind]
+ if(f === 'loading'){
+ while(fileCache.res[ind] === 'loading'){
+ await sleep(10)
+ }
+ return `data:image/png;base64,${Buffer.from(fileCache.res[ind]).toString('base64')}`
+ }
+ return `data:image/png;base64,${Buffer.from(f).toString('base64')}`
+ }
+ }
+ } catch (error) {
+ console.error(error)
+ return ''
+ }
+}
+
+let appDataDirPath = ''
+
+export async function readImage(data:string) {
+ if(isTauri){
+ if(data.startsWith('assets')){
+ if(appDataDirPath === ''){
+ appDataDirPath = await appDataDir();
+ }
+ return await readBinaryFile(await join(appDataDirPath,data))
+ }
+ return await readBinaryFile(data)
+ }
+ else{
+ return (await forageStorage.getItem(data) as Uint8Array)
+ }
+}
+
+export async function saveImage(data:Uint8Array, customId:string = ''){
+ let id = ''
+ if(customId !== ''){
+ id = customId
+ }
+ else{
+ try {
+ id = await hasher(data)
+ } catch (error) {
+ id = uuidv4()
+ }
+ }
+ if(isTauri){
+ await writeBinaryFile(`assets/${id}.png`, data ,{dir: BaseDirectory.AppData})
+ return `assets/${id}.png`
+ }
+ else{
+ await forageStorage.setItem(`assets/${id}.png`, data)
+ return `assets/${id}.png`
+ }
+}
+
+let lastSave = ''
+
+export async function saveDb(){
+ lastSave =JSON.stringify(get(DataBase))
+ while(true){
+ const dbjson = JSON.stringify(get(DataBase))
+ if(dbjson !== lastSave){
+ lastSave = dbjson
+ const dbData = pako.deflate(
+ Buffer.from(dbjson, 'utf-8')
+ )
+ if(isTauri){
+ await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData})
+ }
+ else{
+ await forageStorage.setItem('database/database.bin', dbData)
+ }
+ console.log('saved')
+ }
+
+ await sleep(500)
+ }
+}
+
+let usingSw = false
+
+export async function loadData() {
+ const loaded = get(loadedStore)
+ if(!loaded){
+ try {
+ if(isTauri){
+ appWindow.maximize()
+ if(!await exists('', {dir: BaseDirectory.AppData})){
+ await createDir('', {dir: BaseDirectory.AppData})
+ }
+ if(!await exists('database', {dir: BaseDirectory.AppData})){
+ await createDir('database', {dir: BaseDirectory.AppData})
+ }
+ if(!await exists('assets', {dir: BaseDirectory.AppData})){
+ await createDir('assets', {dir: BaseDirectory.AppData})
+ }
+ if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){
+ await writeBinaryFile('database/database.bin',
+ pako.deflate(Buffer.from(JSON.stringify({}), 'utf-8'))
+ ,{dir: BaseDirectory.AppData})
+ }
+ setDatabase(
+ JSON.parse(Buffer.from(pako.inflate(Buffer.from(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData})))).toString('utf-8'))
+ )
+ await checkUpdate()
+ await changeFullscreen()
+
+ }
+ else{
+ let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin')
+ if(checkNullish(gotStorage)){
+ gotStorage = pako.deflate(Buffer.from(JSON.stringify({}), 'utf-8'))
+ await forageStorage.setItem('database/database.bin', gotStorage)
+ }
+ setDatabase(
+ JSON.parse(Buffer.from(pako.inflate(Buffer.from(gotStorage))).toString('utf-8'))
+ )
+ const isDriverMode = await checkDriverInit()
+ if(navigator.serviceWorker){
+ usingSw = true
+ const rej = await navigator.serviceWorker.register("/sw.js", {
+ scope: "/"
+ });
+ }
+ else{
+ usingSw = false
+ }
+
+
+ }
+ try {
+ await pargeChunks()
+ } catch (error) {}
+ try {
+ await loadPlugins()
+ } catch (error) {}
+ await checkNewFormat()
+ updateTextTheme()
+ loadedStore.set(true)
+ selectedCharID.set(-1)
+ saveDb()
+ } catch (error) {
+ alertError(`${error}`)
+ }
+ }
+}
+
+export async function globalFetch(url:string, arg:{body?:any,headers?:{[key:string]:string}, rawResponse?:boolean, method?:"POST"|"GET"}) {
+ const db = get(DataBase)
+ const method = arg.method ?? "POST"
+
+ function addFetchLog(response:any, success:boolean){
+ try{
+ fetchLog.unshift({
+ body: JSON.stringify(arg.body, null, 2),
+ header: JSON.stringify(arg.headers ?? {}, null, 2),
+ response: JSON.stringify(response, null, 2),
+ success: success,
+ date: (new Date()).toLocaleTimeString(),
+ url: url
+ })
+ }
+ catch{
+ fetchLog.unshift({
+ body: JSON.stringify(arg.body, null, 2),
+ header: JSON.stringify(arg.headers ?? {}, null, 2),
+ response: `${response}`,
+ success: success,
+ date: (new Date()).toLocaleTimeString(),
+ url: url
+ })
+ }
+ }
+
+ if(isTauri){
+
+ if(db.requester === 'new'){
+ try {
+ let preHeader = arg.headers ?? {}
+ preHeader["Content-Type"] = `application/json`
+ const body = JSON.stringify(arg.body)
+ const header = JSON.stringify(preHeader)
+ const res:string = await invoke('native_request', {url:url, body:body, header:header, method: method})
+ const d:{
+ success: boolean
+ body:string
+ } = JSON.parse(res)
+
+ if(!d.success){
+ addFetchLog(Buffer.from(d.body, 'base64').toString('utf-8'), false)
+ return {
+ ok:false,
+ data: Buffer.from(d.body, 'base64').toString('utf-8')
+ }
+ }
+ else{
+ if(arg.rawResponse){
+ addFetchLog("Uint8Array Response", true)
+ return {
+ ok:true,
+ data: new Uint8Array(Buffer.from(d.body, 'base64'))
+ }
+ }
+ else{
+ addFetchLog(JSON.parse(Buffer.from(d.body, 'base64').toString('utf-8')), true)
+ return {
+ ok:true,
+ data: JSON.parse(Buffer.from(d.body, 'base64').toString('utf-8'))
+ }
+ }
+ }
+ } catch (error) {
+ return {
+ ok: false,
+ data: `${error}`,
+ }
+ }
+ }
+
+ const body = Body.json(arg.body)
+ const headers = arg.headers ?? {}
+ const d = await TauriFetch(url, {
+ body: body,
+ method: method,
+ headers: headers,
+ timeout: {
+ secs: db.timeOut,
+ nanos: 0
+ },
+ responseType: arg.rawResponse ? ResponseType.Binary : ResponseType.JSON
+ })
+ if(arg.rawResponse){
+ addFetchLog("Uint8Array Response", d.ok)
+ return {
+ ok: d.ok,
+ data: new Uint8Array(d.data as number[]),
+ }
+ }
+ else{
+ addFetchLog(d.data, d.ok)
+ return {
+ ok: d.ok,
+ data: d.data,
+ }
+ }
+ }
+ else{
+ try {
+ let headers = arg.headers ?? {}
+ if(!headers["Content-Type"]){
+ headers["Content-Type"] = `application/json`
+ }
+ if(arg.rawResponse){
+ const furl = new URL("https://risu.pages.dev/proxy")
+ furl.searchParams.set("url", url)
+
+ const da = await fetch(furl, {
+ body: JSON.stringify(arg.body),
+ headers: arg.headers,
+ method: method
+ })
+
+ addFetchLog("Uint8Array Response", da.ok)
+ return {
+ ok: da.ok,
+ data: new Uint8Array(await da.arrayBuffer())
+ }
+ }
+ else{
+ const furl = new URL("https://risu.pages.dev/proxy")
+ furl.searchParams.set("url", url)
+
+
+ const da = await fetch(furl, {
+ body: JSON.stringify(arg.body),
+ headers: arg.headers,
+ method: method
+ })
+
+ const dat = await da.json()
+ addFetchLog(dat, da.ok)
+ return {
+ ok: da.ok,
+ data: dat
+ }
+ }
+ } catch (error) {
+ return {
+ ok:false,
+ data: `${error}`
+ }
+ }
+ }
+}
+
+const re = /\\/g
+function getBasename(data:string){
+ const splited = data.replace(re, '/').split('/')
+ const lasts = splited[splited.length-1]
+ return lasts
+}
+
+export function getUnpargeables(db:Database) {
+ let unpargeable:string[] = []
+
+ function addParge(data:string){
+ if(!data){
+ return
+ }
+ if(data === ''){
+ return
+ }
+ const bn = getBasename(data)
+ if(!unpargeable.includes(bn)){
+ unpargeable.push(getBasename(data))
+ }
+ }
+
+ addParge(db.customBackground)
+ addParge(db.userIcon)
+
+ for(const cha of db.characters){
+ if(cha.image){
+ addParge(cha.image)
+ }
+ if(cha.emotionImages){
+ for(const em of cha.emotionImages){
+ addParge(em[1])
+ }
+ }
+ }
+ return unpargeable
+}
+
+async function checkNewFormat() {
+ let db = get(DataBase)
+
+ if(!db.formatversion){
+ function checkParge(data:string){
+
+ if(data.startsWith('assets') || (data.length < 3)){
+ return data
+ }
+ else{
+ const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1])
+ if(!d){
+ return data
+ }
+ return d
+ }
+ }
+
+ db.customBackground = checkParge(db.customBackground)
+ db.userIcon = checkParge(db.userIcon)
+
+ for(let i=0;i= 2){
+ db.characters[i].emotionImages[i2][1] = checkParge(db.characters[i].emotionImages[i2][1])
+ }
+ }
+ }
+ }
+
+ db.formatversion = 2
+ }
+ if(db.formatversion < 3){
+
+ for(let i=0;i {
+ if(ev.ctrlKey){
+ switch (ev.key){
+ case "1":{
+ changeToPreset(0)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "2":{
+ changeToPreset(1)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "3":{
+ changeToPreset(2)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "4":{
+ changeToPreset(3)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "5":{
+ changeToPreset(4)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "6":{
+ changeToPreset(5)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "7":{
+ changeToPreset(6)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "8":{
+ changeToPreset(7)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ case "9":{
+ changeToPreset(8)
+ ev.preventDefault()
+ ev.stopPropagation()
+ break
+ }
+ }
+ }
+ })
+}
+
+function changeToPreset(num:number){
+ if(!doingAlert()){
+ let db = get(DataBase)
+ let pres = db.botPresets
+ if(pres.length > num){
+ alertToast(`Changed to Preset ${num+1}`)
+ changeToPreset2(num)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ts/lorebook.ts b/src/ts/lorebook.ts
new file mode 100644
index 00000000..6a0a0770
--- /dev/null
+++ b/src/ts/lorebook.ts
@@ -0,0 +1,170 @@
+import { get } from "svelte/store";
+import {selectedCharID} from './stores'
+import { DataBase, setDatabase, type loreBook } from "./database";
+import { tokenize } from "./tokenizer";
+import { selectSingleFile } from "./util";
+import { alertError, alertNormal } from "./alert";
+import { language } from "../lang";
+import { downloadFile } from "./globalApi";
+
+export function addLorebook(type:number) {
+ let selectedID = get(selectedCharID)
+ let db = get(DataBase)
+ if(type === 0){
+ db.characters[selectedID].globalLore.push({
+ key: '',
+ comment: `New Lore ${db.characters[selectedID].globalLore.length + 1}`,
+ content: '',
+ mode: 'normal',
+ insertorder: 100,
+ alwaysActive: false
+ })
+ }
+ else{
+ const page = db.characters[selectedID].chatPage
+ db.characters[selectedID].chats[page].localLore.push({
+ key: '',
+ comment: `New Lore ${db.characters[selectedID].chats[page].localLore.length + 1}`,
+ content: '',
+ mode: 'normal',
+ insertorder: 100,
+ alwaysActive: false
+ })
+ }
+ setDatabase(db)
+}
+
+interface formatedLore{
+ keys:string[]|'always'
+ content: string
+ order: number
+}
+
+const rmRegex = / |\n/g
+
+export async function loadLoreBookPrompt(){
+ const selectedID = get(selectedCharID)
+ const db = get(DataBase)
+ const page = db.characters[selectedID].chatPage
+ const globalLore = db.characters[selectedID].globalLore
+ const charLore = db.characters[selectedID].chats[page].localLore
+ const fullLore = globalLore.concat(charLore)
+ const currentChat = db.characters[selectedID].chats[page].message
+
+ let activatiedPrompt: string[] = []
+
+ let formatedLore:formatedLore[] = []
+
+ for (const lore of fullLore){
+ if(lore.key.length > 1 || lore.alwaysActive){
+ formatedLore.push({
+ keys: lore.alwaysActive ? 'always' : lore.key.replace(rmRegex, '').toLocaleLowerCase().split(',').filter((a) => {
+ return a.length > 1
+ }),
+ content: lore.content,
+ order: lore.insertorder
+ })
+ }
+ }
+
+ formatedLore.sort((a, b) => {
+ return b.order - a.order
+ })
+
+ const formatedChat = currentChat.slice(currentChat.length - db.loreBookDepth,currentChat.length).map((msg) => {
+ return msg.data
+ }).join('||').replace(rmRegex,'').toLocaleLowerCase()
+
+ for(const lore of formatedLore){
+ const totalTokens = await tokenize(activatiedPrompt.concat([lore.content]).join('\n\n'))
+ if(totalTokens > db.loreBookToken){
+ break
+ }
+
+ if(lore.keys === 'always'){
+ activatiedPrompt.push(lore.content)
+ continue
+ }
+
+ for(const key of lore.keys){
+ if(formatedChat.includes(key)){
+ activatiedPrompt.push(lore.content)
+ break
+ }
+ }
+ }
+
+ return activatiedPrompt.reverse().join('\n\n')
+}
+
+
+export async function importLoreBook(mode:'global'|'local'){
+ const selectedID = get(selectedCharID)
+ let db = get(DataBase)
+ const page = db.characters[selectedID].chatPage
+ let lore = mode === 'global' ? db.characters[selectedID].globalLore : db.characters[selectedID].chats[page].localLore
+ const lorebook = (await selectSingleFile(['json'])).data
+ if(!lorebook){
+ return
+ }
+
+ try {
+ const importedlore = JSON.parse(Buffer.from(lorebook).toString('utf-8'))
+ if(importedlore.type === 'risu' && importedlore.data){
+ const datas:loreBook[] = importedlore.data
+ for(const data of datas){
+ lore.push(data)
+ }
+ }
+ else if(importedlore.entries){
+ const entries:{[key:string]:{
+ key:string[]
+ comment:string
+ content:string
+ order:number
+ constant:boolean
+ }} = importedlore.entries
+ for(const key in entries){
+ const currentLore = entries[key]
+ lore.push({
+ key: currentLore.key.join(', '),
+ insertorder: currentLore.order,
+ comment: currentLore.comment.length < 1 ? 'Unnamed Imported Lore': currentLore.comment,
+ content: currentLore.content,
+ mode: "normal",
+ alwaysActive: currentLore.constant
+ })
+ }
+ }
+ if(mode === 'global'){
+ db.characters[selectedID].globalLore = lore
+ }
+ else{
+ db.characters[selectedID].chats[page].localLore = lore
+ }
+ setDatabase(db)
+ } catch (error) {
+ alertError(`${error}`)
+ }
+}
+
+export async function exportLoreBook(mode:'global'|'local'){
+ try {
+ const selectedID = get(selectedCharID)
+ const db = get(DataBase)
+ const page = db.characters[selectedID].chatPage
+ const lore = mode === 'global' ? db.characters[selectedID].globalLore : db.characters[selectedID].chats[page].localLore
+
+ const stringl = Buffer.from(JSON.stringify({
+ type: 'risu',
+ ver: 1,
+ data: lore
+ }), 'utf-8')
+
+ await downloadFile(`lorebook_export.json`, stringl)
+
+ alertNormal(language.successExport)
+ } catch (error) {
+ alertError(`${error}`)
+ }
+}
\ No newline at end of file
diff --git a/src/ts/parser.ts b/src/ts/parser.ts
new file mode 100644
index 00000000..f8faf970
--- /dev/null
+++ b/src/ts/parser.ts
@@ -0,0 +1,15 @@
+import DOMPurify from 'isomorphic-dompurify';
+import showdown from 'showdown';
+
+const convertor = new showdown.Converter()
+convertor.setOption('simpleLineBreaks', true);
+
+export function ParseMarkdown(data:string) {
+ return DOMPurify.sanitize(convertor.makeHtml(data), {
+ FORBID_TAGS: ['a']
+ })
+}
+
+export async function hasher(data:Uint8Array){
+ return Buffer.from(await crypto.subtle.digest("SHA-256", data)).toString('hex');
+}
\ No newline at end of file
diff --git a/src/ts/process/index.ts b/src/ts/process/index.ts
new file mode 100644
index 00000000..4a1896b9
--- /dev/null
+++ b/src/ts/process/index.ts
@@ -0,0 +1,474 @@
+import { get, writable } from "svelte/store";
+import { DataBase, setDatabase, type character } from "../database";
+import { CharEmotion, selectedCharID } from "../stores";
+import { tokenize, tokenizeNum } from "../tokenizer";
+import { language } from "../../lang";
+import { alertError } from "../alert";
+import { loadLoreBookPrompt } from "../lorebook";
+import { findCharacterbyId, replacePlaceholders } from "../util";
+import { requestChatData } from "./request";
+import { stableDiff } from "./stableDiff";
+import { processScript } from "./scripts";
+
+export interface OpenAIChat{
+ role: 'system'|'user'|'assistant'
+ content: string
+}
+
+export const doingChat = writable(false)
+
+export async function sendChat(chatProcessIndex = -1):Promise {
+
+ let findCharCache:{[key:string]:character} = {}
+ function findCharacterbyIdwithCache(id:string){
+ const d = findCharCache[id]
+ if(!!d){
+ return d
+ }
+ else{
+ const r = findCharacterbyId(id)
+ findCharCache[id] = r
+ return r
+ }
+ }
+
+ function reformatContent(data:string){
+ return data.trim().replace(`${currentChar.name}:`, '').trim()
+ }
+
+ let isDoing = get(doingChat)
+
+ if(isDoing){
+ if(chatProcessIndex === -1){
+ return false
+ }
+ }
+ doingChat.set(true)
+
+ let db = get(DataBase)
+ let selectedChar = get(selectedCharID)
+ const nowChatroom = db.characters[selectedChar]
+ let currentChar:character
+
+ if(nowChatroom.type === 'group'){
+ if(chatProcessIndex === -1){
+ for(let i=0;i 4000){
+ maxContextTokens = 4000
+ }
+ }
+ if(db.aiModel === 'gpt4'){
+ if(maxContextTokens > 8000){
+ maxContextTokens = 8000
+ }
+ }
+
+ let unformated = {
+ 'main':([] as OpenAIChat[]),
+ 'jailbreak':([] as OpenAIChat[]),
+ 'chats':([] as OpenAIChat[]),
+ 'lorebook':([] as OpenAIChat[]),
+ 'globalNote':([] as OpenAIChat[]),
+ 'authorNote':([] as OpenAIChat[]),
+ 'lastChat':([] as OpenAIChat[]),
+ 'description':([] as OpenAIChat[]),
+ }
+
+ if(!currentChar.utilityBot){
+ unformated.main.push({
+ role: 'system',
+ content: replacePlaceholders(db.mainPrompt + ((db.additionalPrompt === '' || (!db.promptPreprocess)) ? '' : `\n${db.additionalPrompt}`), currentChar.name)
+ })
+
+ if(db.jailbreakToggle){
+ unformated.jailbreak.push({
+ role: 'system',
+ content: replacePlaceholders(db.jailbreak, currentChar.name)
+ })
+ }
+
+ unformated.globalNote.push({
+ role: 'system',
+ content: replacePlaceholders(db.globalNote, currentChar.name)
+ })
+ }
+
+ unformated.authorNote.push({
+ role: 'system',
+ content: replacePlaceholders(currentChat.note, currentChar.name)
+ })
+
+ unformated.description.push({
+ role: 'system',
+ content: replacePlaceholders((db.promptPreprocess ? db.descriptionPrefix: '') + currentChar.desc, currentChar.name)
+ })
+
+ unformated.lorebook.push({
+ role: 'system',
+ content: replacePlaceholders(await loadLoreBookPrompt(), currentChar.name)
+ })
+
+ //await tokenize currernt
+ let currentTokens = (await tokenize(Object.keys(unformated).map((key) => {
+ return (unformated[key] as OpenAIChat[]).map((d) => {
+ return d.content
+ }).join('\n\n')
+ }).join('\n\n')) + db.maxResponse) + 150
+
+ let chats:OpenAIChat[] = []
+
+ if(nowChatroom.type === 'group'){
+ chats.push({
+ role: 'system',
+ content: '[Start a new group chat]'
+ })
+ }
+ else{
+ chats.push({
+ role: 'system',
+ content: '[Start a new chat]'
+ })
+ }
+
+ chats.push({
+ role: 'assistant',
+ content: processScript(currentChar,
+ replacePlaceholders(nowChatroom.firstMessage, currentChar.name),
+ 'editprocess')
+ })
+ currentTokens += await tokenize(processScript(currentChar,
+ replacePlaceholders(nowChatroom.firstMessage, currentChar.name),
+ 'editprocess'))
+
+ const ms = currentChat.message
+ for(const msg of ms){
+ let formedChat = processScript(currentChar,replacePlaceholders(msg.data, currentChar.name), 'editprocess')
+ if(nowChatroom.type === 'group'){
+ if(msg.saying && msg.role === 'char'){
+ formedChat = `${findCharacterbyIdwithCache(msg.saying).name}: ${formedChat}`
+
+ }
+ else if(msg.role === 'user'){
+ formedChat = `${db.username}: ${formedChat}`
+ }
+ }
+
+ chats.push({
+ role: msg.role === 'user' ? 'user' : 'assistant',
+ content: formedChat
+ })
+ currentTokens += (await tokenize(formedChat) + 1)
+ }
+
+ if(nowChatroom.type === 'group'){
+ const systemMsg = `[Write the next reply only as ${currentChar.name}]`
+ chats.push({
+ role: 'system',
+ content: systemMsg
+ })
+ currentTokens += (await tokenize(systemMsg) + 1)
+ }
+
+ console.log(currentTokens)
+ console.log(maxContextTokens)
+
+ while(currentTokens > maxContextTokens){
+ if(chats.length <= 1){
+ alertError(language.errors.toomuchtoken)
+
+ return false
+ }
+
+ currentTokens -= (await tokenize(chats[0].content) + 1)
+ chats.splice(0, 1)
+ }
+
+ console.log(currentTokens)
+
+ let bias:{[key:number]:number} = {}
+
+ for(let i=0;i 0){
+ const prompt = sysPrompts.join('\n')
+
+ if(prompt.replace(/\n/g,'').length > 3){
+ formated.push({
+ role: 'system',
+ content: prompt
+ })
+ }
+ sysPrompts = []
+ formated = formated.concat(cha)
+ }
+ else{
+ formated = formated.concat(cha)
+ }
+ }
+
+ if(sysPrompts.length > 0){
+ const prompt = sysPrompts.join('\n')
+
+ if(prompt.replace(/\n/g,'').length > 3){
+ formated.push({
+ role: 'system',
+ content: prompt
+ })
+ }
+ sysPrompts = []
+ }
+
+
+ const req = await requestChatData({
+ formated: formated,
+ bias: bias,
+ currentChar: currentChar
+ }, 'model')
+
+ let result = ''
+
+ if(req.type === 'fail'){
+ alertError(req.result)
+ return false
+ }
+ else{
+ result = reformatContent(req.result)
+ db.characters[selectedChar].chats[selectedChat].message.push({
+ role: 'char',
+ data: result,
+ saying: processScript(currentChar,currentChar.chaId, 'editoutput')
+ })
+ setDatabase(db)
+ }
+
+
+ if(currentChar.viewScreen === 'emotion'){
+
+ let currentEmotion = currentChar.emotionImages
+
+ function shuffleArray(array:string[]) {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array
+ }
+
+ let emotionList = currentEmotion.map((a) => {
+ return a[0]
+ })
+
+ let charemotions = get(CharEmotion)
+
+ let tempEmotion = charemotions[currentChar.chaId]
+ if(!tempEmotion){
+ tempEmotion = []
+ }
+ if(tempEmotion.length > 4){
+ tempEmotion.splice(0, 1)
+ }
+
+ let emobias:{[key:number]:number} = {}
+
+ for(const emo of emotionList){
+ const tokens = await tokenizeNum(emo)
+ for(const token of tokens){
+ emobias[token] = 10
+ }
+ }
+
+ for(let i =0;i {
+ return a[0]
+ })
+ try {
+ const emotion:string = rq.result.replace(/ |\n/g,'').trim().toLocaleLowerCase()
+ let emotionSelected = false
+ for(const emo of currentEmotion){
+ if(emo[0] === emotion){
+ const emos:[string, string,number] = [emo[0], emo[1], Date.now()]
+ tempEmotion.push(emos)
+ charemotions[currentChar.chaId] = tempEmotion
+ CharEmotion.set(charemotions)
+ emotionSelected = true
+ break
+ }
+ }
+ if(!emotionSelected){
+ for(const emo of currentEmotion){
+ if(emotion.includes(emo[0])){
+ const emos:[string, string,number] = [emo[0], emo[1], Date.now()]
+ tempEmotion.push(emos)
+ charemotions[currentChar.chaId] = tempEmotion
+ CharEmotion.set(charemotions)
+ emotionSelected = true
+ break
+ }
+ }
+ }
+ if(!emotionSelected && emotionList.includes('neutral')){
+ const emo = currentEmotion[emotionList.indexOf('neutral')]
+ const emos:[string, string,number] = [emo[0], emo[1], Date.now()]
+ tempEmotion.push(emos)
+ charemotions[currentChar.chaId] = tempEmotion
+ CharEmotion.set(charemotions)
+ emotionSelected = true
+ }
+ } catch (error) {
+ alertError(language.errors.httpError + `${error}`)
+ return true
+ }
+ }
+
+ return true
+
+
+ }
+ else if(currentChar.viewScreen === 'imggen'){
+ if(chatProcessIndex !== -1){
+ alertError("Stable diffusion in group chat is not supported")
+ }
+
+ const msgs = db.characters[selectedChar].chats[selectedChat].message
+ let msgStr = ''
+ for(let i = (msgs.length - 1);i>=0;i--){
+ console.log(i,msgs.length,msgs[i])
+ if(msgs[i].role === 'char'){
+ msgStr = `character: ${msgs[i].data.replace(/\n/, ' ')} \n` + msgStr
+ }
+ else{
+ msgStr = `user: ${msgs[i].data.replace(/\n/, ' ')} \n` + msgStr
+ break
+ }
+ }
+
+
+ const ch = await stableDiff(currentChar, msgStr)
+ if(ch){
+ db.characters[selectedChar].chats[selectedChat].sdData = ch
+ setDatabase(db)
+ }
+ }
+ return true
+}
\ No newline at end of file
diff --git a/src/ts/process/plugins.ts b/src/ts/process/plugins.ts
new file mode 100644
index 00000000..6679fc02
--- /dev/null
+++ b/src/ts/process/plugins.ts
@@ -0,0 +1,250 @@
+import { get, writable } from "svelte/store";
+import { language } from "../../lang";
+import { alertError } from "../alert";
+import { DataBase } from "../database";
+import { checkNullish, selectSingleFile, sleep } from "../util";
+import type { OpenAIChat } from ".";
+import { globalFetch } from "../globalApi";
+
+export const customProviderStore = writable([] as string[])
+
+interface PluginRequest{
+ url: string
+ header?:{[key:string]:string}
+ body: any,
+ res: string
+}
+
+interface ProviderPlugin{
+ name:string
+ displayName?:string
+ script:string
+ arguments:{[key:string]:'int'|'string'|string[]}
+ realArg:{[key:string]:number|string}
+}
+
+export type RisuPlugin = ProviderPlugin
+
+export async function importPlugin(){
+ try {
+ let db = get(DataBase)
+ const f = await selectSingleFile(['js'])
+ if(!f){
+ return
+ }
+ const jsFile = Buffer.from(f.data).toString('utf-8').replace(/^\uFEFF/gm, "");
+ const splitedJs = jsFile.split('\n')
+ let name = ''
+ let displayName:string = undefined
+ let arg:{[key:string]:'int'|'string'|string[]} = {}
+ let realArg:{[key:string]:number|string} = {}
+ for(const line of splitedJs){
+ if(line.startsWith('//@risu-name')){
+ const provied = line.slice(13)
+ if(provied === ''){
+ alertError('plugin name must be longer than "", did you put it correctly?')
+ return
+ }
+ name = provied.trim()
+ }
+ if(line.startsWith('//@risu-display-name')){
+ const provied = line.slice('//@risu-display-name'.length + 1)
+ if(provied === ''){
+ alertError('plugin display name must be longer than "", did you put it correctly?')
+ return
+ }
+ name = provied.trim()
+ }
+ if(line.startsWith('//@risu-arg')){
+ const provied = line.trim().split(' ')
+ if(provied.length < 3){
+ alertError('plugin argument is incorrect, did you put space in argument name?')
+ return
+ }
+ const provKey = provied[1]
+
+ if(provied[2] !== 'int' && provied[2] !== 'string'){
+ alertError(`plugin argument type is "${provied[2]}", which is an unknown type.`)
+ return
+ }
+ if(provied[2] === 'int'){
+ arg[provKey] = 'int'
+ realArg[provKey] = 0
+ }
+ else if(provied[2] === 'string'){
+ arg[provKey] = 'string'
+ realArg[provKey] = ''
+ }
+ }
+ }
+
+ if(name.length === 0){
+ alertError('plugin name not found, did you put it correctly?')
+ return
+ }
+
+ let pluginData:RisuPlugin = {
+ name: name,
+ script: jsFile,
+ realArg: realArg,
+ arguments: arg,
+ displayName: displayName
+ }
+
+ db.plugins.push(pluginData)
+
+ DataBase.set(db)
+ loadPlugins()
+ } catch (error) {
+ console.error(error)
+ alertError(language.errors.noData)
+ }
+}
+
+export function getCurrentPluginMax(prov:string){
+ return 12000
+}
+
+let pluginWorker:Worker = null
+let providerRes:{success:boolean, content:string} = null
+
+function postMsgPluginWorker(type:string, body:any){
+ const bod = {
+ type: type,
+ body: body
+ }
+ pluginWorker.postMessage(bod)
+}
+
+export async function loadPlugins() {
+ let db = get(DataBase)
+ if(pluginWorker){
+ pluginWorker.terminate()
+ pluginWorker = null
+ }
+ if(db.plugins.length > 0){
+
+ const da = await fetch("/pluginApi.js")
+ const pluginApiString = await da.text()
+ let pluginjs = `${pluginApiString}\n`
+
+ for(const plug of db.plugins){
+ pluginjs += `(() => {${plug.script}})()`
+ }
+
+ const blob = new Blob([pluginjs], {type: 'application/javascript'});
+ pluginWorker = new Worker(URL.createObjectURL(blob));
+
+ pluginWorker.addEventListener('message', async (msg) => {
+ const data:{type:string,body:any} = msg.data
+ switch(data.type){
+ case "addProvider":{
+ let provs = get(customProviderStore)
+ provs.push(data.body)
+ customProviderStore.set(provs)
+ console.log(provs)
+ break
+ }
+ case "resProvider":{
+ const provres:{success:boolean, content:string} = data.body
+ if(checkNullish(provres.success) || checkNullish(provres.content)){
+ providerRes = {
+ success: false,
+ content :"provider didn't respond 'success' or 'content' in response object"
+ }
+ }
+ else if(typeof(provres.content) !== 'string'){
+ providerRes = {
+ success: false,
+ content :"provider didn't respond 'content' in response object in string"
+ }
+ }
+ else{
+ providerRes = {
+ success: !!provres.success,
+ content: provres.content
+ }
+ }
+ break
+ }
+ case "fetch": {
+ postMsgPluginWorker('fetchData',{
+ id: data.body.id,
+ data: await globalFetch(data.body.url, data.body.arg)
+ })
+ break
+ }
+ case "getArg":{
+ try {
+ const db = get(DataBase)
+ const arg:string[] = data.body.arg.split('::')
+ for(const plug of db.plugins){
+ if(arg[0] === plug.name){
+ postMsgPluginWorker('fetchData',{
+ id: data.body.id,
+ data: plug.realArg[arg[1]]
+ })
+ return
+ }
+ }
+ postMsgPluginWorker('fetchData',{
+ id: data.body.id,
+ data: null
+ })
+ } catch (error) {
+ postMsgPluginWorker('fetchData',{
+ id: data.body.id,
+ data: null
+ })
+ }
+ break
+ }
+ case "log":{
+ console.log(data.body)
+ break
+ }
+ }
+ })
+ }
+}
+
+export async function pluginProcess(arg:{
+ prompt_chat: OpenAIChat,
+ temperature: number,
+ max_tokens: number,
+ presence_penalty: number
+ frequency_penalty: number
+ bias: {[key:string]:string}
+}|{}){
+ try {
+ let db = get(DataBase)
+ if(!pluginWorker){
+ return {
+ success: false,
+ content: "plugin worker not found error"
+ }
+ }
+ postMsgPluginWorker("requestProvider", {
+ key: db.currentPluginProvider,
+ arg: arg
+ })
+ providerRes = null
+ while(true){
+ await sleep(50)
+ if(providerRes){
+ break
+ }
+ }
+ return {
+ success: providerRes.success,
+ content: providerRes.content
+ }
+ } catch (error) {
+ return {
+ success: false,
+ content: "unknownError"
+ }
+ }
+}
+
+
diff --git a/src/ts/process/request.ts b/src/ts/process/request.ts
new file mode 100644
index 00000000..a9fbba10
--- /dev/null
+++ b/src/ts/process/request.ts
@@ -0,0 +1,215 @@
+import { get } from "svelte/store";
+import type { OpenAIChat } from ".";
+import { DataBase, setDatabase, type character } from "../database";
+import { pluginProcess } from "./plugins";
+import { language } from "../../lang";
+import { stringlizeChat } from "./stringlize";
+import { globalFetch } from "../globalApi";
+
+interface requestDataArgument{
+ formated: OpenAIChat[]
+ bias: {[key:number]:number}
+ currentChar: character
+ temperature?: number
+ maxTokens?:number
+ PresensePenalty?: number
+ frequencyPenalty?: number
+}
+
+type requestDataResponse = {
+ type: 'success'|'fail'
+ result: string
+}
+
+export async function requestChatData(arg:requestDataArgument, model:'model'|'submodel'):Promise {
+ const db = get(DataBase)
+ let trys = 0
+ while(true){
+ const da = await requestChatDataMain(arg, model)
+ if(da.type === 'success'){
+ return da
+ }
+ trys += 1
+ if(trys > db.requestRetrys){
+ return da
+ }
+ }
+}
+
+export async function requestChatDataMain(arg:requestDataArgument, model:'model'|'submodel'):Promise {
+ const db = get(DataBase)
+ let result = ''
+ let formated = arg.formated
+ let maxTokens = db.maxResponse
+ let bias = arg.bias
+ let currentChar = arg.currentChar
+ const replacer = model === 'model' ? db.forceReplaceUrl : db.forceReplaceUrl2
+ const aiModel = model === 'model' ? db.aiModel : db.subModel
+
+ switch(aiModel){
+ case 'gpt35':
+ case 'gpt4':{
+ const body = ({
+ model: aiModel === 'gpt35' ? 'gpt-3.5-turbo' : 'gpt-4',
+ messages: formated,
+ temperature: arg.temperature ?? (db.temperature / 100),
+ max_tokens: arg.maxTokens ?? maxTokens,
+ presence_penalty: arg.PresensePenalty ?? (db.PresensePenalty / 100),
+ frequency_penalty: arg.frequencyPenalty ?? (db.frequencyPenalty / 100),
+ logit_bias: bias,
+ })
+
+ let replacerURL = replacer === '' ? 'https://api.openai.com/v1/chat/completions' : replacer
+
+ if(replacerURL.endsWith('v1')){
+ replacerURL += '/chat/completions'
+ }
+ if(replacerURL.endsWith('v1/')){
+ replacerURL += 'chat/completions'
+ }
+
+ const res = await globalFetch(replacerURL, {
+ body: body,
+ headers: {
+ "Authorization": "Bearer " + db.openAIKey
+ },
+ })
+
+ const dat = res.data as any
+ if(res.ok){
+ try {
+ const msg:OpenAIChat = (dat.choices[0].message)
+ return {
+ type: 'success',
+ result: msg.content
+ }
+ } catch (error) {
+ return {
+ type: 'fail',
+ result: (language.errors.httpError + `${JSON.stringify(dat)}`)
+ }
+ }
+ }
+ else{
+ if(dat.error && dat.error.message){
+ return {
+ type: 'fail',
+ result: (language.errors.httpError + `${dat.error.message}`)
+ }
+ }
+ else{
+ return {
+ type: 'fail',
+ result: (language.errors.httpError + `${JSON.stringify(res.data)}`)
+ }
+ }
+ }
+
+ break
+ }
+ case "textgen_webui":{
+ let DURL = db.textgenWebUIURL
+ if((!DURL.endsWith('textgen')) && (!DURL.endsWith('textgen/'))){
+ if(DURL.endsWith('/')){
+ DURL += 'run/textgen'
+ }
+ else{
+ DURL += '/run/textgen'
+ }
+ }
+
+ const proompt = stringlizeChat(formated, currentChar.name)
+
+ const payload = [
+ proompt,
+ {
+ 'max_new_tokens': 80,
+ 'do_sample': true,
+ 'temperature': (db.temperature / 100),
+ 'top_p': 0.9,
+ 'typical_p': 1,
+ 'repetition_penalty': (db.PresensePenalty / 100),
+ 'encoder_repetition_penalty': 1,
+ 'top_k': 100,
+ 'min_length': 0,
+ 'no_repeat_ngram_size': 0,
+ 'num_beams': 1,
+ 'penalty_alpha': 0,
+ 'length_penalty': 1,
+ 'early_stopping': false,
+ 'truncation_length': maxTokens,
+ 'ban_eos_token': false,
+ 'custom_stopping_strings': [`\nUser:`],
+ 'seed': -1,
+ add_bos_token: true,
+ }
+ ];
+
+ const bodyTemplate = { "data": [JSON.stringify(payload)] };
+
+ const res = await globalFetch(DURL, {
+ body: bodyTemplate,
+ headers: {}
+ })
+
+ const dat = res.data as any
+ console.log(DURL)
+ console.log(res.data)
+ if(res.ok){
+ try {
+ return {
+ type: 'success',
+ result: dat.data[0].substring(proompt.length)
+ }
+ } catch (error) {
+ return {
+ type: 'fail',
+ result: (language.errors.httpError + `${error}`)
+ }
+ }
+ }
+ else{
+ return {
+ type: 'fail',
+ result: (language.errors.httpError + `${JSON.stringify(res.data)}`)
+ }
+ }
+ }
+
+ case 'custom':{
+ const d = await pluginProcess({
+ bias: bias,
+ prompt_chat: formated,
+ temperature: (db.temperature / 100),
+ max_tokens: maxTokens,
+ presence_penalty: (db.PresensePenalty / 100),
+ frequency_penalty: (db.frequencyPenalty / 100)
+ })
+ if(!d){
+ return {
+ type: 'fail',
+ result: (language.errors.unknownModel)
+ }
+ }
+ else if(!d.success){
+ return {
+ type: 'fail',
+ result: d.content
+ }
+ }
+ else{
+ return {
+ type: 'success',
+ result: d.content
+ }
+ }
+ break
+ }
+ default:{
+ return {
+ type: 'fail',
+ result: (language.errors.unknownModel)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ts/process/scripts.ts b/src/ts/process/scripts.ts
new file mode 100644
index 00000000..514985ea
--- /dev/null
+++ b/src/ts/process/scripts.ts
@@ -0,0 +1,15 @@
+import type { character } from "../database";
+
+const dreg = /{{data}}/g
+
+export function processScript(char:character, data:string, mode:'editinput'|'editoutput'|'editprocess'){
+ for (const script of char.customscript){
+ if(script.type === mode){
+ const reg = new RegExp(script.in,'g')
+ data = data.replace(reg, (v) => {
+ return script.out.replace(dreg, v)
+ })
+ }
+ }
+ return data
+}
\ No newline at end of file
diff --git a/src/ts/process/stableDiff.ts b/src/ts/process/stableDiff.ts
new file mode 100644
index 00000000..ebcc4f54
--- /dev/null
+++ b/src/ts/process/stableDiff.ts
@@ -0,0 +1,158 @@
+import { get } from "svelte/store"
+import { DataBase, type character } from "../database"
+import { requestChatData } from "./request"
+import { alertError } from "../alert"
+import { globalFetch } from "../globalApi"
+import { CharEmotion } from "../stores"
+
+
+export async function stableDiff(currentChar:character,prompt:string){
+ const mainPrompt = "assistant is a chat analyzer.\nuser will input a data of situation with key and values before chat, and a chat of a user and character.\nView the status of the chat and change the data.\nif data's key starts with $, it must change it every time.\nif data value is none, it must change it."
+ let db = get(DataBase)
+
+ if(db.sdProvider === ''){
+ alertError("Stable diffusion is not set in settings.")
+ return false
+ }
+
+ let proompt = 'Data:'
+
+ let currentSd:[string,string][] = []
+
+ const sdData = currentChar.chats[currentChar.chatPage].sdData
+ if(sdData){
+ const das = sdData.split('\n')
+ for(const data of das){
+ const splited = data.split(':::')
+ currentSd.push([splited[0].trim(), splited[1].trim()])
+ }
+ }
+ else{
+ currentSd = JSON.parse(JSON.stringify(currentChar.sdData))
+ }
+
+ for(const d of currentSd){
+ let val = d[1].trim()
+ if(val === ''){
+ val = 'none'
+ }
+
+ if(!d[0].startsWith('|') || d[0] === 'negative' || d[0] === 'always'){
+ proompt += `\n${d[0].trim()}: ${val}`
+ }
+ }
+
+ proompt += `\n\nChat:\n${prompt}`
+
+ const promptbody:OpenAIChat[] = [
+ {
+
+ role:'system',
+ content: mainPrompt
+ },
+ {
+ role: 'user',
+ content: `Data:\ncharacter's appearance: red hair, cute, black eyes\ncurrent situation: none\n$character's pose: none\n$character's emotion: none\n\nChat:\nuser: *eats breakfeast* \n I'm ready.\ncharacter: Lemon waits patiently outside your room while you get ready. Once you are dressed and have finished your breakfast, she escorts you to the door.\n"Have a good day at school, Master. Don't forget to study hard and make the most of your time there," Lemon reminds you with a smile as she sees you off.`
+ },
+ {
+ role: 'assistant',
+ content: "character's appearance: red hair, cute, black eyes\ncurrent situation: waking up in the morning\n$character's pose: standing\n$character's emotion: apologetic"
+ },
+ {
+
+ role:'system',
+ content: mainPrompt
+ },
+ {
+ role: 'user',
+ content: proompt
+ },
+ ]
+
+ console.log(proompt)
+ const rq = await requestChatData({
+ formated: promptbody,
+ currentChar: currentChar,
+ temperature: 0.2,
+ maxTokens: 300,
+ bias: {}
+ }, 'submodel')
+
+
+ if(rq.type === 'fail'){
+ alertError(rq.result)
+ return false
+ }
+ else{
+ const res = rq.result
+ const das = res.split('\n')
+ for(const data of das){
+ const splited = data.split(':')
+ if(splited.length === 2){
+ for(let i=0;i {
+ return val.join(':::')
+ }).join('\n')
+
+ if(db.sdProvider === 'webui'){
+
+ let prompts:string[] = []
+ let neg = ''
+ for(let i=0;i{
+ return await tikJS(data)
+}
+
+let tikParser:Tiktoken = null
+
+async function tikJS(text:string) {
+ if(!tikParser){
+ const {Tiktoken} = await import('@dqbd/tiktoken')
+ const cl100k_base = await import("@dqbd/tiktoken/encoders/cl100k_base.json");
+
+ tikParser = new Tiktoken(
+ cl100k_base.bpe_ranks,
+ cl100k_base.special_tokens,
+ cl100k_base.pat_str
+ );
+ }
+ return tikParser.encode(text)
+}
+
+export async function tokenizerChar(char:character) {
+ const encoded = await encode(char.name + '\n' + char.firstMessage + '\n' + char.desc)
+ return encoded.length
+}
+
+export async function tokenize(data:string) {
+ const encoded = await encode(data)
+ return encoded.length
+}
+
+export async function tokenizeNum(data:string) {
+ const encoded = await encode(data)
+ return encoded
+}
diff --git a/src/ts/translator/translator.ts b/src/ts/translator/translator.ts
new file mode 100644
index 00000000..73565774
--- /dev/null
+++ b/src/ts/translator/translator.ts
@@ -0,0 +1,49 @@
+import { Body,fetch,ResponseType } from "@tauri-apps/api/http"
+import { isTauri } from "../globalApi"
+
+let cache={
+ origin: [''],
+ trans: ['']
+}
+
+export async function translate(params:string, reverse:boolean) {
+ if(!isTauri){
+ return params
+ }
+ if(!reverse){
+ const ind = cache.origin.indexOf(params)
+ if(ind !== -1){
+ return cache.trans[ind]
+ }
+ }
+ else{
+ const ind = cache.trans.indexOf(params)
+ if(ind !== -1){
+ return cache.origin[ind]
+ }
+ }
+ return googleTrans(params, reverse)
+}
+
+async function googleTrans(text:string, reverse:boolean) {
+ const arg = {
+ from: reverse ? 'ko' : 'en',
+ to: reverse ? 'en' : 'ko',
+ host: 'translate.google.com',
+ }
+ const body = Body.form({
+ sl: reverse ? 'ko' : 'en',
+ tl: reverse ? 'en' : 'ko',
+ q: text,
+ })
+ const url = `https://${arg.host}/translate_a/single?client=at&dt=t&dt=rm&dj=1`
+
+ const f = await fetch(url, {
+ method: "POST",
+ body: body,
+ responseType: ResponseType.JSON
+ })
+
+ const res = f.data as {sentences:{trans?:string}[]}
+ return res.sentences.filter((s) => 'trans' in s).map((s) => s.trans).join('');
+}
\ No newline at end of file
diff --git a/src/ts/update.ts b/src/ts/update.ts
new file mode 100644
index 00000000..e504eaca
--- /dev/null
+++ b/src/ts/update.ts
@@ -0,0 +1,51 @@
+import { fetch } from "@tauri-apps/api/http";
+import { DataBase, appVer, setDatabase } from "./database";
+import { alertConfirm } from "./alert";
+import { language } from "../lang";
+import { get } from "svelte/store";
+import {open} from '@tauri-apps/api/shell'
+
+
+export async function checkUpdate(){
+ try {
+ let db = get(DataBase)
+ const da = await fetch('https://raw.githubusercontent.com/kwaroran/RisuAI-release/main/version.json')
+ //@ts-ignore
+ const v:string = da.data.version
+ if(!v){
+ return
+ }
+ if(v === db.lastup){
+ return
+ }
+ const nextVer = versionStringToNumber(v)
+ if(isNaN(nextVer) || (!nextVer)){
+ return
+ }
+ const appVerNum = versionStringToNumber(appVer)
+
+ if(appVerNum < nextVer){
+ const conf = await alertConfirm(language.newVersion)
+ if(conf){
+ open("https://github.com/kwaroran/RisuAI-release/releases/latest")
+ }
+ else{
+ db = get(DataBase)
+ db.lastup = v
+ setDatabase(db)
+ }
+ }
+
+ } catch (error) {
+
+ }
+}
+
+function versionStringToNumber(versionString:string):number {
+ return Number(
+ versionString
+ .split(".")
+ .map((component) => component.padStart(2, "0"))
+ .join("")
+ );
+}
\ No newline at end of file
diff --git a/src/ts/util.ts b/src/ts/util.ts
new file mode 100644
index 00000000..2c4e0d68
--- /dev/null
+++ b/src/ts/util.ts
@@ -0,0 +1,271 @@
+import { get } from "svelte/store"
+import type { Database, Message } from "./database"
+import { DataBase } from "./database"
+import { selectedCharID } from "./stores"
+import {open} from '@tauri-apps/api/dialog'
+import { readBinaryFile } from "@tauri-apps/api/fs"
+import { basename } from "@tauri-apps/api/path"
+import { createBlankChar, getCharImage } from "./characters"
+import { appWindow } from '@tauri-apps/api/window';
+import { isTauri } from "./globalApi"
+
+export interface Messagec extends Message{
+ index: number
+}
+
+export function messageForm(arg:Message[], loadPages:number){
+ let db = get(DataBase)
+ let selectedChar = get(selectedCharID)
+ function reformatContent(data:string){
+ return data.trim().replace(`${db.characters[selectedChar].name}:`, '').trim()
+ }
+
+ let a:Messagec[] = []
+ for(let i=0;i setTimeout(resolve, ms) );
+}
+
+export function checkNullish(data:any){
+ return data === undefined || data === null
+}
+
+export async function selectSingleFile(ext:string[]){
+ if(await !isTauri){
+ const v = await selectFileByDom(ext, 'single')
+ const file = v[0]
+ return {name: file.name,data:await readFileAsUint8Array(file)}
+ }
+
+ const selected = await open({
+ filters: [{
+ name: ext.join(', '),
+ extensions: ext
+ }]
+ });
+ if (Array.isArray(selected)) {
+ return null
+ } else if (selected === null) {
+ return null
+ } else {
+ return {name: await basename(selected),data:await readBinaryFile(selected)}
+ }
+}
+
+export async function selectMultipleFile(ext:string[]){
+ if(!isTauri){
+ const v = await selectFileByDom(ext, 'multiple')
+ let arr:{name:string, data:Uint8Array}[] = []
+ for(const file of v){
+ arr.push({name: file.name,data:await readFileAsUint8Array(file)})
+ }
+ return arr
+ }
+
+ const selected = await open({
+ filters: [{
+ name: ext.join(', '),
+ extensions: ext,
+ }],
+ multiple: true
+ });
+ if (Array.isArray(selected)) {
+ let arr:{name:string, data:Uint8Array}[] = []
+ for(const file of selected){
+ arr.push({name: await basename(file),data:await readBinaryFile(file)})
+ }
+ return arr
+ } else if (selected === null) {
+ return null
+ } else {
+ return [{name: await basename(selected),data:await readBinaryFile(selected)}]
+ }
+}
+
+export const replacePlaceholders = (msg:string, name:string) => {
+ let db = get(DataBase)
+ let selectedChar = get(selectedCharID)
+ let currentChar = db.characters[selectedChar]
+ return msg.replace(/({{char}})|({{Char}})|()|()/gi, currentChar.name)
+ .replace(/({{user}})|({{User}})|()|()/gi, db.username)
+}
+
+function selectFileByDom(allowedExtensions:string[], multiple:'multiple'|'single' = 'single') {
+ return new Promise((resolve) => {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.multiple = multiple === 'multiple';
+
+ if (allowedExtensions && allowedExtensions.length) {
+ fileInput.accept = allowedExtensions.map(ext => `.${ext}`).join(',');
+ }
+
+ fileInput.addEventListener('change', (event) => {
+ if (fileInput.files.length === 0) {
+ resolve([]);
+ return;
+ }
+
+ const files = Array.from(fileInput.files).filter(file => {
+ const fileExtension = file.name.split('.').pop().toLowerCase();
+ return !allowedExtensions || allowedExtensions.includes(fileExtension);
+ });
+
+ fileInput.remove()
+ resolve(files);
+ });
+
+ document.body.appendChild(fileInput);
+ fileInput.click();
+ fileInput.style.display = 'none'; // Hide the file input element
+ });
+}
+
+function readFileAsUint8Array(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = (event) => {
+ const buffer = event.target.result;
+ const uint8Array = new Uint8Array(buffer as ArrayBuffer);
+ resolve(uint8Array);
+ };
+
+ reader.onerror = (error) => {
+ reject(error);
+ };
+
+ reader.readAsArrayBuffer(file);
+ });
+}
+
+export async function changeFullscreen(){
+ const db = get(DataBase)
+ const isFull = await appWindow.isFullscreen()
+ console.log(isFull)
+ console.log(db.fullScreen)
+ if(db.fullScreen && (!isFull)){
+ await appWindow.setFullscreen(true)
+ }
+ if((!db.fullScreen) && (isFull)){
+ await appWindow.setFullscreen(false)
+ }
+}
+
+export async function getCustomBackground(db:string){
+ if(db.length < 2){
+ return ''
+ }
+ else{
+ const filesrc = await getCharImage(db, 'plain')
+ return `background: url("${filesrc}"); background-size: cover;`
+ }
+}
+
+export function findCharacterbyId(id:string) {
+ const db = get(DataBase)
+ for(const char of db.characters){
+ if(char.type !== 'group'){
+ if(char.chaId === id){
+ return char
+ }
+ }
+ }
+ let unknown =createBlankChar()
+ unknown.name = 'Unknown Character'
+ return unknown
+}
+
+export function defaultEmotion(em:[string,string][]){
+ if(!em){
+ return ''
+ }
+ for(const v of em){
+ if(v[0] === 'neutral'){
+ return v[1]
+ }
+ }
+ return ''
+}
+
+export async function getEmotion(db:Database,chaEmotion:{[key:string]: [string, string, number][]}, type:'contain'|'plain'|'css'){
+ const selectedChar = get(selectedCharID)
+ const currentDat = db.characters[selectedChar]
+ if(!currentDat){
+ return []
+ }
+ let charIdList:string[] = []
+
+ if(currentDat.type === 'group'){
+ if(currentDat.characters.length === 0){
+ return []
+ }
+ switch(currentDat.viewScreen){
+ case "multiple":
+ charIdList = currentDat.characters
+ break
+ case "single":{
+ let newist:[string,string,number] = ['', '', 0]
+ let newistChar = currentDat.characters[0]
+ for(const currentChar of currentDat.characters){
+ const cha = chaEmotion[currentChar]
+ if(cha){
+ const latestEmotion = cha[cha.length - 1]
+ if(latestEmotion && latestEmotion[2] > newist[2]){
+ newist = latestEmotion
+ newistChar = currentChar
+ }
+ }
+ }
+ charIdList = [newistChar]
+ break
+ }
+ case "emp":{
+ charIdList = currentDat.characters
+ break
+ }
+ }
+ }
+ else{
+ charIdList = [currentDat.chaId]
+ }
+
+ let datas: string[] = [currentDat.viewScreen === 'emp' ? 'emp' : 'normal' as const]
+ for(const chaid of charIdList){
+ const currentChar = findCharacterbyId(chaid)
+ if(currentChar.viewScreen === 'emotion'){
+ const currEmotion = chaEmotion[currentChar.chaId]
+ let im = ''
+ if(!currEmotion || currEmotion.length === 0){
+ im = (await getCharImage(defaultEmotion(currentChar?.emotionImages),type))
+ }
+ else{
+ im = (await getCharImage(currEmotion[currEmotion.length - 1][1], type))
+ }
+ if(im && im.length > 2){
+ datas.push(im)
+ }
+ }
+ else if(currentChar.viewScreen === 'imggen'){
+ const currEmotion = chaEmotion[currentChar.chaId]
+ if(!currEmotion || currEmotion.length === 0){
+ datas.push(await getCharImage(currentChar.image ?? '', 'plain'))
+ }
+ else{
+ datas.push(currEmotion[currEmotion.length - 1][1])
+ }
+ }
+ }
+ return datas
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 00000000..4078e747
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..a409daf4
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,43 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx,svelte}",
+ ],
+ theme: {
+ extend: {
+ colors:{
+ bgcolor: "#282a36",
+ darkbg: "#21222C",
+ borderc: "#6272a4",
+ selected: "#44475a",
+ draculared: "#ff5555"
+ },
+ minWidth: {
+ '20': '5rem',
+ '14': '3.5rem',
+ 'half': '50%'
+ },
+ maxWidth:{
+ 'half': '50%',
+ '14': '3.5rem',
+ },
+ borderWidth: {
+ '1': '1px',
+ },
+ width: {
+ '2xl': '48rem',
+ },
+ minHeight:{
+ '8': '2rem',
+ '14': '3.5rem',
+ '20': '5rem',
+
+ }
+ }
+ },
+ plugins: [
+ require('@tailwindcss/typography')
+ ],
+}
+
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 00000000..ff430e3e
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,13 @@
+디스플레이 이미지:
+감정 외 특정 키워드 입력시 다른 이미지를 띄운다던지 등의 기능 추가
+
+디스플레이:
+어디까지 기억하는지 표시 추가
+
+입력창:
+입력창 엔터 되게 만들기
+현제 입력 토큰 보이기?
+
+플러그인:
+일단 되게 만들기
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..09246f64
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ "baseUrl": ".",
+ /**
+ * Typecheck JS in `.svelte` and `.js` files by default.
+ * Disable checkJs if you'd like to use dynamic types in JS.
+ * Note that setting allowJs false does not prevent the use
+ * of JS in `.svelte` files.
+ */
+ "allowJs": true,
+ "checkJs": true,
+ "isolatedModules": true
+ },
+ "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "public/sw.js", "public/pluginApi.ts"],
+ "exclude": ["src/**/web/*.ts"],
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "ignoreDeprecations": "5.0"
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 00000000..65dbdb96
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node"
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..b0481948
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,63 @@
+import { defineConfig } from "vite";
+import { svelte } from "@sveltejs/vite-plugin-svelte";
+import sveltePreprocess from "svelte-preprocess";
+import wasm from "vite-plugin-wasm";
+import { internalIpV4 } from 'internal-ip'
+import topLevelAwait from "vite-plugin-top-level-await";
+
+// https://vitejs.dev/config/
+export default defineConfig(async () => {
+
+ const host = await internalIpV4()
+
+ return {
+ plugins: [
+
+ svelte({
+ preprocess: [
+ sveltePreprocess({
+ typescript: true,
+ }),
+ ],
+ onwarn: (warning, handler) => {
+ // disable a11y warnings
+ if (warning.code.startsWith("a11y-")) return;
+ handler(warning);
+ },
+ }),
+ wasm(),
+ topLevelAwait(),
+ ],
+
+ // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
+ // prevent vite from obscuring rust errors
+ clearScreen: false,
+ // tauri expects a fixed port, fail if that port is not available
+ server: {
+ host: '0.0.0.0', // listen on all addresses
+ port: 5174,
+ strictPort: true,
+ hmr: {
+ protocol: 'ws',
+ host,
+ port: 5184,
+ },
+ },
+ // to make use of `TAURI_DEBUG` and other env variables
+ // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
+ envPrefix: ["VITE_", "TAURI_"],
+ build: {
+ // Tauri supports es2021
+ target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
+ // don't minify for debug builds
+ minify: process.env.TAURI_DEBUG ? false : 'esbuild',
+ // produce sourcemaps for debug builds
+ sourcemap: !!process.env.TAURI_DEBUG,
+ },
+
+ resolve:{
+ alias:{
+ 'src':'/src'
+ }
+ }
+}});