[feat] Add support for chat stickers

This commit adds support for chat stickers by allowing users to use stickers in chat message window.
The users can toggle show additional asset list using a button.
Added Additional assets file extension data.
Additional assets list shows preview.
Optimized render when use streaming api. (prevent markdown again when message not changed)
Added controls to Video/Audio Assets
This commit is contained in:
LL
2023-06-22 07:10:18 +09:00
parent 89015ac009
commit d8a5ee5b4e
13 changed files with 200 additions and 37 deletions

View File

@@ -304,6 +304,7 @@ export const languageChinese = {
tags: "标签", tags: "标签",
copied: "已复制", copied: "已复制",
useChatCopy: "使用聊天复制", useChatCopy: "使用聊天复制",
useChatSticker: "使用聊天贴纸",
autoTranslateInput: "使用自动翻译输入", autoTranslateInput: "使用自动翻译输入",
enterMessageForTranslateToEnglish: "输入要翻译为英语的消息", enterMessageForTranslateToEnglish: "输入要翻译为英语的消息",
recent: '最新', recent: '最新',

View File

@@ -308,6 +308,7 @@ export const languageEnglish = {
backgroundHTML: "Background Embedding", backgroundHTML: "Background Embedding",
copied: "Copied", copied: "Copied",
useChatCopy: "Use Chat Message Copy", useChatCopy: "Use Chat Message Copy",
useChatSticker: "Use Chat Sticker",
autoTranslateInput: "Auto Translate Input", autoTranslateInput: "Auto Translate Input",
enterMessageForTranslateToEnglish: "Enter Message for Translate to English", enterMessageForTranslateToEnglish: "Enter Message for Translate to English",
recent: 'Recent', recent: 'Recent',

View File

@@ -280,6 +280,7 @@ export const languageKorean = {
backgroundHTML: "백그라운드 임베딩", backgroundHTML: "백그라운드 임베딩",
copied: "복사됨", copied: "복사됨",
useChatCopy: "채팅 메시지 복사 사용", useChatCopy: "채팅 메시지 복사 사용",
useChatSticker: "채팅 스티커 사용",
autoTranslateInput: "입력 자동 번역", autoTranslateInput: "입력 자동 번역",
enterMessageForTranslateToEnglish: "영어로 번역할 메시지를 입력해주세요", enterMessageForTranslateToEnglish: "영어로 번역할 메시지를 입력해주세요",
imageCompression: "이미지 압축", imageCompression: "이미지 압축",

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { get } from 'svelte/store';
import { FileAudioIcon, PlusIcon } from "lucide-svelte";
import { DataBase, setDatabase, type character, type groupChat } from "src/ts/storage/database";
import { getFileSrc, saveAsset } from "src/ts/storage/globalApi";
import { selectMultipleFile } from "src/ts/util";
export let currentCharacter:character|groupChat;
export let onSelect:(additionalAsset:[string,string,string])=>void;
let assetFileExtensions:string[] = []
let assetFilePath:string[] = []
$:{
if(currentCharacter.type ==='character'){
if(currentCharacter.additionalAssets){
for(let i = 0; i < currentCharacter.additionalAssets.length; i++){
// console.log('check content type ...', currentCharacter.additionalAssets[i][0], currentCharacter.additionalAssets[i][1]);
if(currentCharacter.additionalAssets[i].length > 2 && currentCharacter.additionalAssets[i][2]) {
assetFileExtensions[i] = currentCharacter.additionalAssets[i][2]
} else {
assetFileExtensions[i] = currentCharacter.additionalAssets[i][1].split('.').pop()
}
getFileSrc(currentCharacter.additionalAssets[i][1]).then((filePath) => {
assetFilePath[i] = filePath
})
}
}
}
}
</script>
{#if currentCharacter.type ==='character'}
<button class="hover:text-green-500 bg-gray-500 flex justify-center items-center w-16 h-16 m-1 rounded-md" on:click={async () => {
if(currentCharacter.type === 'character'){
const da = await selectMultipleFile(['png', 'webp', 'mp4', 'mp3', 'gif'])
currentCharacter.additionalAssets = currentCharacter.additionalAssets ?? []
if(!da){
return
}
for(const f of da){
console.log(f)
const img = f.data
const name = f.name
const extension = name.split('.').pop().toLowerCase()
const imgp = await saveAsset(img,'',extension)
currentCharacter.additionalAssets.push([name, imgp, extension])
currentCharacter = currentCharacter
}
const db = get(DataBase);
setDatabase(db)
}
}}>
<PlusIcon />
</button>
{#if currentCharacter.additionalAssets}
{#each currentCharacter.additionalAssets as additionalAsset, i}
<button on:click={()=>{
onSelect(additionalAsset)
}}>
{#if assetFilePath[i]}
{#if assetFileExtensions[i] === 'mp4'}
<!-- svelte-ignore a11y-media-has-caption -->
<video class="w-16 h-16 m-1 rounded-md"><source src={assetFilePath[i]} type="video/mp4"></video>
{:else if assetFileExtensions[i] === 'mp3'}
<div class='w-16 h-16 m-1 rounded-md bg-slate-500 flex flex-col justify-center items-center'>
<FileAudioIcon/>
<div class='w-16 px-1 text-ellipsis whitespace-nowrap overflow-hidden'>{additionalAsset[0]}</div>
</div>
<!-- <audio controls class="w-16 h-16 m-1 rounded-md"><source src={assetPath} type="audio/mpeg"></audio> -->
{:else}
<img src={assetFilePath[i]} class="w-16 h-16 m-1 rounded-md" alt={additionalAsset[0]}/>
{/if}
{/if}
</button>
{/each}
{/if}
{/if}

View File

@@ -8,6 +8,7 @@
import { selectedCharID } from "../../ts/stores"; import { selectedCharID } from "../../ts/stores";
import { translate } from "../../ts/translator/translator"; import { translate } from "../../ts/translator/translator";
import { replacePlaceholders } from "../../ts/util"; import { replacePlaceholders } from "../../ts/util";
export let message = '' export let message = ''
export let name = '' export let name = ''
export let isLastMemory:boolean export let isLastMemory:boolean
@@ -17,6 +18,7 @@
export let onReroll = () => {} export let onReroll = () => {}
export let unReroll = () => {} export let unReroll = () => {}
export let character:character|groupChat|null = null export let character:character|groupChat|null = null
let md:string
let translating = false let translating = false
let editMode = false let editMode = false
let statusMessage:string = '' let statusMessage:string = ''
@@ -24,7 +26,8 @@
let msgDisplay = '' let msgDisplay = ''
let msgTranslated = '' let msgTranslated = ''
let translated = false; let translated = false
async function rm(){ async function rm(){
const rm = $DataBase.askRemoval ? await alertConfirm(language.removeChat) : true const rm = $DataBase.askRemoval ? await alertConfirm(language.removeChat) : true
if(rm){ if(rm){
@@ -53,7 +56,7 @@
$DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message = msg $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message = msg
} }
async function displaya(message:string){ async function displaya(message:string, isStreaming:boolean = false){
if($DataBase.autoTranslate && $DataBase.translator !== ''){ if($DataBase.autoTranslate && $DataBase.translator !== ''){
if(msgTranslated==='') if(msgTranslated==='')
msgDisplay = replacePlaceholders(message, name) msgDisplay = replacePlaceholders(message, name)
@@ -64,6 +67,12 @@
else{ else{
msgDisplay = replacePlaceholders(message, name) msgDisplay = replacePlaceholders(message, name)
} }
if(!md || !isStreaming || isStreaming && idx === $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message.length - 1) {
ParseMarkdown(msgDisplay, character, 'normal').then(mdNew=>{
md = mdNew
})
}
} }
const setStatusMessage = (message:string, timeout:number = 0)=>{ const setStatusMessage = (message:string, timeout:number = 0)=>{
@@ -74,7 +83,7 @@
}, timeout) }, timeout)
} }
$: displaya(message) $: displaya(message, $DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].isStreaming)
</script> </script>
<div class="flex max-w-full justify-center" class:bgc={isLastMemory}> <div class="flex max-w-full justify-center" class:bgc={isLastMemory}>
<div class="text-neutral-200 mt-1 ml-4 mr-4 mb-1 p-2 bg-transparent flex-grow border-t-gray-900 border-opacity-30 border-transparent flexium items-start max-w-full" > <div class="text-neutral-200 mt-1 ml-4 mr-4 mb-1 p-2 bg-transparent flex-grow border-t-gray-900 border-opacity-30 border-transparent flexium items-start max-w-full" >
@@ -161,19 +170,17 @@
</div> </div>
{#if editMode} {#if editMode}
<AutoresizeArea bind:value={message} /> <AutoresizeArea bind:value={message} />
{:else} {:else if md}
{#await ParseMarkdown(msgDisplay, character) then md} <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <span class="text chat chattext prose prose-invert minw-0" on:click={() => {
<span class="text chat chattext prose prose-invert minw-0" on:click={() => { if($DataBase.clickToEdit && idx > -1){
if($DataBase.clickToEdit && idx > -1){ editMode = true
editMode = true msgTranslated = ""
msgTranslated = "" }
} }}
}} style:font-size="{0.875 * ($DataBase.zoomsize / 100)}rem"
style:font-size="{0.875 * ($DataBase.zoomsize / 100)}rem" style:line-height="{1.25 * ($DataBase.zoomsize / 100)}rem"
style:line-height="{1.25 * ($DataBase.zoomsize / 100)}rem" >{@html md}</span>
>{@html md}</span>
{/await}
{/if} {/if}
</span> </span>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Suggestion from './Suggestion.svelte'; import Suggestion from './Suggestion.svelte';
import { DatabaseIcon, DicesIcon, LanguagesIcon, MenuIcon, MicOffIcon, PowerIcon, RefreshCcwIcon, ReplyIcon, Send } from "lucide-svelte"; import { DatabaseIcon, DicesIcon, LanguagesIcon, Laugh, MenuIcon, MicOffIcon, RefreshCcwIcon, ReplyIcon, Send } from "lucide-svelte";
import { selectedCharID } from "../../ts/stores"; import { selectedCharID } from "../../ts/stores";
import Chat from "./Chat.svelte"; import Chat from "./Chat.svelte";
import { DataBase, type Message } from "../../ts/storage/database"; import { DataBase, type Message, type character, type groupChat } from "../../ts/storage/database";
import { getCharImage } from "../../ts/characters"; import { getCharImage } from "../../ts/characters";
import { doingChat, sendChat } from "../../ts/process/index"; import { doingChat, sendChat } from "../../ts/process/index";
import { findCharacterbyId, messageForm, sleep } from "../../ts/util"; import { findCharacterbyId, messageForm, sleep } from "../../ts/util";
@@ -17,6 +17,7 @@
import { stopTTS } from "src/ts/process/tts"; import { stopTTS } from "src/ts/process/tts";
import MainMenu from '../UI/MainMenu.svelte'; import MainMenu from '../UI/MainMenu.svelte';
import Help from '../Others/Help.svelte'; import Help from '../Others/Help.svelte';
import AssetInput from './AssetInput.svelte';
let messageInput:string = '' let messageInput:string = ''
let messageInputTranslate:string = '' let messageInputTranslate:string = ''
@@ -28,6 +29,8 @@
let rerollid = -1 let rerollid = -1
let lastCharId = -1 let lastCharId = -1
let doingChatInputTranslate = false let doingChatInputTranslate = false
let currentCharacter:character|groupChat = $DataBase.characters[$selectedCharID]
let toggleStickers:boolean = false
async function send() { async function send() {
let selectedChar = $selectedCharID let selectedChar = $selectedCharID
@@ -218,6 +221,10 @@
} }
}) })
} }
$: {
currentCharacter = $DataBase.characters[$selectedCharID]
}
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="w-full h-full" style={customStyle} on:click={() => { <div class="w-full h-full" style={customStyle} on:click={() => {
@@ -234,6 +241,12 @@
} }
}}> }}>
<div class="flex items-end mt-2 mb-2 w-full"> <div class="flex items-end mt-2 mb-2 w-full">
{#if $DataBase.useChatSticker && currentCharacter.type !== 'group'}
<div on:click={()=>{toggleStickers = !toggleStickers}}
class={"ml-4 bg-gray-500 flex justify-center items-center w-12 h-12 rounded-md hover:bg-green-500 transition-colors "+(toggleStickers ? 'text-green-500':'text-white')}>
<Laugh/>
</div>
{/if}
<textarea class="text-neutral-200 p-2 min-w-0 bg-transparent input-text text-xl flex-grow ml-4 mr-2 border-gray-700 resize-none focus:bg-selected overflow-y-hidden overflow-x-hidden max-w-full" <textarea class="text-neutral-200 p-2 min-w-0 bg-transparent input-text text-xl flex-grow ml-4 mr-2 border-gray-700 resize-none focus:bg-selected overflow-y-hidden overflow-x-hidden max-w-full"
bind:value={messageInput} bind:value={messageInput}
bind:this={inputEle} bind:this={inputEle}
@@ -299,9 +312,27 @@
</div> </div>
{/if} {/if}
{#if toggleStickers}
<div class="ml-4 flex flex-wrap">
<AssetInput bind:currentCharacter={currentCharacter} onSelect={(additionalAsset)=>{
let fileType = 'img'
if(additionalAsset.length > 2 && additionalAsset[2]) {
const fileExtension = additionalAsset[2]
if(fileExtension === 'mp4' || fileExtension === 'webm')
fileType = 'video'
else if(fileExtension === 'mp3' || fileExtension === 'wav')
fileType = 'audio'
}
messageInput += `<span class='notranslate' translate='no'>{{${fileType}::${additionalAsset[0]}}}</span> *${additionalAsset[0]} added*`
updateInputSizeAll()
}}/>
</div>
{/if}
{#if $DataBase.useAutoSuggestions} {#if $DataBase.useAutoSuggestions}
<Suggestion messageInput={(msg)=>messageInput=msg} {send}/> <Suggestion messageInput={(msg)=>messageInput=msg} {send}/>
{/if} {/if}
{#each messageForm($DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message, loadPages) as chat, i} {#each messageForm($DataBase.characters[$selectedCharID].chats[$DataBase.characters[$selectedCharID].chatPage].message, loadPages) as chat, i}
{#if chat.role === 'char'} {#if chat.role === 'char'}
{#if $DataBase.characters[$selectedCharID].type !== 'group'} {#if $DataBase.characters[$selectedCharID].type !== 'group'}

View File

@@ -141,4 +141,12 @@
<div class="flex items-center mt-2"> <div class="flex items-center mt-2">
<Check bind:check={$DataBase.useChatCopy} name={language.useChatCopy}/> <Check bind:check={$DataBase.useChatCopy} name={language.useChatCopy}/>
</div> </div>
{#if $DataBase.useExperimental}
<div class="flex items-center mt-2">
<Check bind:check={$DataBase.useChatSticker} name={language.useChatSticker}/>
<Help key="experimental" name={language.useChatSticker}/>
</div>
{/if}

View File

@@ -16,7 +16,7 @@
import RegexData from "./RegexData.svelte"; import RegexData from "./RegexData.svelte";
import { exportChar, shareRisuHub } from "src/ts/characterCards"; import { exportChar, shareRisuHub } from "src/ts/characterCards";
import { getElevenTTSVoices, getWebSpeechTTSVoices, getVOICEVOXVoices } from "src/ts/process/tts"; import { getElevenTTSVoices, getWebSpeechTTSVoices, getVOICEVOXVoices } from "src/ts/process/tts";
import { checkCharOrder } from "src/ts/storage/globalApi"; import { checkCharOrder, getFileSrc } from "src/ts/storage/globalApi";
import { addGroupChar, rmCharFromGroup } from "src/ts/process/group"; import { addGroupChar, rmCharFromGroup } from "src/ts/process/group";
import HubUpload from "../UI/HubUpload.svelte"; import HubUpload from "../UI/HubUpload.svelte";
@@ -95,7 +95,9 @@
currentChar = currentChar currentChar = currentChar
}) })
let assetFileExtensions:string[] = []
let assetFilePath:string[] = []
$: { $: {
if(database.characters[$selectedCharID].chaId === currentChar.data.chaId){ if(database.characters[$selectedCharID].chaId === currentChar.data.chaId){
database.characters[$selectedCharID] = currentChar.data database.characters[$selectedCharID] = currentChar.data
@@ -106,6 +108,20 @@
emos = currentChar.data.emotionImages emos = currentChar.data.emotionImages
DataBase.set(database) DataBase.set(database)
loadTokenize(currentChar.data) loadTokenize(currentChar.data)
if(currentChar.type ==='character'){
if(currentChar.data.additionalAssets){
for(let i = 0; i < currentChar.data.additionalAssets.length; i++){
if(currentChar.data.additionalAssets[i].length > 2 && currentChar.data.additionalAssets[i][2]) {
assetFileExtensions[i] = currentChar.data.additionalAssets[i][2]
} else
assetFileExtensions[i] = currentChar.data.additionalAssets[i][1].split('.').pop()
getFileSrc(currentChar.data.additionalAssets[i][1]).then((filePath) => {
assetFilePath[i] = filePath
})
}
}
}
} }
onDestroy(unsub); onDestroy(unsub);
@@ -615,9 +631,10 @@
for(const f of da){ for(const f of da){
console.log(f) console.log(f)
const img = f.data const img = f.data
const imgp = await saveAsset(img)
const name = f.name const name = f.name
currentChar.data.additionalAssets.push([name, imgp]) const extension = name.split('.').pop().toLowerCase()
const imgp = await saveAsset(img,'', extension)
currentChar.data.additionalAssets.push([name, imgp, extension])
currentChar.data.additionalAssets = currentChar.data.additionalAssets currentChar.data.additionalAssets = currentChar.data.additionalAssets
} }
} }
@@ -634,6 +651,16 @@
{#each currentChar.data.additionalAssets as assets, i} {#each currentChar.data.additionalAssets as assets, i}
<tr> <tr>
<td class="font-medium truncate"> <td class="font-medium truncate">
{#if assetFilePath[i]}
{#if assetFileExtensions[i] === 'mp4'}
<!-- svelte-ignore a11y-media-has-caption -->
<video controls class="mt-2 px-2 w-full m-1 rounded-md"><source src={assetFilePath[i]} type="video/mp4"></video>
{:else if assetFileExtensions[i] === 'mp3'}
<audio controls class="mt-2 px-2 w-full h-16 m-1 rounded-md" loop><source src={assetFilePath[i]} type="audio/mpeg"></audio>
{:else}
<img src={assetFilePath[i]} class="w-16 h-16 m-1 rounded-md" alt={assets[0]}/>
{/if}
{/if}
<input class="text-neutral-200 mt-2 mb-4 p-2 bg-transparent input-text focus:bg-selected w-full resize-none" bind:value={currentChar.data.additionalAssets[i][0]} placeholder="..." /> <input class="text-neutral-200 mt-2 mb-4 p-2 bg-transparent input-text focus:bg-selected w-full resize-none" bind:value={currentChar.data.additionalAssets[i][0]} placeholder="..." />
</td> </td>
<th class="font-medium cursor-pointer w-10"> <th class="font-medium cursor-pointer w-10">

View File

@@ -329,7 +329,7 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'
let customScripts:customscript[] = [] let customScripts:customscript[] = []
let utilityBot = false let utilityBot = false
let sdData = defaultSdDataFunc() let sdData = defaultSdDataFunc()
let extAssets:[string,string][] = [] let extAssets:[string,string,string][] = []
if(risuext){ if(risuext){
if(risuext.emotions){ if(risuext.emotions){
@@ -350,8 +350,11 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'
msg: `Loading... (Getting Assets ${i} / ${risuext.additionalAssets.length})` msg: `Loading... (Getting Assets ${i} / ${risuext.additionalAssets.length})`
}) })
await sleep(10) await sleep(10)
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.additionalAssets[i][1])) :Buffer.from(risuext.additionalAssets[i][1], 'base64')) let fileName = ''
extAssets.push([risuext.additionalAssets[i][0],imgp]) if(risuext.additionalAssets[i].length >= 3)
fileName = risuext.additionalAssets[i][2]
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.additionalAssets[i][1])) :Buffer.from(risuext.additionalAssets[i][1], 'base64'), '', fileName)
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
} }
} }
bias = risuext.bias ?? bias bias = risuext.bias ?? bias
@@ -756,7 +759,7 @@ type CharacterCardV2 = {
customScripts?:customscript[] customScripts?:customscript[]
utilityBot?: boolean, utilityBot?: boolean,
sdData?:[string,string][], sdData?:[string,string][],
additionalAssets?:[string,string][], additionalAssets?:[string,string,string][],
backgroundHTML?:string backgroundHTML?:string
} }
} }

View File

@@ -64,9 +64,9 @@ export async function ParseMarkdown(data:string, char:(character | groupChat) =
for(const asset of char.additionalAssets){ for(const asset of char.additionalAssets){
const assetPath = await getFileSrc(asset[1]) const assetPath = await getFileSrc(asset[1])
data = data.replaceAll(`{{raw::${asset[0]}}}`, assetPath). data = data.replaceAll(`{{raw::${asset[0]}}}`, assetPath).
replaceAll(`{{img::${asset[0]}}}`,`<img src="${assetPath}" />`) replaceAll(`{{img::${asset[0]}}}`,`<img src="${assetPath}" alt="${asset[0]}"/>`)
.replaceAll(`{{video::${asset[0]}}}`,`<video autoplay loop><source src="${assetPath}" type="video/mp4"></video>`) .replaceAll(`{{video::${asset[0]}}}`,`<video controls autoplay loop><source src="${assetPath}" type="video/mp4"></video>`)
.replaceAll(`{{audio::${asset[0]}}}`,`<audio autoplay loop><source src="${assetPath}" type="audio/mpeg"></audio>`) .replaceAll(`{{audio::${asset[0]}}}`,`<audio controls autoplay loop><source src="${assetPath}" type="audio/mpeg"></audio>`)
if(mode === 'back'){ if(mode === 'back'){
data = data.replaceAll(`{{bg::${asset[0]}}}`, `<div style="width:100%;height:100%;background: linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8)),url(${assetPath}); background-size: cover;"></div>`) data = data.replaceAll(`{{bg::${asset[0]}}}`, `<div style="width:100%;height:100%;background: linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8)),url(${assetPath}); background-size: cover;"></div>`)
} }

View File

@@ -404,6 +404,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
else if(req.type === 'streaming'){ else if(req.type === 'streaming'){
const reader = req.result.getReader() const reader = req.result.getReader()
const msgIndex = db.characters[selectedChar].chats[selectedChat].message.length const msgIndex = db.characters[selectedChar].chats[selectedChat].message.length
db.characters[selectedChar].chats[selectedChat].isStreaming = true
db.characters[selectedChar].chats[selectedChat].message.push({ db.characters[selectedChar].chats[selectedChat].message.push({
role: 'char', role: 'char',
data: "", data: "",
@@ -419,6 +420,8 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n
setDatabase(db) setDatabase(db)
} }
if(readed.done){ if(readed.done){
db.characters[selectedChar].chats[selectedChat].isStreaming = false
setDatabase(db)
break break
} }
} }

View File

@@ -339,7 +339,7 @@ export interface character{
VOLUME_SCALE?: number VOLUME_SCALE?: number
} }
supaMemory?:boolean supaMemory?:boolean
additionalAssets?:[string, string][] additionalAssets?:[string, string, string][]
ttsReadOnlyQuoted?:boolean ttsReadOnlyQuoted?:boolean
replaceGlobalNote:string replaceGlobalNote:string
backgroundHTML?:string backgroundHTML?:string
@@ -522,7 +522,8 @@ export interface Database{
expires_in?: number expires_in?: number
} }
}, },
classicMaxWidth: boolean classicMaxWidth: boolean,
useChatSticker:boolean,
} }
interface hordeConfig{ interface hordeConfig{
@@ -561,6 +562,7 @@ export interface Chat{
supaMemoryData?:string supaMemoryData?:string
lastMemory?:string lastMemory?:string
suggestMessages?:string[] suggestMessages?:string[]
isStreaming?:boolean
} }
export interface Message{ export interface Message{

View File

@@ -170,7 +170,7 @@ export async function readImage(data:string) {
} }
} }
export async function saveAsset(data:Uint8Array, customId:string = ''){ export async function saveAsset(data:Uint8Array, customId:string = '', fileName:string = ''){
let id = '' let id = ''
if(customId !== ''){ if(customId !== ''){
id = customId id = customId
@@ -182,13 +182,17 @@ export async function saveAsset(data:Uint8Array, customId:string = ''){
id = uuidv4() id = uuidv4()
} }
} }
let fileExtension:string = 'png'
if(fileName && fileName.split('.').length > 0){
fileExtension = fileName.split('.').pop()
}
if(isTauri){ if(isTauri){
await writeBinaryFile(`assets/${id}.png`, data ,{dir: BaseDirectory.AppData}) await writeBinaryFile(`assets/${id}.${fileExtension}`, data ,{dir: BaseDirectory.AppData})
return `assets/${id}.png` return `assets/${id}.${fileExtension}`
} }
else{ else{
await forageStorage.setItem(`assets/${id}.png`, data) await forageStorage.setItem(`assets/${id}.${fileExtension}`, data)
return `assets/${id}.png` return `assets/${id}.${fileExtension}`
} }
} }