feat: add translate button to HypaV3 modal

This commit is contained in:
Bo26fhmC5M
2025-01-15 02:05:23 +09:00
parent a10a2c5502
commit f397b6ef1a
2 changed files with 420 additions and 159 deletions

View File

@@ -22,9 +22,9 @@
import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme"; import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme";
import Help from "./Help.svelte"; import Help from "./Help.svelte";
import { getChatBranches } from "src/ts/gui/branches"; import { getChatBranches } from "src/ts/gui/branches";
import { getCurrentCharacter } from "src/ts/storage/database.svelte"; import { getCurrentCharacter } from "src/ts/storage/database.svelte";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import HypaV3Modal from './HypaV3Modal.svelte'; import HypaV3Modal from './HypaV3Modal.svelte';
let btn let btn
let input = $state('') let input = $state('')
let cardExportType = $state('realm') let cardExportType = $state('realm')

View File

@@ -1,36 +1,264 @@
<script lang="ts"> <script lang="ts">
import { Trash2Icon, XIcon, StarIcon, RefreshCw } from "lucide-svelte"; import {
Trash2Icon,
XIcon,
LanguagesIcon,
StarIcon,
RefreshCw,
CheckIcon,
} from "lucide-svelte";
import TextAreaInput from "../UI/GUI/TextAreaInput.svelte"; import TextAreaInput from "../UI/GUI/TextAreaInput.svelte";
import { alertConfirm } from "../../ts/alert"; import { alertConfirm } from "../../ts/alert";
import { DBState, alertStore, selectedCharID } from "src/ts/stores.svelte"; import { DBState, alertStore, selectedCharID } from "src/ts/stores.svelte";
import { summarize } from "src/ts/process/memory/hypav3"; import {
type SerializableHypaV3Data,
summarize,
} from "src/ts/process/memory/hypav3";
import { translateHTML } from "src/ts/translator/translator";
import PersonaSettings from "../Setting/Pages/PersonaSettings.svelte";
let hypaV3IsResummarizing = $state(false); type Summary = SerializableHypaV3Data["summaries"][number];
let hypaV3ExpandedChatMemo = $state<{
summaryChatMemos: string[]; interface ExtendedSummary extends Summary {
summaryChatMemo: string; state: {
}>({ isTranslating: boolean;
summaryChatMemos: [], translation?: string | null;
summaryChatMemo: "", isRerolling: boolean;
rerolledText?: string | null;
isRerolledTranslating: boolean;
rerolledTranslation?: string | null;
};
}
interface HypaV3ModalState {
summaries: ExtendedSummary[];
expandedMessage: {
summaryChatMemos: string[];
selectedChatMemo: string;
isTranslating: boolean;
translation?: string | null;
} | null;
}
// Initialize modal state
let modalState = $state<HypaV3ModalState>({
summaries: DBState.db.characters[$selectedCharID].chats[
DBState.db.characters[$selectedCharID].chatPage
].hypaV3Data.summaries.map((s) => {
const summary = s as ExtendedSummary;
summary.state = {
isTranslating: false,
translation: null,
isRerolling: false,
rerolledText: null,
isRerolledTranslating: false,
rerolledTranslation: null,
};
return summary;
}),
expandedMessage: null,
}); });
async function toggleTranslate(summary: ExtendedSummary): Promise<void> {
if (summary.state.isTranslating) return;
if (summary.state.translation) {
summary.state.translation = null;
return;
}
summary.state.isTranslating = true;
summary.state.translation = "Loading...";
const result = await translate(summary.text);
summary.state.translation = result;
summary.state.isTranslating = false;
}
function isRerollable(summary: ExtendedSummary): boolean {
for (const chatMemo of summary.chatMemos) {
if (typeof chatMemo === "string") {
const char = DBState.db.characters[$selectedCharID];
const chat =
char.chats[DBState.db.characters[$selectedCharID].chatPage];
if (!chat.message.find((m) => m.chatId === chatMemo)) {
return false;
}
}
}
return true;
}
async function toggleReroll(summary: ExtendedSummary): Promise<void> {
if (summary.state.isRerolling) return;
if (!isRerollable(summary)) return;
summary.state.isRerolling = true;
summary.state.rerolledText = "Loading...";
try {
const char = DBState.db.characters[$selectedCharID];
const chat = char.chats[DBState.db.characters[$selectedCharID].chatPage];
const firstMessage =
chat.fmIndex === -1
? char.firstMessage
: char.alternateGreetings?.[chat.fmIndex ?? 0];
const toSummarize = summary.chatMemos.map((chatMemo) => {
if (chatMemo == null) {
return {
role: "assistant",
data: firstMessage,
};
}
const msg = chat.message.find((m) => m.chatId === chatMemo);
return msg
? {
role: msg.role === "char" ? "assistant" : msg.role,
data: msg.data,
}
: null;
});
const stringifiedChats = toSummarize
.map((m) => `${m.role}: ${m.data}`)
.join("\n");
const summarizeResult = await summarize(stringifiedChats);
if (summarizeResult.success) {
summary.state.rerolledText = summarizeResult.data;
}
} catch (error) {
summary.state.rerolledText = "Reroll failed";
} finally {
summary.state.isRerolling = false;
}
}
async function toggleTranslateRerolled(
summary: ExtendedSummary
): Promise<void> {
if (summary.state.isRerolledTranslating) return;
if (summary.state.rerolledTranslation) {
summary.state.rerolledTranslation = null;
return;
}
if (!summary.state.rerolledText) return;
summary.state.isRerolledTranslating = true;
summary.state.rerolledTranslation = "Loading...";
const result = await translate(summary.state.rerolledText);
summary.state.rerolledTranslation = result;
summary.state.isRerolledTranslating = false;
}
async function toggleTranslateExpandedMessage(): Promise<void> {
if (!modalState.expandedMessage || modalState.expandedMessage.isTranslating)
return;
if (modalState.expandedMessage.translation) {
modalState.expandedMessage.translation = null;
return;
}
const messageData = getMessageData();
if (!messageData) return;
modalState.expandedMessage.isTranslating = true;
modalState.expandedMessage.translation = "Loading...";
const result = await translate(messageData.data);
modalState.expandedMessage.translation = result;
modalState.expandedMessage.isTranslating = false;
}
function isMessageExpanded(
summary: ExtendedSummary,
chatMemo: string | null
): boolean {
return (
modalState.expandedMessage?.summaryChatMemos === summary.chatMemos &&
modalState.expandedMessage?.selectedChatMemo === chatMemo
);
}
function toggleExpandMessage(
summary: ExtendedSummary,
chatMemo: string | null
): void {
modalState.expandedMessage = isMessageExpanded(summary, chatMemo)
? null
: {
summaryChatMemos: summary.chatMemos,
selectedChatMemo: chatMemo,
isTranslating: false,
translation: null,
};
}
function getMessageData(): { role: string; data: string } | null {
const char = DBState.db.characters[$selectedCharID];
const chat = char.chats[DBState.db.characters[$selectedCharID].chatPage];
const firstMessage =
chat.fmIndex === -1
? char.firstMessage
: char.alternateGreetings?.[chat.fmIndex ?? 0];
const targetMessage =
modalState.expandedMessage?.selectedChatMemo == null
? { role: "char", data: firstMessage }
: chat.message.find(
(m) => m.chatId === modalState.expandedMessage!.selectedChatMemo
);
if (!targetMessage) {
return null;
}
return {
...targetMessage,
role: targetMessage.role === "char" ? char.name : targetMessage.role,
};
}
async function translate(text) {
try {
return await translateHTML(text, false, "", -1);
} catch (error) {
return `Translation failed: ${error}`;
}
}
</script> </script>
<div class="fixed inset-0 z-50 bg-black bg-opacity-50 p-4"> <div class="fixed inset-0 z-50 bg-black/50 p-4">
<div class="h-full w-full flex justify-center"> <div class="h-full w-full flex justify-center">
<div <div
class="bg-darkbg p-4 break-any rounded-md flex flex-col w-full max-w-3xl {DBState class="bg-zinc-900 p-6 rounded-lg flex flex-col w-full max-w-3xl {modalState
.db.characters[$selectedCharID].chats[ .summaries.length === 0
DBState.db.characters[$selectedCharID].chatPage
].hypaV3Data.summaries.length === 0
? 'h-48' ? 'h-48'
: 'max-h-full'}" : 'max-h-full'}"
> >
<!-- Header -->
<div class="flex justify-between items-center w-full mb-4"> <div class="flex justify-between items-center w-full mb-4">
<h1 class="text-xl font-bold">HypaV3 Data</h1> <h1 class="text-2xl font-semibold text-zinc-100">HypaV3 Data</h1>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Reset Button --> <!-- Reset Button -->
<button <button
class="p-2 hover:text-red-500 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 hover:text-rose-300 transition-colors"
onclick={async () => { onclick={async () => {
let confirmed = await alertConfirm( let confirmed = await alertConfirm(
"This action cannot be undone. Do you want to reset HypaV3 data?" "This action cannot be undone. Do you want to reset HypaV3 data?"
@@ -53,9 +281,10 @@
> >
<Trash2Icon size={24} /> <Trash2Icon size={24} />
</button> </button>
<!-- Close Button --> <!-- Close Button -->
<button <button
class="p-2 hover:text-red-500 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 hover:text-rose-300 transition-colors"
onclick={() => { onclick={() => {
alertStore.set({ alertStore.set({
type: "none", type: "none",
@@ -67,167 +296,199 @@
</button> </button>
</div> </div>
</div> </div>
<div class="flex flex-col gap-4 w-full overflow-y-auto">
{#each DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].hypaV3Data.summaries as summary, i} <!-- Summaries List -->
<div class="flex flex-col gap-3 w-full overflow-y-auto">
{#each modalState.summaries as summary, i}
<div <div
class="flex flex-col p-4 rounded-md border-darkborderc border bg-bgcolor" class="flex flex-col p-4 rounded-lg border border-zinc-700 bg-zinc-800/50"
> >
<!-- Summary Area --> <!-- Summary Header -->
<div class="mb-4"> <div class="flex justify-between items-center mb-2">
<div class="flex justify-between items-center mb-2"> <span class="text-sm text-textcolor2">Summary #{i + 1}</span>
<span class="text-sm text-textcolor2">Summary #{i + 1}</span> <div class="flex items-center gap-4">
<div class="flex items-center gap-4"> <!-- Translate Button -->
<!-- Important Button --> <button
<button class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
class="p-1 hover:text-yellow-500 transition-colors {summary.isImportant onclick={async () => await toggleTranslate(summary)}
? 'text-yellow-500' >
: 'text-textcolor2'}" <LanguagesIcon size={16} />
onclick={() => { </button>
summary.isImportant = !summary.isImportant;
}}
>
<StarIcon size={16} />
</button>
<!-- Resummarize Button -->
<button
class="p-1 hover:text-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onclick={async () => {
hypaV3IsResummarizing = true;
try { <!-- Important Button -->
const char = DBState.db.characters[$selectedCharID]; <button
const chat = class="p-2 hover:text-zinc-200 hover:text-amber-300 transition-colors {summary.isImportant
char.chats[ ? 'text-yellow-500'
DBState.db.characters[$selectedCharID].chatPage : 'text-zinc-400'}"
]; onclick={() => {
const firstMessage = summary.isImportant = !summary.isImportant;
chat.fmIndex === -1 }}
? char.firstMessage >
: char.alternateGreetings?.[chat.fmIndex ?? 0]; <StarIcon size={16} />
const toSummarize = summary.chatMemos.map( </button>
(chatMemo) => {
if (chatMemo == null) {
return {
role: "assistant",
data: firstMessage,
};
}
const msg = chat.message.find( <!-- Reroll Button -->
(m) => m.chatId === chatMemo <button
); class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
return msg onclick={async () => await toggleReroll(summary)}
? { disabled={!isRerollable(summary)}
role: >
msg.role === "char" <RefreshCw size={16} />
? "assistant" </button>
: msg.role,
data: msg.data,
}
: null;
}
);
const stringifiedChats = toSummarize
.map((m) => `${m.role}: ${m.data}`)
.join("\n");
const summarizeResult =
await summarize(stringifiedChats);
if (summarizeResult.success) {
summary.text = summarizeResult.data;
}
} finally {
hypaV3IsResummarizing = false;
}
}}
disabled={hypaV3IsResummarizing}
>
<RefreshCw size={16} />
</button>
</div>
</div> </div>
<!-- Editable Summary -->
<TextAreaInput bind:value={summary.text} className="bg-darkbg" />
</div> </div>
<!-- Connected Messages --> <!-- Original Summary -->
<div class="mt-2"> <TextAreaInput
<span class="text-sm text-textcolor2 mb-2 block"> bind:value={summary.text}
Connected Messages ({summary.chatMemos.length}) className="w-full bg-zinc-900 text-zinc-200 rounded-md p-3 min-h-[100px] resize-y"
</span> />
<div class="flex flex-col gap-2">
<!-- Message ID --> <!-- Translation (if exists) -->
<div class="flex flex-wrap gap-1"> {#if summary.state.translation}
{#each summary.chatMemos as chatMemo} <div class="mt-4">
<span class="text-sm text-textcolor2 mb-2 block"
>Translation</span
>
<div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
>
{summary.state.translation}
</div>
</div>
{/if}
<!-- Rerolled Summary (if exists) -->
{#if summary.state.rerolledText}
<div class="mt-4">
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-textcolor2">Rerolled Summary</span>
<div class="flex items-center gap-2">
<!-- Translate Rerolled Button -->
<button <button
class="text-xs px-2 py-1 bg-darkbg rounded-full text-textcolor2 hover:bg-opacity-80 cursor-pointer {hypaV3ExpandedChatMemo.summaryChatMemos === class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
summary.chatMemos && onclick={async () =>
hypaV3ExpandedChatMemo.summaryChatMemo === chatMemo await toggleTranslateRerolled(summary)}
? 'ring-1 ring-blue-500' >
: ''}" <LanguagesIcon size={16} />
</button>
<!-- Cancel Button -->
<button
class="p-2 text-zinc-400 hover:text-zinc-200 hover:text-rose-300 transition-colors"
onclick={() => { onclick={() => {
hypaV3ExpandedChatMemo = summary.state.rerolledText = null;
hypaV3ExpandedChatMemo.summaryChatMemos === summary.state.rerolledTranslation = null;
summary.chatMemos &&
hypaV3ExpandedChatMemo.summaryChatMemo === chatMemo
? { summaryChatMemos: [], summaryChatMemo: "" }
: {
summaryChatMemos: summary.chatMemos,
summaryChatMemo: chatMemo,
};
}} }}
> >
{chatMemo == null ? "First Message" : chatMemo} <XIcon size={16} />
</button> </button>
{/each}
</div>
<!-- Message Content Area --> <!-- Apply Button -->
{#if hypaV3ExpandedChatMemo.summaryChatMemos === summary.chatMemos && hypaV3ExpandedChatMemo.summaryChatMemo !== ""} <button
<div class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
class="text-sm bg-darkbg/50 rounded border border-darkborderc" onclick={() => {
> summary.text = summary.state.rerolledText!;
<div summary.state.rerolledText = null;
class="p-2 max-h-48 overflow-y-auto" summary.state.rerolledTranslation = null;
style="white-space: pre-wrap;" }}
> >
{(() => { <CheckIcon size={16} />
const char = DBState.db.characters[$selectedCharID]; </button>
const chat = </div>
char.chats[ </div>
DBState.db.characters[$selectedCharID].chatPage <TextAreaInput
]; bind:value={summary.state.rerolledText}
const firstMessage = className="w-full bg-zinc-900 text-zinc-200 rounded-md p-3 min-h-[100px] resize-y"
chat.fmIndex === -1 />
? char.firstMessage
: char.alternateGreetings?.[chat.fmIndex ?? 0];
const targetMessage =
hypaV3ExpandedChatMemo.summaryChatMemo == null
? { role: "char", data: firstMessage }
: chat.message.find(
(m) =>
m.chatId ===
hypaV3ExpandedChatMemo.summaryChatMemo
);
if (targetMessage) { <!-- Rerolled Translation (if exists) -->
const displayRole = {#if summary.state.rerolledTranslation}
targetMessage.role === "char" <div class="mt-4">
? char.name <span class="text-sm text-textcolor2 mb-2 block"
: targetMessage.role; >Rerolled Translation</span
return `${displayRole}:\n${targetMessage.data}`; >
} <div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
return "Message not found"; >
})()} {summary.state.rerolledTranslation}
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
{/if}
<!-- Connected Messages -->
<div class="mt-4">
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-textcolor2">
Connected Messages ({summary.chatMemos.length})
</span>
<!-- Translate Message Button -->
<button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
onclick={async () => await toggleTranslateExpandedMessage()}
>
<LanguagesIcon size={16} />
</button>
</div>
<!-- Message IDs -->
<div class="flex flex-wrap gap-1">
{#each summary.chatMemos as chatMemo}
<button
class="text-xs px-3 py-1.5 bg-zinc-900 text-zinc-300 rounded-full hover:bg-zinc-800 transition-colors {isMessageExpanded(
summary,
chatMemo
)
? 'ring-1 ring-blue-500'
: ''}"
onclick={() => toggleExpandMessage(summary, chatMemo)}
>
{chatMemo == null ? "First Message" : chatMemo}
</button>
{/each}
</div>
<!-- Selected Message Content -->
{#if modalState.expandedMessage?.summaryChatMemos === summary.chatMemos}
{@const messageData = getMessageData()}
<div class="mt-4">
{#if messageData}
<!-- Role -->
<div class="text-sm text-textcolor2 mb-2 block">
{messageData.role}:
</div>
<!-- Content -->
<div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
>
{messageData.data}
</div>
{:else}
<div class="text-sm text-red-500">Message not found</div>
{/if}
<!-- Message Translation -->
{#if modalState.expandedMessage.translation}
<div class="mt-4">
<span class="text-sm text-textcolor2 mb-2 block"
>Translation</span
>
<div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
>
{modalState.expandedMessage.translation}
</div>
</div>
{/if}
</div>
{/if}
</div> </div>
</div> </div>
{/each} {/each}
{#if DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].hypaV3Data.summaries.length === 0}
{#if modalState.summaries.length === 0}
<span class="text-textcolor2 text-center p-4">No summaries yet</span> <span class="text-textcolor2 text-center p-4">No summaries yet</span>
{/if} {/if}
</div> </div>