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" import { Marked } from "marked" const mconverted = new Marked({ gfm: true, breaks: true, silent: true, tokenizer: { } }) 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, db.username) .replace(/(\{\{((set)|(get))var::.+?\}\})/gu,'') } 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'; if(!(get(DataBase).allowAllExtentionFiles || checkIsIos())){ 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() 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'; } else if (child.nodeName === 'DEL') { result += '~' + getNodetextToSentence(child) + '~'; } else { result += getNodetextToSentence(child); } } } return result; } export function applyMarkdownToNode(node: Node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; if (text) { let markdown = mconverted.parseInline(text); if (markdown !== text) { const span = document.createElement('span'); span.innerHTML = markdown; // inherit inline style from the parent node const parentStyle = (node.parentNode as HTMLElement).style; for(let i=0;i