Files
risuai/src/ts/util.ts
2024-09-24 08:54:18 +09:00

1022 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<arg.length;i++){
const m = arg[i]
a.unshift({
role: m.role,
data: reformatContent(m.data),
index: i,
saying: m.saying,
chatId: m.chatId ?? 'none',
generationInfo: m.generationInfo,
})
}
return a.slice(0, loadPages)
}
export function sleep(ms: number) {
return new Promise( resolve => 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}})|(<Char>)|(<char>)/gi, currentChar.name)
.replace(/({{user}})|({{User}})|(<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]
if(!chat.bindedPersona){
return null
}
const persona = db.personas.find(v => v.id === chat.bindedPersona)
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()
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<null|File[]>((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<Uint8Array>((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:T):Writable<T>{
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) => {
try {
switch(code){
case 'xx':{ //Special case for unknown language
return 'Unknown Language'
}
default:{
return new Intl.DisplayNames([code, 'en'], {type: 'language'}).of(code)
}
}
} catch (error) {
return code
}
}
export const capitalize = (s:string) => {
return s.charAt(0).toUpperCase() + s.slice(1)
}
export function blobToUint8Array(data:Blob){
return new Promise<Uint8Array>((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<uuid.length;i++){
result += uuid.charCodeAt(i)
}
return result
}
export function isLastCharPunctuation(s:string){
const lastChar = s.trim().at(-1)
const punctuation = [
'.', '!', '?', '。', '', '', '…', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '<', '>', ',', '.', '/', '~', '`', ' ',
'¡', '¿', '‽', '⁉', "'", '"'
]
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<br>World<del>Deleted</del>';
* 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