import { get, writable, type Writable } from "svelte/store" import type { Database, Message } from "./storage/database" import { DataBase } from "./storage/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 "./storage/globalApi" export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 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() } let a:Messagec[] = [] for(let i=0;i setTimeout(resolve, ms) ); } export function checkNullish(data:any){ return data === undefined || data === null } const domSelect = true export async function selectSingleFile(ext:string[]){ if(domSelect){ 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, getUserName()) .replace(/(\{\{((set)|(get))var::.+?\}\})/gu,'') } function checkPersonaBinded(){ try { let db = get(DataBase) const selectedChar = get(selectedCharID) const character = db.characters[selectedChar] const chat = character.chats[character.chatPage] console.log(chat.bindedPersona) if(!chat.bindedPersona){ return null } const persona = db.personas.find(v => v.id === chat.bindedPersona) console.log(db.personas, persona) return persona } catch (error) { return null } } export function getUserName(){ const bindedPersona = checkPersonaBinded() if(bindedPersona){ return bindedPersona.name } const db = get(DataBase) return db.username ?? 'User' } export function getUserIcon(){ const bindedPersona = checkPersonaBinded() console.log(`Icon: ${bindedPersona?.icon}`) if(bindedPersona){ return bindedPersona.icon } const db = get(DataBase) return db.userIcon ?? '' } export function getPersonaPrompt(){ const bindedPersona = checkPersonaBinded() if(bindedPersona){ return bindedPersona.personaPrompt } const db = get(DataBase) return db.personaPrompt ?? '' } export function getUserIconProtrait(){ try { const bindedPersona = checkPersonaBinded() if(bindedPersona){ return bindedPersona.largePortrait } const db = get(DataBase) return db.personas[db.selectedPersona].largePortrait } catch (error) { return false } } export function checkIsIos(){ return /(iPad|iPhone|iPod)/g.test(navigator.userAgent) } export function selectFileByDom(allowedExtensions:string[], multiple:'multiple'|'single' = 'single') { return new Promise((resolve) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.multiple = multiple === 'multiple'; const acceptAll = (get(DataBase).allowAllExtentionFiles || checkIsIos() || allowedExtensions[0] === '*') if(!acceptAll){ if (allowedExtensions && allowedExtensions.length) { fileInput.accept = allowedExtensions.map(ext => `.${ext}`).join(','); } } else{ fileInput.accept = '*' } fileInput.addEventListener('change', (event) => { if (fileInput.files.length === 0) { resolve([]); return; } const files = acceptAll ? Array.from(fileInput.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() 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 findCharacterIndexbyId(id:string) { const db = get(DataBase) let i=0; for(const char of db.characters){ if(char.chaId === id){ return i } i += 1 } return -1 } export function getCharacterIndexObject() { const db = get(DataBase) let i=0; let result:{[key:string]:number} = {} for(const char of db.characters){ result[char.chaId] = i i += 1 } return result } 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 } export function getAuthorNoteDefaultText(){ const db = get(DataBase) const template = db.promptTemplate if(!template){ return '' } for(const v of template){ if(v.type === 'authornote'){ return v.defaultText ?? '' } } return '' } export async function encryptBuffer(data:Uint8Array, keys:string){ // hash the key to get a fixed length key value const keyArray = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(keys)) const key = await window.crypto.subtle.importKey( "raw", keyArray, "AES-GCM", false, ["encrypt", "decrypt"] ) // use web crypto api to encrypt the data const result = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: new Uint8Array(12), }, key, data ) return result } export async function decryptBuffer(data:Uint8Array, keys:string){ // hash the key to get a fixed length key value const keyArray = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(keys)) const key = await window.crypto.subtle.importKey( "raw", keyArray, "AES-GCM", false, ["encrypt", "decrypt"] ) // use web crypto api to encrypt the data const result = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(12), }, key, data ) return result } export function getCurrentCharacter(){ const db = get(DataBase) const selectedChar = get(selectedCharID) return db.characters[selectedChar] } export function toState(t:T):Writable{ return writable(t) } export function BufferToText(data:Uint8Array){ if(!TextDecoder){ return Buffer.from(data).toString('utf-8') } return new TextDecoder().decode(data) } export function encodeMultilangString(data:{[code:string]:string}){ let result = '' if(data.xx){ result = data.xx } for(const key in data){ result = `${result}\n# \`${key}\`\n${data[key]}` } return result } export function parseMultilangString(data:string){ let result:{[code:string]:string} = {} const regex = /# `(.+?)`\n([\s\S]+?)(?=\n# `|$)/g let m:RegExpExecArray while ((m = regex.exec(data)) !== null) { if (m.index === regex.lastIndex) { regex.lastIndex++; } result[m[1]] = m[2] } result.xx = data.replace(regex, '') return result } export const toLangName = (code:string) => { switch(code){ case 'xx':{ //Special case for unknown language return 'Unknown Language' } default:{ return new Intl.DisplayNames([code, 'en'], {type: 'language'}).of(code) } } } export const capitalize = (s:string) => { return s.charAt(0).toUpperCase() + s.slice(1) } export function blobToUint8Array(data:Blob){ return new Promise((resolve,reject) => { const reader = new FileReader() reader.onload = () => { if(reader.result instanceof ArrayBuffer){ resolve(new Uint8Array(reader.result)) } else{ reject(new Error('reader.result is not ArrayBuffer')) } } reader.onerror = () => { reject(reader.error) } reader.readAsArrayBuffer(data) }) } export const languageCodes = ["af","ak","am","an","ar","as","ay","az","be","bg","bh","bm","bn","br","bs","ca","co","cs","cy","da","de","dv","ee","el","en","eo","es","et","eu","fa","fi","fo","fr","fy","ga","gd","gl","gn","gu","ha","he","hi","hr","ht","hu","hy","ia","id","ig","is","it","iu","ja","jv","ka","kk","km","kn","ko","ku","ky","la","lb","lg","ln","lo","lt","lv","mg","mi","mk","ml","mn","mr","ms","mt","my","nb","ne","nl","nn","no","ny","oc","om","or","pa","pl","ps","pt","qu","rm","ro","ru","rw","sa","sd","si","sk","sl","sm","sn","so","sq","sr","st","su","sv","sw","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ug","uk","ur","uz","vi","wa","wo","xh","yi","yo","zh","zu"] export function sfc32(a:number, b:number, c:number, d:number) { return function() { a |= 0; b |= 0; c |= 0; d |= 0; let t = (a + b | 0) + d | 0; d = d + 1 | 0; a = b ^ b >>> 9; b = c + (c << 3) | 0; c = (c << 21 | c >>> 11); c = c + t | 0; return (t >>> 0) / 4294967296; } } export function uuidtoNumber(uuid:string){ let result = 0 for(let i=0;i', ',', '.', '/', '~', '`', ' ', '¡', '¿', '‽', '⁉', "'", '"' ] if(lastChar && !(punctuation.indexOf(lastChar) !== -1 //spacing modifier letters || (lastChar.charCodeAt(0) >= 0x02B0 && lastChar.charCodeAt(0) <= 0x02FF) //combining diacritical marks || (lastChar.charCodeAt(0) >= 0x0300 && lastChar.charCodeAt(0) <= 0x036F) //hebrew punctuation || (lastChar.charCodeAt(0) >= 0x0590 && lastChar.charCodeAt(0) <= 0x05CF) //CJK symbols and punctuation || (lastChar.charCodeAt(0) >= 0x3000 && lastChar.charCodeAt(0) <= 0x303F) )){ return false } return true } export function trimUntilPunctuation(s:string){ let result = s while(result.length > 0 && !isLastCharPunctuation(result)){ result = result.slice(0, -1) } return result } /** * Appends the given last path to the provided URL. * * @param {string} url - The base URL to which the last path will be appended. * @param {string} lastPath - The path to be appended to the URL. * @returns {string} The modified URL with the last path appended. * * @example * appendLastPath("https://github.com/kwaroran/RisuAI","/commits/main") * return 'https://github.com/kwaroran/RisuAI/commits/main' * * @example * appendLastPath("https://github.com/kwaroran/RisuAI/","/commits/main") * return 'https://github.com/kwaroran/RisuAI/commits/main * * @example * appendLastPath("http://127.0.0.1:7997","embeddings") * return 'http://127.0.0.1:7997/embeddings' */ export function appendLastPath(url, lastPath) { // Remove trailing slash from url if exists url = url.replace(/\/$/, ''); // Remove leading slash from lastPath if exists lastPath = lastPath.replace(/^\//, ''); // Concat the url and lastPath return url + '/' + lastPath; } /** * Converts the text content of a given Node object, including HTML elements, into a plain text sentence. * * @param {Node} node - The Node object from which the text content will be extracted. * @returns {string} The plain text sentence representing the content of the Node object. * * @example * const div = document.createElement('div'); * div.innerHTML = 'Hello
WorldDeleted'; * const sentence = getNodetextToSentence(div); * console.log(sentence); // Output: "Hello\nWorld~Deleted~" */ export function getNodetextToSentence(node: Node): string { let result = ''; for (const child of node.childNodes) { if (child.nodeType === Node.TEXT_NODE) { result += child.textContent; } else if (child.nodeType === Node.ELEMENT_NODE) { if (child.nodeName === 'BR') { result += '\n'; continue; } // If a child has a style it's not for a markdown formatting const childStyle = (child as HTMLElement)?.style; if (childStyle?.cssText!== '') { result += getNodetextToSentence(child); continue; } // convert HTML elements to markdown format if (child.nodeName === 'DEL') { result += '~' + getNodetextToSentence(child) + '~'; } else if (child.nodeName === 'STRONG' || child.nodeName === 'B') { result += '**' + getNodetextToSentence(child) + '**'; } else if (child.nodeName === 'EM' || child.nodeName === 'I') { result += '*' + getNodetextToSentence(child) + '*'; } else { result += getNodetextToSentence(child); } } } return result; } export const TagList = [ { value: 'female', alias: [ 'feminine', 'girl' ] }, { value: 'male', alias: [ 'masculine', 'boy' ] }, { value: 'OC', alias: [ 'original-character', 'original-characters', ] }, { value: 'game-character', alias: [ 'video_game', 'video-game', 'game', 'video-game-character' ] }, { value: 'anime', alias: [ 'animation', 'anime-character' ] }, { value: 'v-tuber', alias: [ 'virtual-tuber', 'virtual-youtuber', 'virtual-youtube' ] }, { value: 'fantasy', alias: [ 'mystical' ] }, { value: 'religious', alias: [ 'spiritual', 'faith', 'religion', 'religious-character' ] }, { value: 'comedy', alias: [ 'funny', 'humor', 'humorous' ] }, { value: 'mystery', alias: [ 'mysterious', 'enigma' ] }, { value: 'romance', alias: [ 'love', 'lovers', 'couple' ] }, { value: 'dominance', alias: [ 'dominant', 'dom', 'submissive', 'sub', 'bdsm' ] }, { value: 'yandere', alias: [ 'yan', 'yandere-character' ] }, { value: 'non-character', alias: [ 'not-a-character', 'noncharacter', 'non-characters' ] }, { value: 'simulator', alias: [ 'simulation', 'sim' ] }, { value: 'minor', alias: [ 'underage', 'young' ] }, { value: 'giant', alias: [ 'giantess', 'giant-character' ] }, { value: 'tiny', alias: [ 'tiny-character', 'tiny-characters' ] }, { value: 'realistic', alias: [ 'real', 'real-life' ] }, { value: 'cartoon', alias: [ 'toon', 'animated' ] }, { value: 'furry', alias: [ 'anthropomorphic' ] }, { value: 'kenomimi', alias: [ 'animal-ears', ] }, { value: 'mecha', alias: [ 'robot', 'mech' ] }, { value: 'monster', alias: [ 'creature', 'beast', 'monstrous' ] }, { value: 'alien', alias: [ 'extraterrestrial', 'alien-character' ] }, { value: 'demon', alias: [ 'devil', 'demonic', 'demon-character' ] }, { value: 'angel', alias: [ 'heavenly', 'angelic', 'angel-character' ] }, { value: 'elf', alias: [ 'elven', 'elf-character' ] }, { value: 'mermaid', alias: [ 'merfolk', 'mermaid-character' ] }, { value: 'vampire', alias: [ 'vampiric', 'vampire-character' ] }, { value: 'werewolf', alias: [ 'lycan', 'lycanthrope', 'werewolf-character' ] }, { value: 'zombie', alias: [ 'undead', 'zombie-character' ] }, { value: 'ghost', alias: [ 'spirit', 'apparition', 'ghost-character' ] }, { value: 'witch', alias: [ 'sorceress', 'witch-character' ] }, { value: 'wizard', alias: [ 'sorcerer', 'wizard-character' ] }, { value: 'ninja', alias: [ 'shinobi', 'ninja-character' ] }, { value: 'pirate', alias: [ 'buccaneer', 'pirate-character' ] }, { value: 'knight', alias: [ 'paladin', 'knight-character' ] }, { value: 'samurai', alias: [ 'bushi', 'samurai-character' ] }, { value: 'cowboy', alias: [ 'cowgirl', 'cowboy-character' ] }, { value: 'noble', alias: [ 'royal', 'nobility', 'noble-character' ] }, { value: 'thief', alias: [ 'rogue', 'thief-character' ] }, { value: 'spy', alias: [ 'secret-agent', 'spy-character' ] }, { value: 'soldier', alias: [ 'military', 'soldier-character' ] }, { value: 'villain', alias: [ 'antagonist', 'villain-character' ] }, { value: 'hero', alias: [ 'protagonist', 'hero-character' ] }, { value: 'superhero', alias: [ 'super-hero', 'super-heroine', 'superhero-character' ] }, { value: 'mage', alias: [ 'magician', 'mage-character', 'magical' ] }, { value: 'animal', alias: [ 'pet', 'pet-character' ] }, { value: 'cute', alias: [ 'adorable', 'cute-character' ] }, { value: 'nonbinary', alias: [ 'genderqueer', 'genderfluid' ] }, { value: 'multiple-characters', alias: [ 'group', 'multiple' ] }, { value: 'rpg', alias: [ 'roleplaying', 'role-playing' ] }, { value: 'non-human', alias: [ 'inhuman', 'nonhuman', 'non-human-character', 'not-human' ] } ] export const searchTagList = (query:string) => { const splited = query.split(',').map(v => v.trim()) if(splited.length > 10){ return [] } const realQuery = splited.at(-1).trim().toLowerCase() let result = [] for(const tag of TagList){ if(tag.value.startsWith(realQuery)){ result.push(tag.value) continue } for(const alias of tag.alias){ if(alias.startsWith(realQuery)){ result.push(tag.value) break } } } return result.filter(v => splited.indexOf(v) === -1) } export const isKnownUri = (uri:string) => { return uri.startsWith('http://') || uri.startsWith('https://') || uri.startsWith('ccdefault:') || uri.startsWith('embeded://') } export function parseKeyValue(template:string){ try { if(!template){ return [] } const keyValue:[string, string][] = [] for(const line of template.split('\n')){ const [key, value] = line.split('=') if(key && value){ keyValue.push([key, value]) } } return keyValue } catch (error) { return [] } } export const sortableOptions = { delay: 300, // time in milliseconds to define when the sorting should start delayOnTouchOnly: true } as const