feat: add import summary filtering and improve search in HypaV3

- Add important summary filtering button
- Improve search navigation with focus-based results
- Add mobile-friendly previous/next search buttons
This commit is contained in:
Bo26fhmC5M
2025-02-09 15:23:24 +09:00
parent 02e0b326b6
commit 3e3511760e
7 changed files with 488 additions and 358 deletions

View File

@@ -796,7 +796,7 @@ export const languageChinese = {
"convertSuccessMessage": "成功将 HypaV2 数据转换为 V3", "convertSuccessMessage": "成功将 HypaV2 数据转换为 V3",
"convertErrorMessage": "将 HypaV2 数据转换为 V3 失败:{0}", "convertErrorMessage": "将 HypaV2 数据转换为 V3 失败:{0}",
"noSummariesLabel": "尚无总结", "noSummariesLabel": "尚无总结",
"searchPlaceholder": "输入 #N、ID 或搜索查询", "searchPlaceholder": "输入 #N、ID 或搜索关键词",
"summaryNumberLabel": "总结 #{0}", "summaryNumberLabel": "总结 #{0}",
"deleteAfterConfirmMessage": "删除此后的所有总结?", "deleteAfterConfirmMessage": "删除此后的所有总结?",
"deleteAfterConfirmSecondMessage": "此操作无法撤销。您确定吗?", "deleteAfterConfirmSecondMessage": "此操作无法撤销。您确定吗?",

View File

@@ -867,7 +867,7 @@ export const languageEnglish = {
convertSuccessMessage: "Successfully converted HypaV2 data to V3", convertSuccessMessage: "Successfully converted HypaV2 data to V3",
convertErrorMessage: "Failed to convert HypaV2 data to V3: {0}", convertErrorMessage: "Failed to convert HypaV2 data to V3: {0}",
noSummariesLabel: "No summaries yet", noSummariesLabel: "No summaries yet",
searchPlaceholder: "Enter #N, ID, or search query", searchPlaceholder: "Enter #N, ID, or query",
summaryNumberLabel: "Summary #{0}", summaryNumberLabel: "Summary #{0}",
deleteAfterConfirmMessage: "Delete all summaries after this one?", deleteAfterConfirmMessage: "Delete all summaries after this one?",
deleteAfterConfirmSecondMessage: "This action cannot be undone. Are you really sure?", deleteAfterConfirmSecondMessage: "This action cannot be undone. Are you really sure?",

View File

@@ -705,7 +705,7 @@ export const languageSpanish = {
"convertSuccessMessage": "Datos de HypaV2 convertidos exitosamente a V3", "convertSuccessMessage": "Datos de HypaV2 convertidos exitosamente a V3",
"convertErrorMessage": "Error al convertir datos de HypaV2 a V3: {0}", "convertErrorMessage": "Error al convertir datos de HypaV2 a V3: {0}",
"noSummariesLabel": "Aún no hay resúmenes", "noSummariesLabel": "Aún no hay resúmenes",
"searchPlaceholder": "Ingrese #N, ID o consulta de búsqueda", "searchPlaceholder": "Ingrese #N, ID o búsqueda",
"summaryNumberLabel": "Resumen #{0}", "summaryNumberLabel": "Resumen #{0}",
"deleteAfterConfirmMessage": "¿Eliminar todos los resúmenes después de este?", "deleteAfterConfirmMessage": "¿Eliminar todos los resúmenes después de este?",
"deleteAfterConfirmSecondMessage": "Esta acción no se puede deshacer. ¿Está realmente seguro?", "deleteAfterConfirmSecondMessage": "Esta acción no se puede deshacer. ¿Está realmente seguro?",

View File

@@ -792,7 +792,7 @@ export const languageKorean = {
"convertSuccessMessage": "HypaV2 데이터를 V3로 성공적으로 변환했습니다", "convertSuccessMessage": "HypaV2 데이터를 V3로 성공적으로 변환했습니다",
"convertErrorMessage": "HypaV2 데이터를 V3로 변환하는데 실패했습니다: {0}", "convertErrorMessage": "HypaV2 데이터를 V3로 변환하는데 실패했습니다: {0}",
"noSummariesLabel": "아직 요약이 없습니다", "noSummariesLabel": "아직 요약이 없습니다",
"searchPlaceholder": "#N, ID 또는 검색어 입력하세요", "searchPlaceholder": "#N, ID 또는 검색어 입력",
"summaryNumberLabel": "요약 #{0}", "summaryNumberLabel": "요약 #{0}",
"deleteAfterConfirmMessage": "이 요약 이후의 모든 요약을 삭제하시겠습니까?", "deleteAfterConfirmMessage": "이 요약 이후의 모든 요약을 삭제하시겠습니까?",
"deleteAfterConfirmSecondMessage": "이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?", "deleteAfterConfirmSecondMessage": "이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?",

View File

@@ -434,7 +434,7 @@ export const LanguageVietnamese = {
"convertSuccessMessage": "Đã chuyển đổi thành công dữ liệu HypaV2 sang V3", "convertSuccessMessage": "Đã chuyển đổi thành công dữ liệu HypaV2 sang V3",
"convertErrorMessage": "Chuyển đổi dữ liệu HypaV2 sang V3 thất bại: {0}", "convertErrorMessage": "Chuyển đổi dữ liệu HypaV2 sang V3 thất bại: {0}",
"noSummariesLabel": "Chưa có tóm tắt nào", "noSummariesLabel": "Chưa có tóm tắt nào",
"searchPlaceholder": "Nhập #N, ID, hoặc từ khóa tìm kiếm", "searchPlaceholder": "Nhập #N, ID hoặc từ khóa",
"summaryNumberLabel": "Tóm tắt #{0}", "summaryNumberLabel": "Tóm tắt #{0}",
"deleteAfterConfirmMessage": "Xóa tất cả các tóm tắt sau tóm tắt này?", "deleteAfterConfirmMessage": "Xóa tất cả các tóm tắt sau tóm tắt này?",
"deleteAfterConfirmSecondMessage": "Hành động này không thể hoàn tác. Bạn có chắc chắn không?", "deleteAfterConfirmSecondMessage": "Hành động này không thể hoàn tác. Bạn có chắc chắn không?",

View File

@@ -829,7 +829,7 @@ export const languageChineseTraditional = {
"convertSuccessMessage": "成功將 HypaV2 數據轉換為 V3", "convertSuccessMessage": "成功將 HypaV2 數據轉換為 V3",
"convertErrorMessage": "無法將 HypaV2 數據轉換為 V3{0}", "convertErrorMessage": "無法將 HypaV2 數據轉換為 V3{0}",
"noSummariesLabel": "尚無摘要", "noSummariesLabel": "尚無摘要",
"searchPlaceholder": "輸入 #N、ID 或搜索查詢", "searchPlaceholder": "輸入 #N、ID 或搜尋關鍵字",
"summaryNumberLabel": "摘要 #{0}", "summaryNumberLabel": "摘要 #{0}",
"deleteAfterConfirmMessage": "刪除此摘要之後的所有摘要?", "deleteAfterConfirmMessage": "刪除此摘要之後的所有摘要?",
"deleteAfterConfirmSecondMessage": "此操作無法撤銷。您確定要這樣做嗎?", "deleteAfterConfirmSecondMessage": "此操作無法撤銷。您確定要這樣做嗎?",

View File

@@ -5,6 +5,8 @@
SettingsIcon, SettingsIcon,
Trash2Icon, Trash2Icon,
XIcon, XIcon,
ChevronUpIcon,
ChevronDownIcon,
LanguagesIcon, LanguagesIcon,
StarIcon, StarIcon,
RefreshCw, RefreshCw,
@@ -51,20 +53,30 @@
translationRef: HTMLTextAreaElement; translationRef: HTMLTextAreaElement;
} }
interface SearchResult { class SummarySearchResult {
element: HTMLElement; constructor(
matchType: "chatMemo" | "summary"; public summaryIndex: number,
summaryPosition?: { public start: number,
start: number; public end: number
end: number; ) {}
};
} }
class ChatMemoSearchResult {
constructor(
public summaryIndex: number,
public memoIndex: number
) {}
}
type SearchResult = SummarySearchResult | ChatMemoSearchResult;
interface SearchUI { interface SearchUI {
ref: HTMLInputElement; ref: HTMLInputElement;
query: string; query: string;
currentIndex: number;
results: SearchResult[]; results: SearchResult[];
currentResultIndex: number;
requestedSearchFromIndex: number;
isNavigating: boolean;
} }
const hypaV3DataState = $derived( const hypaV3DataState = $derived(
@@ -76,6 +88,7 @@
let summaryUIStates = $state<SummaryUI[]>([]); let summaryUIStates = $state<SummaryUI[]>([]);
let expandedMessageUIState = $state<ExpandedMessageUI>(null); let expandedMessageUIState = $state<ExpandedMessageUI>(null);
let searchUIState = $state<SearchUI>(null); let searchUIState = $state<SearchUI>(null);
let showImportantOnly = $state(false);
$effect.pre(() => { $effect.pre(() => {
summaryUIStates = hypaV3DataState.summaries.map((summary) => ({ summaryUIStates = hypaV3DataState.summaries.map((summary) => ({
@@ -111,8 +124,10 @@
searchUIState = { searchUIState = {
ref: null, ref: null,
query: "", query: "",
currentIndex: -1,
results: [], results: [],
currentResultIndex: -1,
requestedSearchFromIndex: -1,
isNavigating: false,
}; };
// Focus on search element after it's rendered // Focus on search element after it's rendered
@@ -135,122 +150,187 @@
} }
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); // Prevent event default action e.preventDefault?.(); // Prevent event default action
const query = searchUIState.query.trim(); const query = searchUIState.query.trim();
if (!query) return; if (!query) return;
// When received a new query
if (searchUIState.currentResultIndex === -1) {
const results = generateSearchResults(query);
if (results.length === 0) return;
searchUIState.results = results;
}
const nextResult = getNextSearchResult(e.shiftKey);
if (nextResult) {
navigateToSearchResult(nextResult);
}
}
}
function generateSearchResults(query: string): SearchResult[] {
const results: SearchResult[] = [];
const normalizedQuery = query.trim().toLowerCase();
// Search summary index // Search summary index
if (query.match(/^#\d+$/)) { if (query.match(/^#\d+$/)) {
const summaryNumber = parseInt(query.substring(1)) - 1; const summaryNumber = parseInt(query.substring(1)) - 1;
if ( if (
summaryNumber >= 0 && summaryNumber >= 0 &&
summaryNumber < hypaV3DataState.summaries.length summaryNumber < hypaV3DataState.summaries.length &&
(!showImportantOnly ||
hypaV3DataState.summaries[summaryNumber].isImportant)
) { ) {
summaryUIStates[summaryNumber].originalRef.scrollIntoView({ results.push(new SummarySearchResult(summaryNumber, 0, 0));
behavior: "instant",
block: "center",
});
} }
return; return results;
} }
const normalizedQuery = query.toLowerCase();
if (searchUIState.currentIndex === -1) {
const results: SearchResult[] = [];
if (isGuidLike(query)) { if (isGuidLike(query)) {
// Search chatMemo // Search chatMemo
summaryUIStates.forEach((summaryUI) => { summaryUIStates.forEach((summaryUI, summaryIndex) => {
summaryUI.chatMemoRefs.forEach((buttonRef) => { if (
!showImportantOnly ||
hypaV3DataState.summaries[summaryIndex].isImportant
) {
summaryUI.chatMemoRefs.forEach((buttonRef, memoIndex) => {
const buttonText = buttonRef.textContent?.toLowerCase() || ""; const buttonText = buttonRef.textContent?.toLowerCase() || "";
if (buttonText.includes(normalizedQuery)) { if (buttonText.includes(normalizedQuery)) {
results.push({ results.push(new ChatMemoSearchResult(summaryIndex, memoIndex));
element: buttonRef as HTMLButtonElement,
matchType: "chatMemo",
});
} }
}); });
}
}); });
} else { } else {
// Search summary // Search summary
summaryUIStates.forEach((summaryUI) => { summaryUIStates.forEach((summaryUI, summaryIndex) => {
if (
!showImportantOnly ||
hypaV3DataState.summaries[summaryIndex].isImportant
) {
const textAreaText = summaryUI.originalRef.value?.toLowerCase(); const textAreaText = summaryUI.originalRef.value?.toLowerCase();
let pos = -1; let pos = -1;
while ( while (
(pos = textAreaText.indexOf(normalizedQuery, pos + 1)) !== -1 (pos = textAreaText.indexOf(normalizedQuery, pos + 1)) !== -1
) { ) {
results.push({ results.push(
element: summaryUI.originalRef as HTMLTextAreaElement, new SummarySearchResult(
matchType: "summary", summaryIndex,
summaryPosition: { pos,
start: pos, pos + normalizedQuery.length
end: pos + normalizedQuery.length, )
}, );
}); }
} }
}); });
} }
searchUIState.results = results; return results;
} }
if (searchUIState.results.length === 0) return; function isGuidLike(str: string): boolean {
const strTrimed = str.trim();
// Move to next result // Exclude too short inputs
searchUIState.currentIndex = if (strTrimed.length < 4) return false;
(searchUIState.currentIndex + 1) % searchUIState.results.length;
const result = searchUIState.results[searchUIState.currentIndex]; return /^[0-9a-f]{4,12}(-[0-9a-f]{4,12}){0,4}-?$/i.test(strTrimed);
}
function getNextSearchResult(backward: boolean): SearchResult | null {
if (!searchUIState || searchUIState.results.length === 0) return null;
let nextIndex: number;
if (searchUIState.requestedSearchFromIndex !== -1) {
const fromSummaryIndex = searchUIState.requestedSearchFromIndex;
nextIndex = backward
? searchUIState.results.findLastIndex(
(r) => r.summaryIndex <= fromSummaryIndex
)
: searchUIState.results.findIndex(
(r) => r.summaryIndex >= fromSummaryIndex
);
if (nextIndex === -1) {
nextIndex = backward ? searchUIState.results.length - 1 : 0;
}
searchUIState.requestedSearchFromIndex = -1;
} else {
const delta = backward ? -1 : 1;
nextIndex =
(searchUIState.currentResultIndex +
delta +
searchUIState.results.length) %
searchUIState.results.length;
}
searchUIState.currentResultIndex = nextIndex;
return searchUIState.results[nextIndex];
}
function navigateToSearchResult(result: SearchResult) {
searchUIState.isNavigating = true;
if (result instanceof SummarySearchResult) {
const textarea = summaryUIStates[result.summaryIndex].originalRef;
// Scroll to element // Scroll to element
result.element.scrollIntoView({ textarea.scrollIntoView({
behavior: "instant", behavior: "instant",
block: "center", block: "center",
}); });
if (result.matchType === "chatMemo") { if (result.start === result.end) {
// Highlight chatMemo result return;
result.element.classList.add("ring-2", "ring-zinc-500"); }
// Remove highlight after a short delay // Scroll to query
window.setTimeout(() => { textarea.setSelectionRange(result.start, result.end);
result.element.classList.remove("ring-2", "ring-zinc-500");
}, 1000);
} else {
// Handle summary text selection
const textarea = result.element as HTMLTextAreaElement;
// Make readonly temporarily
textarea.readOnly = true;
// Select query
textarea.setSelectionRange(
result.summaryPosition.start,
result.summaryPosition.end
);
textarea.focus();
scrollToSelection(textarea); scrollToSelection(textarea);
// This only works on firefox // Highlight query on desktop environment
//textarea.scrollTop = textarea.scrollHeight; // Scroll to the bottom if (!("ontouchend" in window)) {
//textarea.blur(); // Collapse selection // Make readonly temporarily
//textarea.focus(); // This scrolls the textarea textarea.readOnly = true;
textarea.focus();
// Highlight textarea
window.setTimeout(() => { window.setTimeout(() => {
searchUIState.ref.focus(); // Restore focus to search bar searchUIState.ref.focus(); // Restore focus to search bar
textarea.readOnly = false; // Remove readonly after focus moved textarea.readOnly = false; // Remove readonly after focus moved
}, 300); }, 300);
} }
} else {
const button =
summaryUIStates[result.summaryIndex].chatMemoRefs[result.memoIndex];
// Scroll to element
button.scrollIntoView({
behavior: "instant",
block: "center",
});
// Highlight chatMemo
button.classList.add("ring-2", "ring-zinc-500");
// Remove highlight after a short delay
window.setTimeout(() => {
button.classList.remove("ring-2", "ring-zinc-500");
}, 1000);
} }
searchUIState.isNavigating = false;
} }
function scrollToSelection(textarea: HTMLTextAreaElement) { function scrollToSelection(textarea: HTMLTextAreaElement) {
@@ -287,15 +367,6 @@
textarea.scrollTop = selectionTop - textarea.clientHeight / 2; textarea.scrollTop = selectionTop - textarea.clientHeight / 2;
} }
function isGuidLike(str: string): boolean {
const strTrimed = str.trim();
// Exclude too short inputs
if (strTrimed.length < 4) return false;
return /^[0-9a-f]{4,12}(-[0-9a-f]{4,12}){0,4}-?$/i.test(strTrimed);
}
async function toggleTranslate( async function toggleTranslate(
summaryIndex: number, summaryIndex: number,
regenerate?: boolean regenerate?: boolean
@@ -767,14 +838,35 @@
<!-- Search Button --> <!-- Search 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"
tabindex="-1"
onclick={async () => toggleSearch()} onclick={async () => toggleSearch()}
> >
<SearchIcon class="w-6 h-6" /> <SearchIcon class="w-6 h-6" />
</button> </button>
<!-- Filter Important Summary Button -->
<button
class="p-2 transition-colors {showImportantOnly
? 'text-yellow-400 hover:text-yellow-300'
: 'text-zinc-400 hover:text-zinc-200'}"
tabindex="-1"
onclick={() => {
if (searchUIState) {
searchUIState.query = "";
searchUIState.results = [];
searchUIState.currentResultIndex = -1;
}
showImportantOnly = !showImportantOnly;
}}
>
<StarIcon class="w-6 h-6" />
</button>
<!-- Settings Button --> <!-- Settings 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"
tabindex="-1"
onclick={() => { onclick={() => {
alertStore.set({ alertStore.set({
type: "none", type: "none",
@@ -791,6 +883,7 @@
<!-- Reset Button --> <!-- Reset Button -->
<button <button
class="p-2 text-zinc-400 hover:text-rose-300 transition-colors" class="p-2 text-zinc-400 hover:text-rose-300 transition-colors"
tabindex="-1"
onclick={async () => { onclick={async () => {
if ( if (
await alertConfirmTwice( await alertConfirmTwice(
@@ -815,6 +908,7 @@
<!-- Close Button --> <!-- Close 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"
tabindex="-1"
onclick={() => { onclick={() => {
alertStore.set({ alertStore.set({
type: "none", type: "none",
@@ -828,7 +922,7 @@
</div> </div>
<!-- Scrollable Container --> <!-- Scrollable Container -->
<div class="flex flex-col gap-2 sm:gap-4 overflow-y-auto"> <div class="flex flex-col gap-2 sm:gap-4 overflow-y-auto" tabindex="-1">
{#if hypaV3DataState.summaries.length === 0} {#if hypaV3DataState.summaries.length === 0}
<!-- Conversion Section --> <!-- Conversion Section -->
{#if isHypaV2ConversionPossible()} {#if isHypaV2ConversionPossible()}
@@ -841,6 +935,7 @@
</div> </div>
<button <button
class="my-1 sm:my-2 px-4 py-2 rounded-md text-zinc-300 font-semibold bg-zinc-700 hover:bg-zinc-500 transition-colors" class="my-1 sm:my-2 px-4 py-2 rounded-md text-zinc-300 font-semibold bg-zinc-700 hover:bg-zinc-500 transition-colors"
tabindex="-1"
onclick={async () => { onclick={async () => {
const conversionResult = convertHypaV2ToV3(); const conversionResult = convertHypaV2ToV3();
@@ -889,8 +984,8 @@
bind:value={searchUIState.query} bind:value={searchUIState.query}
oninput={() => { oninput={() => {
if (searchUIState) { if (searchUIState) {
searchUIState.currentIndex = -1;
searchUIState.results = []; searchUIState.results = [];
searchUIState.currentResultIndex = -1;
} }
}} }}
onkeydown={(e) => onSearch(e)} onkeydown={(e) => onSearch(e)}
@@ -901,16 +996,32 @@
<span <span
class="absolute right-3 top-1/2 -translate-y-1/2 px-1.5 sm:px-3 py-1 sm:py-2 rounded text-sm font-semibold text-zinc-100 bg-zinc-700/65" class="absolute right-3 top-1/2 -translate-y-1/2 px-1.5 sm:px-3 py-1 sm:py-2 rounded text-sm font-semibold text-zinc-100 bg-zinc-700/65"
> >
{searchUIState.currentIndex + 1}/{searchUIState.results {searchUIState.currentResultIndex + 1}/{searchUIState
.length} .results.length}
</span> </span>
{/if} {/if}
</div> </div>
<!-- Previous 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 () => toggleSearch()} tabindex="-1"
onclick={() => {
onSearch({ shiftKey: true, key: "Enter" } as KeyboardEvent);
}}
> >
<XIcon class="w-6 h-6" /> <ChevronUpIcon class="w-6 h-6" />
</button>
<!-- Next Button -->
<button
class="p-2 text-zinc-400 hover:text-zinc-200 transition-colors"
tabindex="-1"
onclick={() => {
onSearch({ key: "Enter" } as KeyboardEvent);
}}
>
<ChevronDownIcon class="w-6 h-6" />
</button> </button>
</div> </div>
</div> </div>
@@ -918,6 +1029,7 @@
<!-- Summaries List --> <!-- Summaries List -->
{#each hypaV3DataState.summaries as summary, i} {#each hypaV3DataState.summaries as summary, i}
{#if !showImportantOnly || summary.isImportant}
{#if summaryUIStates[i]} {#if summaryUIStates[i]}
<!-- Summary Item --> <!-- Summary Item -->
<div <div
@@ -936,6 +1048,7 @@
<!-- Translate Button --> <!-- Translate 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"
tabindex="-1"
use:handleDualAction={{ use:handleDualAction={{
onMainAction: () => toggleTranslate(i, false), onMainAction: () => toggleTranslate(i, false),
onAlternativeAction: () => toggleTranslate(i, true), onAlternativeAction: () => toggleTranslate(i, true),
@@ -946,9 +1059,10 @@
<!-- Important Button --> <!-- Important Button -->
<button <button
class="p-2 hover:text-zinc-200 transition-colors {summary.isImportant class="p-2 transition-colors {summary.isImportant
? 'text-yellow-400' ? 'text-yellow-400 hover:text-yellow-300'
: 'text-zinc-400'}" : 'text-zinc-400 hover:text-zinc-200'}"
tabindex="-1"
onclick={() => { onclick={() => {
summary.isImportant = !summary.isImportant; summary.isImportant = !summary.isImportant;
}} }}
@@ -959,8 +1073,9 @@
<!-- 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(i)} tabindex="-1"
disabled={!isRerollable(i)} disabled={!isRerollable(i)}
onclick={async () => await toggleReroll(i)}
> >
<RefreshCw class="w-4 h-4" /> <RefreshCw class="w-4 h-4" />
</button> </button>
@@ -968,6 +1083,7 @@
<!-- Delete After Button --> <!-- Delete After Button -->
<button <button
class="p-2 text-zinc-400 hover:text-rose-300 transition-colors" class="p-2 text-zinc-400 hover:text-rose-300 transition-colors"
tabindex="-1"
onclick={async () => { onclick={async () => {
if ( if (
await alertConfirmTwice( await alertConfirmTwice(
@@ -992,6 +1108,11 @@
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-colors text-zinc-200 bg-zinc-900"
bind:this={summaryUIStates[i].originalRef} bind:this={summaryUIStates[i].originalRef}
bind:value={summary.text} bind:value={summary.text}
onfocus={() => {
if (searchUIState && !searchUIState.isNavigating) {
searchUIState.requestedSearchFromIndex = i;
}
}}
> >
</textarea> </textarea>
</div> </div>
@@ -1004,10 +1125,10 @@
</div> </div>
<textarea <textarea
readonly
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900"
bind:this={summaryUIStates[i].translationRef} readonly
tabindex="-1" tabindex="-1"
bind:this={summaryUIStates[i].translationRef}
value={summaryUIStates[i].translation} value={summaryUIStates[i].translation}
></textarea> ></textarea>
</div> </div>
@@ -1024,8 +1145,10 @@
<!-- Translate Rerolled Button --> <!-- Translate Rerolled 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"
tabindex="-1"
use:handleDualAction={{ use:handleDualAction={{
onMainAction: () => toggleTranslateRerolled(i, false), onMainAction: () =>
toggleTranslateRerolled(i, false),
onAlternativeAction: () => onAlternativeAction: () =>
toggleTranslateRerolled(i, true), toggleTranslateRerolled(i, true),
}} }}
@@ -1036,6 +1159,7 @@
<!-- Cancel Button --> <!-- Cancel 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"
tabindex="-1"
onclick={() => { onclick={() => {
summaryUIStates[i].rerolledText = null; summaryUIStates[i].rerolledText = null;
summaryUIStates[i].rerolledTranslation = null; summaryUIStates[i].rerolledTranslation = null;
@@ -1047,6 +1171,7 @@
<!-- Apply Button --> <!-- Apply Button -->
<button <button
class="p-2 text-zinc-400 hover:text-rose-300 transition-colors" class="p-2 text-zinc-400 hover:text-rose-300 transition-colors"
tabindex="-1"
onclick={() => { onclick={() => {
summary.text = summaryUIStates[i].rerolledText!; summary.text = summaryUIStates[i].rerolledText!;
summaryUIStates[i].translation = null; summaryUIStates[i].translation = null;
@@ -1064,6 +1189,7 @@
<div class="mt-2 sm:mt-4"> <div class="mt-2 sm:mt-4">
<textarea <textarea
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-colors text-zinc-200 bg-zinc-900"
tabindex="-1"
bind:value={summaryUIStates[i].rerolledText} bind:value={summaryUIStates[i].rerolledText}
> >
</textarea> </textarea>
@@ -1077,10 +1203,10 @@
</div> </div>
<textarea <textarea
readonly
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900"
bind:this={summaryUIStates[i].rerolledTranslationRef} readonly
tabindex="-1" tabindex="-1"
bind:this={summaryUIStates[i].rerolledTranslationRef}
value={summaryUIStates[i].rerolledTranslation} value={summaryUIStates[i].rerolledTranslation}
></textarea> ></textarea>
</div> </div>
@@ -1101,6 +1227,7 @@
<!-- Translate Message Button --> <!-- Translate Message 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"
tabindex="-1"
use:handleDualAction={{ use:handleDualAction={{
onMainAction: () => onMainAction: () =>
toggleTranslateExpandedMessage(false), toggleTranslateExpandedMessage(false),
@@ -1124,6 +1251,7 @@
) )
? 'ring-2 ring-zinc-500' ? 'ring-2 ring-zinc-500'
: ''}" : ''}"
tabindex="-1"
bind:this={summaryUIStates[i].chatMemoRefs[memoIndex]} bind:this={summaryUIStates[i].chatMemoRefs[memoIndex]}
onclick={() => toggleExpandMessage(i, chatMemo)} onclick={() => toggleExpandMessage(i, chatMemo)}
> >
@@ -1150,8 +1278,9 @@
<!-- Content --> <!-- Content -->
<textarea <textarea
readonly
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900"
readonly
tabindex="-1"
value={expandedMessage.data} value={expandedMessage.data}
></textarea> ></textarea>
{:else} {:else}
@@ -1178,10 +1307,10 @@
</div> </div>
<textarea <textarea
readonly
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-vertical rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900"
bind:this={expandedMessageUIState.translationRef} readonly
tabindex="-1" tabindex="-1"
bind:this={expandedMessageUIState.translationRef}
value={expandedMessageUIState.translation} value={expandedMessageUIState.translation}
></textarea> ></textarea>
</div> </div>
@@ -1189,6 +1318,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{/if}
{/each} {/each}
<!-- Next Summarization Target --> <!-- Next Summarization Target -->
@@ -1209,8 +1339,8 @@
</div> </div>
<textarea <textarea
readonly
class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-none overflow-y-auto rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900" class="p-2 sm:p-4 w-full min-h-40 sm:min-h-56 resize-none overflow-y-auto rounded border border-zinc-700 focus:outline-none transition-colors text-zinc-200 bg-zinc-900"
readonly
value={nextMessage.data} value={nextMessage.data}
></textarea> ></textarea>
{:else} {:else}