diff --git a/src/lang/cn.ts b/src/lang/cn.ts index 6e543d76..abcd8dc2 100644 --- a/src/lang/cn.ts +++ b/src/lang/cn.ts @@ -796,7 +796,7 @@ export const languageChinese = { "convertSuccessMessage": "成功将 HypaV2 数据转换为 V3", "convertErrorMessage": "将 HypaV2 数据转换为 V3 失败:{0}", "noSummariesLabel": "尚无总结", - "searchPlaceholder": "输入 #N、ID 或搜索查询", + "searchPlaceholder": "输入 #N、ID 或搜索关键词", "summaryNumberLabel": "总结 #{0}", "deleteAfterConfirmMessage": "删除此后的所有总结?", "deleteAfterConfirmSecondMessage": "此操作无法撤销。您确定吗?", diff --git a/src/lang/en.ts b/src/lang/en.ts index 760a8dcd..ea9f3133 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -867,7 +867,7 @@ export const languageEnglish = { convertSuccessMessage: "Successfully converted HypaV2 data to V3", convertErrorMessage: "Failed to convert HypaV2 data to V3: {0}", noSummariesLabel: "No summaries yet", - searchPlaceholder: "Enter #N, ID, or search query", + searchPlaceholder: "Enter #N, ID, or query", summaryNumberLabel: "Summary #{0}", deleteAfterConfirmMessage: "Delete all summaries after this one?", deleteAfterConfirmSecondMessage: "This action cannot be undone. Are you really sure?", diff --git a/src/lang/es.ts b/src/lang/es.ts index f0c5787c..193d18a4 100644 --- a/src/lang/es.ts +++ b/src/lang/es.ts @@ -705,7 +705,7 @@ export const languageSpanish = { "convertSuccessMessage": "Datos de HypaV2 convertidos exitosamente a V3", "convertErrorMessage": "Error al convertir datos de HypaV2 a V3: {0}", "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}", "deleteAfterConfirmMessage": "¿Eliminar todos los resúmenes después de este?", "deleteAfterConfirmSecondMessage": "Esta acción no se puede deshacer. ¿Está realmente seguro?", diff --git a/src/lang/ko.ts b/src/lang/ko.ts index f409e77c..86d456e9 100644 --- a/src/lang/ko.ts +++ b/src/lang/ko.ts @@ -792,7 +792,7 @@ export const languageKorean = { "convertSuccessMessage": "HypaV2 데이터를 V3로 성공적으로 변환했습니다", "convertErrorMessage": "HypaV2 데이터를 V3로 변환하는데 실패했습니다: {0}", "noSummariesLabel": "아직 요약이 없습니다", - "searchPlaceholder": "#N, ID 또는 검색어를 입력하세요", + "searchPlaceholder": "#N, ID 또는 검색어 입력", "summaryNumberLabel": "요약 #{0}", "deleteAfterConfirmMessage": "이 요약 이후의 모든 요약을 삭제하시겠습니까?", "deleteAfterConfirmSecondMessage": "이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?", diff --git a/src/lang/vi.ts b/src/lang/vi.ts index 258c9ae1..2d8a7275 100644 --- a/src/lang/vi.ts +++ b/src/lang/vi.ts @@ -434,7 +434,7 @@ export const LanguageVietnamese = { "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}", "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}", "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?", diff --git a/src/lang/zh-Hant.ts b/src/lang/zh-Hant.ts index 3f580f64..6325845c 100644 --- a/src/lang/zh-Hant.ts +++ b/src/lang/zh-Hant.ts @@ -829,7 +829,7 @@ export const languageChineseTraditional = { "convertSuccessMessage": "成功將 HypaV2 數據轉換為 V3", "convertErrorMessage": "無法將 HypaV2 數據轉換為 V3:{0}", "noSummariesLabel": "尚無摘要", - "searchPlaceholder": "輸入 #N、ID 或搜索查詢", + "searchPlaceholder": "輸入 #N、ID 或搜尋關鍵字", "summaryNumberLabel": "摘要 #{0}", "deleteAfterConfirmMessage": "刪除此摘要之後的所有摘要?", "deleteAfterConfirmSecondMessage": "此操作無法撤銷。您確定要這樣做嗎?", diff --git a/src/lib/Others/HypaV3Modal.svelte b/src/lib/Others/HypaV3Modal.svelte index 03bbb14a..51451563 100644 --- a/src/lib/Others/HypaV3Modal.svelte +++ b/src/lib/Others/HypaV3Modal.svelte @@ -5,6 +5,8 @@ SettingsIcon, Trash2Icon, XIcon, + ChevronUpIcon, + ChevronDownIcon, LanguagesIcon, StarIcon, RefreshCw, @@ -51,20 +53,30 @@ translationRef: HTMLTextAreaElement; } - interface SearchResult { - element: HTMLElement; - matchType: "chatMemo" | "summary"; - summaryPosition?: { - start: number; - end: number; - }; + class SummarySearchResult { + constructor( + public summaryIndex: number, + public start: number, + public end: number + ) {} } + class ChatMemoSearchResult { + constructor( + public summaryIndex: number, + public memoIndex: number + ) {} + } + + type SearchResult = SummarySearchResult | ChatMemoSearchResult; + interface SearchUI { ref: HTMLInputElement; query: string; - currentIndex: number; results: SearchResult[]; + currentResultIndex: number; + requestedSearchFromIndex: number; + isNavigating: boolean; } const hypaV3DataState = $derived( @@ -76,6 +88,7 @@ let summaryUIStates = $state([]); let expandedMessageUIState = $state(null); let searchUIState = $state(null); + let showImportantOnly = $state(false); $effect.pre(() => { summaryUIStates = hypaV3DataState.summaries.map((summary) => ({ @@ -111,8 +124,10 @@ searchUIState = { ref: null, query: "", - currentIndex: -1, results: [], + currentResultIndex: -1, + requestedSearchFromIndex: -1, + isNavigating: false, }; // Focus on search element after it's rendered @@ -135,122 +150,187 @@ } if (e.key === "Enter") { - e.preventDefault(); // Prevent event default action + e.preventDefault?.(); // Prevent event default action const query = searchUIState.query.trim(); if (!query) return; - // Search summary index - if (query.match(/^#\d+$/)) { - const summaryNumber = parseInt(query.substring(1)) - 1; + // When received a new query + if (searchUIState.currentResultIndex === -1) { + const results = generateSearchResults(query); - if ( - summaryNumber >= 0 && - summaryNumber < hypaV3DataState.summaries.length - ) { - summaryUIStates[summaryNumber].originalRef.scrollIntoView({ - behavior: "instant", - block: "center", - }); - } - - return; - } - - const normalizedQuery = query.toLowerCase(); - - if (searchUIState.currentIndex === -1) { - const results: SearchResult[] = []; - - if (isGuidLike(query)) { - // Search chatMemo - summaryUIStates.forEach((summaryUI) => { - summaryUI.chatMemoRefs.forEach((buttonRef) => { - const buttonText = buttonRef.textContent?.toLowerCase() || ""; - - if (buttonText.includes(normalizedQuery)) { - results.push({ - element: buttonRef as HTMLButtonElement, - matchType: "chatMemo", - }); - } - }); - }); - } else { - // Search summary - summaryUIStates.forEach((summaryUI) => { - const textAreaText = summaryUI.originalRef.value?.toLowerCase(); - - let pos = -1; - while ( - (pos = textAreaText.indexOf(normalizedQuery, pos + 1)) !== -1 - ) { - results.push({ - element: summaryUI.originalRef as HTMLTextAreaElement, - matchType: "summary", - summaryPosition: { - start: pos, - end: pos + normalizedQuery.length, - }, - }); - } - }); - } + if (results.length === 0) return; searchUIState.results = results; } - if (searchUIState.results.length === 0) return; + const nextResult = getNextSearchResult(e.shiftKey); - // Move to next result - searchUIState.currentIndex = - (searchUIState.currentIndex + 1) % searchUIState.results.length; + if (nextResult) { + navigateToSearchResult(nextResult); + } + } + } - const result = searchUIState.results[searchUIState.currentIndex]; + function generateSearchResults(query: string): SearchResult[] { + const results: SearchResult[] = []; + const normalizedQuery = query.trim().toLowerCase(); + + // Search summary index + if (query.match(/^#\d+$/)) { + const summaryNumber = parseInt(query.substring(1)) - 1; + + if ( + summaryNumber >= 0 && + summaryNumber < hypaV3DataState.summaries.length && + (!showImportantOnly || + hypaV3DataState.summaries[summaryNumber].isImportant) + ) { + results.push(new SummarySearchResult(summaryNumber, 0, 0)); + } + + return results; + } + + if (isGuidLike(query)) { + // Search chatMemo + summaryUIStates.forEach((summaryUI, summaryIndex) => { + if ( + !showImportantOnly || + hypaV3DataState.summaries[summaryIndex].isImportant + ) { + summaryUI.chatMemoRefs.forEach((buttonRef, memoIndex) => { + const buttonText = buttonRef.textContent?.toLowerCase() || ""; + + if (buttonText.includes(normalizedQuery)) { + results.push(new ChatMemoSearchResult(summaryIndex, memoIndex)); + } + }); + } + }); + } else { + // Search summary + summaryUIStates.forEach((summaryUI, summaryIndex) => { + if ( + !showImportantOnly || + hypaV3DataState.summaries[summaryIndex].isImportant + ) { + const textAreaText = summaryUI.originalRef.value?.toLowerCase(); + let pos = -1; + + while ( + (pos = textAreaText.indexOf(normalizedQuery, pos + 1)) !== -1 + ) { + results.push( + new SummarySearchResult( + summaryIndex, + pos, + pos + normalizedQuery.length + ) + ); + } + } + }); + } + + return results; + } + + 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); + } + + 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 - result.element.scrollIntoView({ + textarea.scrollIntoView({ behavior: "instant", block: "center", }); - if (result.matchType === "chatMemo") { - // Highlight chatMemo result - result.element.classList.add("ring-2", "ring-zinc-500"); + if (result.start === result.end) { + return; + } - // Remove highlight after a short delay - window.setTimeout(() => { - result.element.classList.remove("ring-2", "ring-zinc-500"); - }, 1000); - } else { - // Handle summary text selection - const textarea = result.element as HTMLTextAreaElement; + // Scroll to query + textarea.setSelectionRange(result.start, result.end); + scrollToSelection(textarea); + // Highlight query on desktop environment + if (!("ontouchend" in window)) { // Make readonly temporarily textarea.readOnly = true; - - // Select query - textarea.setSelectionRange( - result.summaryPosition.start, - result.summaryPosition.end - ); - textarea.focus(); - scrollToSelection(textarea); - - // This only works on firefox - //textarea.scrollTop = textarea.scrollHeight; // Scroll to the bottom - //textarea.blur(); // Collapse selection - //textarea.focus(); // This scrolls the textarea - - // Highlight textarea window.setTimeout(() => { searchUIState.ref.focus(); // Restore focus to search bar textarea.readOnly = false; // Remove readonly after focus moved }, 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) { @@ -287,15 +367,6 @@ 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( summaryIndex: number, regenerate?: boolean @@ -767,14 +838,35 @@ + + + + + + @@ -918,276 +1029,295 @@ {#each hypaV3DataState.summaries as summary, i} - {#if summaryUIStates[i]} - -
- -
- {language.hypaV3Modal.summaryNumberLabel.replace( - "{0}", - (i + 1).toString() - )} - -
- - - - - - - - - - - -
-
- - -
- -
- - - {#if summaryUIStates[i].translation} -
-
- {language.hypaV3Modal.translationLabel} -
- - -
- {/if} - - {#if summaryUIStates[i].rerolledText} - -
-
- {language.hypaV3Modal.rerolledSummaryLabel} -
- - - - - - - - -
-
-
- - -
- -
- - - {#if summaryUIStates[i].rerolledTranslation} -
-
- {language.hypaV3Modal.rerolledTranslationLabel} -
- - -
- {/if} - {/if} - - -
+ {#if !showImportantOnly || summary.isImportant} + {#if summaryUIStates[i]} + +
+
{language.hypaV3Modal.connectedMessageCountLabel.replace( + >{language.hypaV3Modal.summaryNumberLabel.replace( "{0}", - summary.chatMemos.length.toString() + (i + 1).toString() )}
- + + + + + + + + + +
-
- -
- {#each summary.chatMemos as chatMemo, memoIndex} - - {/each} -
- - {#if expandedMessageUIState?.summaryIndex === i} - +
- - {#await getProcessedMessageFromChatMemo(expandedMessageUIState.selectedChatMemo) then expandedMessage} - {#if expandedMessage} - -
- {language.hypaV3Modal.connectedMessageRoleLabel.replace( - "{0}", - expandedMessage.role - )} -
- - - - {:else} - {language.hypaV3Modal - .connectedMessageNotFoundLabel} - {/if} - {:catch error} - {language.hypaV3Modal.connectedMessageLoadingError.replace( - "{0}", - error.message - )} - {/await} +
- - {#if expandedMessageUIState.translation} + + {#if summaryUIStates[i].translation}
- {language.hypaV3Modal.connectedMessageTranslationLabel} + {language.hypaV3Modal.translationLabel}
{/if} - {/if} -
+ + {#if summaryUIStates[i].rerolledText} + +
+
+ {language.hypaV3Modal.rerolledSummaryLabel} +
+ + + + + + + + +
+
+
+ + +
+ +
+ + + {#if summaryUIStates[i].rerolledTranslation} +
+
+ {language.hypaV3Modal.rerolledTranslationLabel} +
+ + +
+ {/if} + {/if} + + +
+
+ {language.hypaV3Modal.connectedMessageCountLabel.replace( + "{0}", + summary.chatMemos.length.toString() + )} + +
+ + +
+
+
+ + +
+ {#each summary.chatMemos as chatMemo, memoIndex} + + {/each} +
+ + {#if expandedMessageUIState?.summaryIndex === i} + +
+ + {#await getProcessedMessageFromChatMemo(expandedMessageUIState.selectedChatMemo) then expandedMessage} + {#if expandedMessage} + +
+ {language.hypaV3Modal.connectedMessageRoleLabel.replace( + "{0}", + expandedMessage.role + )} +
+ + + + {:else} + {language.hypaV3Modal + .connectedMessageNotFoundLabel} + {/if} + {:catch error} + {language.hypaV3Modal.connectedMessageLoadingError.replace( + "{0}", + error.message + )} + {/await} +
+ + + {#if expandedMessageUIState.translation} +
+
+ {language.hypaV3Modal.connectedMessageTranslationLabel} +
+ + +
+ {/if} + {/if} +
+ {/if} {/if} {/each} @@ -1209,8 +1339,8 @@ {:else}