diff --git a/src/lang/en.ts b/src/lang/en.ts index f14c1f51..4f2785bc 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -658,4 +658,9 @@ export const languageEnglish = { notCharxWarn: "This character uses multiple assets. it is recommended to export this character as a CharX format for better compatibility.", noPlugins: "No Plugins Installed", legacyTranslation: "Legacy Translation", + clipboardSuccess: "Copied to Clipboard", + translateContent: 'Translate Content', + doNotTranslate: "Do Not Translate", + includePersonaName: "Include Persona Name", + hidePersonaName: "Hide Persona Name", } \ No newline at end of file diff --git a/src/lib/UI/Realm/RealmPopUp.svelte b/src/lib/UI/Realm/RealmPopUp.svelte index defb9f1d..a1fd7761 100644 --- a/src/lib/UI/Realm/RealmPopUp.svelte +++ b/src/lib/UI/Realm/RealmPopUp.svelte @@ -95,7 +95,7 @@ {/if} diff --git a/src/ts/characters.ts b/src/ts/characters.ts index ff7fd1d4..8c443b64 100644 --- a/src/ts/characters.ts +++ b/src/ts/characters.ts @@ -1,6 +1,6 @@ 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 } from "./alert"; +import { alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert"; import { language } from "../lang"; import { decode as decodeMsgpack } from "msgpackr"; import { checkNullish, findCharacterbyId, selectMultipleFile, selectSingleFile, sleep } from "./util"; @@ -10,6 +10,8 @@ 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) @@ -153,12 +155,31 @@ export async function rmCharEmotion(charId:number, emotionId:number) { export async function exportChat(page:number){ try { - const mode = await alertSelect(['Export as JSON', "Export as TXT"]) + 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', @@ -168,20 +189,132 @@ export async function exportChat(page:number){ 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 ? '×××' : db.username + 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 ? '×××' : db.username + chatContentHTML += ` + ${name} + ${await htmlChatParse(v.data)} + ` + } + + const template = ` + + + + + + + + + + ${chatContentHTML} +
CharacterMessage
${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}` + return `--${findCharacterbyId(v.saying).name}\n${v.data}` } else{ - return `${v.role === 'char' ? char.name : db.username}\n${v.data}` + return `--${v.role === 'char' ? char.name : db.username}\n${v.data}` } }).join('\n\n') if(char.type !== 'group'){ - stringl = `${char.name}\n${char.firstMessage}\n\n` + stringl + stringl = `--${char.name}\n${char.firstMessage}\n\n` + stringl } await downloadFile(`${char.name}_${date}_chat`.replace(/[<>:"/\\|?*\.\,]/g, "") + '.txt', Buffer.from(stringl, 'utf-8')) @@ -194,7 +327,7 @@ export async function exportChat(page:number){ } export async function importChat(){ - const dat =await selectSingleFile(['json','jsonl']) + const dat =await selectSingleFile(['json','jsonl','txt','html']) if(!dat){ return } @@ -236,7 +369,7 @@ export async function importChat(){ setDatabase(db) alertNormal(language.successImport) } - else{ + 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 @@ -256,7 +389,19 @@ export async function importChat(){ 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}`) }