refactor: update UI management to index-based approach

This commit is contained in:
Bo26fhmC5M
2025-01-15 19:56:07 +09:00
parent 3e234dcb15
commit d59cc1a29c

View File

@@ -10,78 +10,94 @@
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 { import { summarize } from "src/ts/process/memory/hypav3";
type SerializableHypaV3Data,
summarize,
} from "src/ts/process/memory/hypav3";
import { translateHTML } from "src/ts/translator/translator"; import { translateHTML } from "src/ts/translator/translator";
import PersonaSettings from "../Setting/Pages/PersonaSettings.svelte";
type Summary = SerializableHypaV3Data["summaries"][number]; interface SummaryUI {
isTranslating: boolean;
interface ExtendedSummary extends Summary { translation: string | null;
state: { isRerolling: boolean;
isTranslating: boolean; rerolledText: string | null;
translation?: string | null; isRerolledTranslating: boolean;
isRerolling: boolean; rerolledTranslation: string | null;
rerolledText?: string | null;
isRerolledTranslating: boolean;
rerolledTranslation?: string | null;
};
} }
interface HypaV3ModalState { interface ExpandedMessageUI {
summaries: ExtendedSummary[]; summaryIndex: number;
expandedMessage: { selectedChatMemo: string;
summaryChatMemos: string[]; isTranslating: boolean;
selectedChatMemo: string; translation?: string | null;
isTranslating: boolean;
translation?: string | null;
} | null;
} }
// Initialize modal state const hypaV3DataState = $state(
let modalState = $state<HypaV3ModalState>({ DBState.db.characters[$selectedCharID].chats[
summaries: DBState.db.characters[$selectedCharID].chats[
DBState.db.characters[$selectedCharID].chatPage DBState.db.characters[$selectedCharID].chatPage
].hypaV3Data.summaries.map((s) => { ].hypaV3Data
const summary = s as ExtendedSummary; );
const summaryUIStates = $state<SummaryUI[]>(
hypaV3DataState.summaries.map(() => ({
isTranslating: false,
translation: null,
isRerolling: false,
rerolledText: null,
isRerolledTranslating: false,
rerolledTranslation: null,
}))
);
let expandedMessageUIState = $state<ExpandedMessageUI | null>(null);
summary.state = { $effect(() => {
isTranslating: false, // Detects changes in all nested properties including summaries
translation: null, hypaV3DataState.summaries;
isRerolling: false, expandedMessageUIState = null;
rerolledText: null,
isRerolledTranslating: false,
rerolledTranslation: null,
};
return summary;
}),
expandedMessage: null,
}); });
function getMessageFromChatMemo(
chatMemo: string | null
): { 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 =
chatMemo == null
? { role: "char", data: firstMessage }
: chat.message.find((m) => m.chatId === chatMemo);
return targetMessage;
}
async function toggleTranslate( async function toggleTranslate(
summary: ExtendedSummary, summaryIndex: number,
regenerate?: boolean regenerate?: boolean
): Promise<void> { ): Promise<void> {
if (summary.state.isTranslating) return; const summaryUIState = summaryUIStates[summaryIndex];
if (summary.state.translation) { if (summaryUIState.isTranslating) return;
summary.state.translation = null;
if (summaryUIState.translation) {
summaryUIState.translation = null;
return; return;
} }
summary.state.isTranslating = true; summaryUIState.isTranslating = true;
summary.state.translation = "Loading..."; summaryUIState.translation = "Loading...";
const result = await translate(summary.text, regenerate); const result = await translate(
hypaV3DataState.summaries[summaryIndex].text,
regenerate
);
summary.state.translation = result; summaryUIState.translation = result;
summary.state.isTranslating = false; summaryUIState.isTranslating = false;
} }
function isRerollable(summary: ExtendedSummary): boolean { function isRerollable(summaryIndex: number): boolean {
const summary = hypaV3DataState.summaries[summaryIndex];
for (const chatMemo of summary.chatMemos) { for (const chatMemo of summary.chatMemos) {
if (typeof chatMemo === "string") { if (typeof chatMemo === "string") {
const char = DBState.db.characters[$selectedCharID]; const char = DBState.db.characters[$selectedCharID];
@@ -93,154 +109,121 @@
} }
} }
} }
return true; return true;
} }
async function toggleReroll(summary: ExtendedSummary): Promise<void> { async function toggleReroll(summaryIndex: number): Promise<void> {
if (summary.state.isRerolling) return; const summaryUIState = summaryUIStates[summaryIndex];
if (!isRerollable(summary)) return;
summary.state.isRerolling = true; if (summaryUIState.isRerolling) return;
summary.state.rerolledText = "Loading..."; if (!isRerollable(summaryIndex)) return;
summaryUIState.isRerolling = true;
summaryUIState.rerolledText = "Loading...";
try { try {
const char = DBState.db.characters[$selectedCharID]; const summary = hypaV3DataState.summaries[summaryIndex];
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) => { const toSummarize = summary.chatMemos.map((chatMemo) => {
if (chatMemo == null) { const message = getMessageFromChatMemo(chatMemo);
return {
role: "assistant",
data: firstMessage,
};
}
const msg = chat.message.find((m) => m.chatId === chatMemo); return {
...message,
return msg role: message.role === "char" ? "assistant" : message.role,
? { };
role: msg.role === "char" ? "assistant" : msg.role,
data: msg.data,
}
: null;
}); });
const stringifiedChats = toSummarize const stringifiedChats = toSummarize
.map((m) => `${m.role}: ${m.data}`) .map((m) => `${m.role}: ${m.data}`)
.join("\n"); .join("\n");
const summarizeResult = await summarize(stringifiedChats); const summarizeResult = await summarize(stringifiedChats);
if (summarizeResult.success) { if (summarizeResult.success) {
summary.state.rerolledText = summarizeResult.data; summaryUIState.rerolledText = summarizeResult.data;
} }
} catch (error) { } catch (error) {
summary.state.rerolledText = "Reroll failed"; summaryUIState.rerolledText = "Reroll failed";
} finally { } finally {
summary.state.isRerolling = false; summaryUIState.isRerolling = false;
} }
} }
async function toggleTranslateRerolled( async function toggleTranslateRerolled(
summary: ExtendedSummary, summaryIndex: number,
regenerate?: boolean regenerate?: boolean
): Promise<void> { ): Promise<void> {
if (summary.state.isRerolledTranslating) return; const summaryUIState = summaryUIStates[summaryIndex];
if (summary.state.rerolledTranslation) { if (summaryUIState.isRerolledTranslating) return;
summary.state.rerolledTranslation = null;
if (summaryUIState.rerolledTranslation) {
summaryUIState.rerolledTranslation = null;
return; return;
} }
if (!summary.state.rerolledText) return; if (!summaryUIState.rerolledText) return;
summary.state.isRerolledTranslating = true; summaryUIState.isRerolledTranslating = true;
summary.state.rerolledTranslation = "Loading..."; summaryUIState.rerolledTranslation = "Loading...";
const result = await translate(summary.state.rerolledText, regenerate); const result = await translate(summaryUIState.rerolledText, regenerate);
summary.state.rerolledTranslation = result; summaryUIState.rerolledTranslation = result;
summary.state.isRerolledTranslating = false; summaryUIState.isRerolledTranslating = false;
} }
async function toggleTranslateExpandedMessage( async function toggleTranslateExpandedMessage(
regenerate?: boolean regenerate?: boolean
): Promise<void> { ): Promise<void> {
if (!modalState.expandedMessage || modalState.expandedMessage.isTranslating) if (!expandedMessageUIState || expandedMessageUIState.isTranslating) return;
return;
if (modalState.expandedMessage.translation) { if (expandedMessageUIState.translation) {
modalState.expandedMessage.translation = null; expandedMessageUIState.translation = null;
return; return;
} }
const messageData = getMessageData(); const message = getMessageFromChatMemo(
expandedMessageUIState.selectedChatMemo
);
if (!messageData) return; if (!message) return;
modalState.expandedMessage.isTranslating = true; expandedMessageUIState.isTranslating = true;
modalState.expandedMessage.translation = "Loading..."; expandedMessageUIState.translation = "Loading...";
const result = await translate(messageData.data, regenerate); const result = await translate(message.data, regenerate);
modalState.expandedMessage.translation = result; expandedMessageUIState.translation = result;
modalState.expandedMessage.isTranslating = false; expandedMessageUIState.isTranslating = false;
} }
function isMessageExpanded( function isMessageExpanded(
summary: ExtendedSummary, summaryIndex: number,
chatMemo: string | null chatMemo: string | null
): boolean { ): boolean {
if (!expandedMessageUIState) return false;
const summary = hypaV3DataState.summaries[summaryIndex];
return ( return (
modalState.expandedMessage?.summaryChatMemos === summary.chatMemos && expandedMessageUIState.summaryIndex === summaryIndex &&
modalState.expandedMessage?.selectedChatMemo === chatMemo expandedMessageUIState.selectedChatMemo === chatMemo
); );
} }
function toggleExpandMessage( function toggleExpandMessage(
summary: ExtendedSummary, summaryIndex: number,
chatMemo: string | null chatMemo: string | null
): void { ): void {
modalState.expandedMessage = isMessageExpanded(summary, chatMemo) expandedMessageUIState = isMessageExpanded(summaryIndex, chatMemo)
? null ? null
: { : {
summaryChatMemos: summary.chatMemos, summaryIndex,
selectedChatMemo: chatMemo, selectedChatMemo: chatMemo,
isTranslating: false, isTranslating: false,
translation: null, 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( async function translate(
text: string, text: string,
regenerate?: boolean regenerate?: boolean
@@ -318,7 +301,7 @@
<div class="fixed inset-0 z-50 bg-black/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-zinc-900 p-6 rounded-lg flex flex-col w-full max-w-3xl {modalState class="bg-zinc-900 p-6 rounded-lg flex flex-col w-full max-w-3xl {hypaV3DataState
.summaries.length === 0 .summaries.length === 0
? 'h-48' ? 'h-48'
: 'max-h-full'}" : 'max-h-full'}"
@@ -370,7 +353,7 @@
<!-- Summaries List --> <!-- Summaries List -->
<div class="flex flex-col gap-3 w-full overflow-y-auto"> <div class="flex flex-col gap-3 w-full overflow-y-auto">
{#each modalState.summaries as summary, i} {#each hypaV3DataState.summaries as summary, i}
<div <div
class="flex flex-col p-4 rounded-lg border border-zinc-700 bg-zinc-800/50" class="flex flex-col p-4 rounded-lg border border-zinc-700 bg-zinc-800/50"
> >
@@ -382,8 +365,8 @@
<button <button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
use:handleDualAction={{ use:handleDualAction={{
onMainAction: () => toggleTranslate(summary, false), onMainAction: () => toggleTranslate(i, false),
onAlternativeAction: () => toggleTranslate(summary, true), onAlternativeAction: () => toggleTranslate(i, true),
}} }}
> >
<LanguagesIcon size={16} /> <LanguagesIcon size={16} />
@@ -404,8 +387,8 @@
<!-- Reroll Button --> <!-- Reroll Button -->
<button <button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
onclick={async () => await toggleReroll(summary)} onclick={async () => await toggleReroll(i)}
disabled={!isRerollable(summary)} disabled={!isRerollable(i)}
> >
<RefreshCw size={16} /> <RefreshCw size={16} />
</button> </button>
@@ -419,7 +402,7 @@
/> />
<!-- Translation (if exists) --> <!-- Translation (if exists) -->
{#if summary.state.translation} {#if summaryUIStates[i].translation}
<div class="mt-4"> <div class="mt-4">
<span class="text-sm text-textcolor2 mb-2 block" <span class="text-sm text-textcolor2 mb-2 block"
>Translation</span >Translation</span
@@ -427,13 +410,13 @@
<div <div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap" class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
> >
{summary.state.translation} {summaryUIStates[i].translation}
</div> </div>
</div> </div>
{/if} {/if}
<!-- Rerolled Summary (if exists) --> <!-- Rerolled Summary (if exists) -->
{#if summary.state.rerolledText} {#if summaryUIStates[i].rerolledText}
<div class="mt-4"> <div class="mt-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">Rerolled Summary</span> <span class="text-sm text-textcolor2">Rerolled Summary</span>
@@ -442,10 +425,9 @@
<button <button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
use:handleDualAction={{ use:handleDualAction={{
onMainAction: () => onMainAction: () => toggleTranslateRerolled(i, false),
toggleTranslateRerolled(summary, false),
onAlternativeAction: () => onAlternativeAction: () =>
toggleTranslateRerolled(summary, true), toggleTranslateRerolled(i, true),
}} }}
> >
<LanguagesIcon size={16} /> <LanguagesIcon size={16} />
@@ -455,8 +437,8 @@
<button <button
class="p-2 text-zinc-400 hover:text-zinc-200 hover:text-rose-300 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 hover:text-rose-300 transition-colors"
onclick={() => { onclick={() => {
summary.state.rerolledText = null; summaryUIStates[i].rerolledText = null;
summary.state.rerolledTranslation = null; summaryUIStates[i].rerolledTranslation = null;
}} }}
> >
<XIcon size={16} /> <XIcon size={16} />
@@ -466,9 +448,9 @@
<button <button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors" class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
onclick={() => { onclick={() => {
summary.text = summary.state.rerolledText!; summary.text = summaryUIStates[i].rerolledText!;
summary.state.rerolledText = null; summaryUIStates[i].rerolledText = null;
summary.state.rerolledTranslation = null; summaryUIStates[i].rerolledTranslation = null;
}} }}
> >
<CheckIcon size={16} /> <CheckIcon size={16} />
@@ -476,12 +458,12 @@
</div> </div>
</div> </div>
<TextAreaInput <TextAreaInput
bind:value={summary.state.rerolledText} bind:value={summaryUIStates[i].rerolledText}
className="w-full bg-zinc-900 text-zinc-200 rounded-md p-3 min-h-[100px] resize-y" className="w-full bg-zinc-900 text-zinc-200 rounded-md p-3 min-h-[100px] resize-y"
/> />
<!-- Rerolled Translation (if exists) --> <!-- Rerolled Translation (if exists) -->
{#if summary.state.rerolledTranslation} {#if summaryUIStates[i].rerolledTranslation}
<div class="mt-4"> <div class="mt-4">
<span class="text-sm text-textcolor2 mb-2 block" <span class="text-sm text-textcolor2 mb-2 block"
>Rerolled Translation</span >Rerolled Translation</span
@@ -489,7 +471,7 @@
<div <div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap" class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
> >
{summary.state.rerolledTranslation} {summaryUIStates[i].rerolledTranslation}
</div> </div>
</div> </div>
{/if} {/if}
@@ -520,12 +502,12 @@
{#each summary.chatMemos as chatMemo} {#each summary.chatMemos as chatMemo}
<button <button
class="text-xs px-3 py-1.5 bg-zinc-900 text-zinc-300 rounded-full hover:bg-zinc-800 transition-colors {isMessageExpanded( class="text-xs px-3 py-1.5 bg-zinc-900 text-zinc-300 rounded-full hover:bg-zinc-800 transition-colors {isMessageExpanded(
summary, i,
chatMemo chatMemo
) )
? 'ring-1 ring-blue-500' ? 'ring-1 ring-blue-500'
: ''}" : ''}"
onclick={() => toggleExpandMessage(summary, chatMemo)} onclick={() => toggleExpandMessage(i, chatMemo)}
> >
{chatMemo == null ? "First Message" : chatMemo} {chatMemo == null ? "First Message" : chatMemo}
</button> </button>
@@ -533,26 +515,28 @@
</div> </div>
<!-- Selected Message Content --> <!-- Selected Message Content -->
{#if modalState.expandedMessage?.summaryChatMemos === summary.chatMemos} {#if expandedMessageUIState?.summaryIndex === i}
{@const messageData = getMessageData()} {@const message = getMessageFromChatMemo(
expandedMessageUIState.selectedChatMemo
)}
<div class="mt-4"> <div class="mt-4">
{#if messageData} {#if message}
<!-- Role --> <!-- Role -->
<div class="text-sm text-textcolor2 mb-2 block"> <div class="text-sm text-textcolor2 mb-2 block">
{messageData.role}: {message.role}'s Message
</div> </div>
<!-- Content --> <!-- Content -->
<div <div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap" class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
> >
{messageData.data} {message.data}
</div> </div>
{:else} {:else}
<div class="text-sm text-red-500">Message not found</div> <div class="text-sm text-red-500">Message not found</div>
{/if} {/if}
<!-- Message Translation --> <!-- Message Translation -->
{#if modalState.expandedMessage.translation} {#if expandedMessageUIState.translation}
<div class="mt-4"> <div class="mt-4">
<span class="text-sm text-textcolor2 mb-2 block" <span class="text-sm text-textcolor2 mb-2 block"
>Translation</span >Translation</span
@@ -560,7 +544,7 @@
<div <div
class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap" class="p-2 max-h-48 overflow-y-auto bg-zinc-800 rounded-md whitespace-pre-wrap"
> >
{modalState.expandedMessage.translation} {expandedMessageUIState.translation}
</div> </div>
</div> </div>
{/if} {/if}
@@ -570,7 +554,7 @@
</div> </div>
{/each} {/each}
{#if modalState.summaries.length === 0} {#if hypaV3DataState.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>