import { get, writable } from "svelte/store"; import { saveImage, setDatabase, type character, type Chat, defaultSdDataFunc, type loreBook, getDatabase, getCharacterByIndex, setCharacterByIndex } from "./storage/database.svelte"; import { alertAddCharacter, alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert"; import { language } from "../lang"; import { decode as decodeMsgpack } from "msgpackr"; import { checkNullish, findCharacterbyId, getUserName, selectMultipleFile, selectSingleFile, sleep } from "./util"; import { v4 as uuidv4 } from 'uuid'; import { MobileGUIStack, OpenRealmStore, selectedCharID } from "./stores.svelte"; import { checkCharOrder, downloadFile, getFileSrc } from "./globalApi.svelte"; import { reencodeImage } from "./process/files/image"; import { updateInlayScreen } from "./process/inlayScreen"; import { PngChunk } from "./pngChunk"; import { parseMarkdownSafe } from "./parser.svelte"; import { translateHTML } from "./translator/translator"; import { doingChat } from "./process/index.svelte"; import { importCharacter } from "./characterCards"; export function createNewCharacter() { let db = getDatabase() db.characters.push(createBlankChar()) setDatabase(db) checkCharOrder() return db.characters.length - 1 } export function createNewGroup(){ let db = getDatabase() 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(), firstMsgIndex: -1, characterTalks: [], characterActive: [], realmId: '' }) setDatabase(db) checkCharOrder() return db.characters.length - 1 } export async function getCharImage(loc:string, type:'plain'|'css'|'contain'|'lgcss') { if(!loc || loc === ''){ if(type ==='css'){ return '' } return null } const filesrc = await getFileSrc(loc) if(type === 'plain'){ return filesrc } else if(type ==='css'){ return `background: url("${filesrc}");background-size: cover;` } else if(type ='lgcss'){ return `background: url("${filesrc}");background-size: cover;height: 10.66rem;` } else{ return `background: url("${filesrc}");background-size: contain;background-repeat: no-repeat;background-position: center;` } } export async function selectCharImg(charIndex:number) { const selected = await selectSingleFile(['png', 'webp', 'gif', 'jpg', 'jpeg']) if(!selected){ return } const img = selected.data let db = getDatabase() const imgp = await saveImage(await reencodeImage(img)) dumpCharImage(charIndex) db.characters[charIndex].image = imgp setDatabase(db) } export function dumpCharImage(charIndex:number) { let db = getDatabase() const char = db.characters[charIndex] as character if(!char.image || char.image === ''){ return } char.ccAssets ??= [] char.ccAssets.push({ type: 'icon', name: 'iconx', uri: char.image, ext: 'png' }) char.image = '' db.characters[charIndex] = char setDatabase(db) } export function changeCharImage(charIndex:number,changeIndex:number) { let db = getDatabase() const char = db.characters[charIndex] as character const image = char.ccAssets[changeIndex].uri char.ccAssets.splice(changeIndex, 1) dumpCharImage(charIndex) char.image = image db.characters[charIndex] = char 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 = getDatabase() for(const f of selected){ 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 = getDatabase() let dbChar = db.characters[charId] if(dbChar.type !== 'group'){ dbChar.emotionImages.splice(emotionId, 1) db.characters[charId] = dbChar } setDatabase(db) } export async function exportChat(page:number){ try { const mode = await alertSelect(['Export as JSON', "Export as TXT", "Export as HTML File", "Export as HTML Embed"]) const doTranslate = (mode === '2' || mode === '3') ? (await alertSelect([language.translateContent, language.doNotTranslate])) === '0' : false const anonymous = (mode === '2' || mode === '3') ? ((await alertSelect([language.includePersonaName, language.hidePersonaName])) === '1') : false const selectedID = get(selectedCharID) const db = getDatabase() const chat = db.characters[selectedID].chats[page] const char = db.characters[selectedID] const date = new Date().toJSON(); const htmlChatParse = async (v:string) => { v = parseMarkdownSafe(v) if(doTranslate){ v = await translateHTML(v, false, '', -1) } if(anonymous){ //case insensitive match, replace all const excapedName = char.name.replace(/[-\/\\^$*+\?\.()|[\]{}]/g, '\\$&') v = v.replace(new RegExp(`${excapedName}`, 'gi'), '×××') } return v } 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 if(mode === '2'){ let chatContentHTML = '' let i = 0 for(const v of chat.message){ alertWait(`Translating... ${i++}/${chat.message.length}`) const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : getUserName() chatContentHTML += `
| Character | Message |
|---|---|
| ${char.name} | ${await htmlChatParse(char.firstMessage)} |
Chat from RisuAI
` //copy to clipboard const item = new ClipboardItem({ 'text/html': new Blob([template], { type: 'text/html' }), 'text/plain': new Blob([template], { type: 'text/plain' }) }) await navigator.clipboard.write([item]) alertNormal(language.clipboardSuccess) return } 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 : getUserName()}\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','txt','html']) if(!dat){ return } try { const selectedID = get(selectedCharID) let db = getDatabase() 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: [], fmIndex: -1 } 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.unshift(newChat) setDatabase(db) alertNormal(language.successImport) } else if(dat.name.endsWith('json')){ 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))){ das.fmIndex ??= -1 db.characters[selectedID].chats.unshift(das) setDatabase(db) alertNormal(language.successImport) return } else{ alertError(language.errors.noData) return } } else{ alertError(language.errors.noData) return } } else if(dat.name.endsWith('html')){ const doc = new DOMParser().parseFromString(Buffer.from(dat.data).toString('utf-8'), 'text/html') const chat = doc.querySelector('.idat').textContent const json = JSON.parse(chat) if(json.message && json.note && json.name && json.localLore){ db.characters[selectedID].chats.unshift(json) setDatabase(db) alertNormal(language.successImport) } else{ alertError(language.errors.noData) } } } catch (error) { alertError(`${error}`) } } function formatTavernChat(chat:string, charName:string){ const db = getDatabase() return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, getUserName()).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName) } export function characterFormatUpdate(indexOrCharacter:number|character, arg:{ updateInteraction?:boolean, } = {}){ let cha = typeof(indexOrCharacter) === 'number' ? getCharacterByIndex(indexOrCharacter) : indexOrCharacter 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 } cha.triggerscript = cha.triggerscript ?? [] cha.alternateGreetings = cha.alternateGreetings ?? [] cha.exampleMessage = cha.exampleMessage ?? '' cha.creatorNotes = cha.creatorNotes ?? '' cha.systemPrompt = cha.systemPrompt ?? '' cha.tags = cha.tags ?? [] cha.creator = cha.creator ?? '' cha.characterVersion = cha.characterVersion ?? '' cha.personality = cha.personality ?? '' cha.scenario = cha.scenario ?? '' cha.firstMsgIndex = cha.firstMsgIndex ?? -1 cha.additionalData = cha.additionalData ?? { tag: [], creator: '', character_version: '' } cha.voicevoxConfig = cha.voicevoxConfig ?? { SPEED_SCALE: 1, PITCH_SCALE: 0, INTONATION_SCALE: 1, VOLUME_SCALE: 1 } if(cha.postHistoryInstructions){ cha.chats[cha.chatPage].note += "\n" + cha.postHistoryInstructions cha.chats[cha.chatPage].note = cha.chats[cha.chatPage].note.trim() cha.postHistoryInstructions = null } cha.additionalText ??= '' cha.depth_prompt ??= { depth: 0, prompt: '' } cha.hfTTS ??= { model: '', language: 'en' } cha.backgroundHTML ??= '' cha.backgroundCSS ??= '' cha.creation_date ??= Date.now() cha.globalLore = updateLorebooks(cha.globalLore) if(!cha.newGenData){ cha = updateInlayScreen(cha) } cha.ttsMode ||= 'none' } else{ if((!cha.characterTalks) || cha.characterTalks.length !== cha.characters.length){ cha.characterTalks = [] for(let i=0;i