import { get, writable } from "svelte/store"; import { DataBase, saveImage, setDatabase, type character, type Chat, defaultSdDataFunc, type loreBook } from "./storage/database"; import { 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 { selectedCharID } from "./stores"; import { checkCharOrder, downloadFile, getFileSrc } from "./storage/globalApi"; import { reencodeImage } from "./process/files/image"; import { updateInlayScreen } from "./process/inlayScreen"; import { PngChunk } from "./pngChunk"; import { parseMarkdownSafe } from "./parser"; import { translateHTML } from "./translator/translator"; export function createNewCharacter() { let db = get(DataBase) db.characters.push(createBlankChar()) setDatabase(db) checkCharOrder() 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(), firstMsgIndex: -1, characterTalks: [], characterActive: [] }) 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 = get(DataBase) const imgp = await saveImage(await reencodeImage(img)) dumpCharImage(charIndex) db.characters[charIndex].image = imgp setDatabase(db) } export function dumpCharImage(charIndex:number) { let db = get(DataBase) 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 = get(DataBase) 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 = get(DataBase) 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 = 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 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 = get(DataBase) 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 += `

${name}

${await htmlChatParse(v.data)}
` } const doc = ` ${char.name} Chat

${char.name}

${await htmlChatParse( char.firstMsgIndex === -1 ? char.firstMessage : char.alternateGreetings?.[char.firstMsgIndex ?? 0] )}
${chatContentHTML}
${ JSON.stringify(chat).replace(//g, '>') }
` await downloadFile(`${char.name}_${date}_chat`.replace(/[<>:"/\\|?*\.\,]/g, "") + '.html', Buffer.from(doc, 'utf-8')) } else if(mode === '3'){ //create a html table 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 += ` ${name} ${await htmlChatParse(v.data)} ` } const template = ` ${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 = 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.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))){ 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 = get(DataBase) return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, getUserName()).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 } 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 { v.bookVersion ??= 1 if(v.bookVersion >= 2){ return v } if(v.activationPercent){ const perc = v.activationPercent v.activationPercent = null v.content = `@@probability ${perc}\n${v.content}` } v.content = v.content.replace(/@@@?end/g, '@@depth 0').replace(/\<(char|bot)\>/g, '{{char}}').replace(/\<(user)\>/g, '{{user}}') v.bookVersion = 2 return v }) } 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: '', creatorNotes:'', systemPrompt:'', postHistoryInstructions:'', alternateGreetings:[], tags:[], creator:"", characterVersion: '', personality:"", scenario:"", firstMsgIndex: -1, replaceGlobalNote: "", triggerscript: [], additionalText: '' } } 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() 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 = PngChunk.read(Buffer.from(imgBuffer), ["risuai"])?.risuai await sleep(10) const va = decodeMsgpack(Buffer.from(readed,'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