From 9954c22f8822f5a06912247d0098d937c20ec6b5 Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Thu, 21 May 2026 12:38:06 +0300 Subject: [PATCH 01/10] feat(save-modal): chunk highlights, AI advice chat, bundle suggestion PRFAQ-001 Phase 1 (extension, CU-86exnxnd3): - Inline chunk highlights on the selection; "Ask AI" chat (explain or fix) with conversation history and editable user messages (re-runs from edit point). - Bundle suggestion: URL-matched bundle -> suggested chip (editable) -> last-used; named "Save to " button; sends chunks + sourceUrl to createPhrase. - Preview flashcard (chunk cloze) and Practice now button (-> practice-config stub). - Modal trims: drop examples + related expressions; "Phonetic" -> "Pronunciation" with per-chunk transliteration; pass pageTitle/pageUrl to translateWithContext. - dir="auto" on chat bubbles/input for mixed RTL/LTR. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/common/helper/url-normalise.ts | 30 ++ src/common/services/translate.service.ts | 40 ++- .../components/SaveWordSectionV2.vue | 239 +++++++++++-- .../modules/practice-config/index.vue | 17 + .../modules/word-detail/index.vue | 332 +++++++++++++++--- .../modules/word-detail/types.ts | 49 ++- src/console-crane/router.ts | 6 + src/console-crane/types.ts | 6 +- 8 files changed, 617 insertions(+), 102 deletions(-) create mode 100644 src/common/helper/url-normalise.ts create mode 100644 src/console-crane/modules/practice-config/index.vue diff --git a/src/common/helper/url-normalise.ts b/src/common/helper/url-normalise.ts new file mode 100644 index 0000000..a047ca4 --- /dev/null +++ b/src/common/helper/url-normalise.ts @@ -0,0 +1,30 @@ +/** + * Normalise a source page URL so saves from the same content group together. + * Mirrors the server-side normaliser (server/src/modules/translation/url-normalise.ts): + * drops query strings, fragments, and timestamps; lowercases the host; removes a + * trailing slash. Returns the input unchanged when it cannot be parsed. + */ +export function normaliseSourceUrl(raw: string): string { + if (!raw || typeof raw !== "string") return ""; + + const trimmed = raw.trim(); + + let url: URL; + try { + url = new URL(trimmed); + } catch { + return trimmed; + } + + url.hash = ""; + url.search = ""; + url.hostname = url.hostname.toLowerCase(); + + let normalised = url.toString(); + + if (normalised.endsWith("/") && url.pathname !== "/") { + normalised = normalised.slice(0, -1); + } + + return normalised; +} diff --git a/src/common/services/translate.service.ts b/src/common/services/translate.service.ts index 426fc27..31880e5 100644 --- a/src/common/services/translate.service.ts +++ b/src/common/services/translate.service.ts @@ -7,7 +7,11 @@ import { Dictionary } from "../types/general.type"; import proxy from "./proxy.service"; import { functionProvider } from "@modular-rest/client"; -import { LanguageLearningData } from "../../console-crane/modules/word-detail/types"; +import { + Chunk, + LanguageLearningData, + TranslationAdvice, +} from "../../console-crane/modules/word-detail/types"; import { LanguageDetector } from "../helper/language-detection"; import { useSettingsStore } from "../store/settings"; @@ -155,6 +159,12 @@ export class TranslateService { return cachedResult; } + // Page context drives the bundle-name suggestion on first save from a page. + const pageTitle = + typeof document !== "undefined" ? document.title : undefined; + const pageUrl = + typeof location !== "undefined" ? location.href : undefined; + // If not cached, fetch from API try { const data = await functionProvider.run({ @@ -165,12 +175,15 @@ export class TranslateService { targetLanguage: this.languageTitle, phrase: text, context: context || "", + pageTitle, + pageUrl, }, }); // Add context and phrase to the result data.context = context; data.phrase = text; + if (!Array.isArray(data.chunks)) data.chunks = []; // Cache the result this.cacheResult(cacheKey, data); @@ -183,6 +196,31 @@ export class TranslateService { } } + /** + * Conversational advisor for the save modal's "fix this?" chat. + * Returns either a plain-text reply or an updated chunks list. + */ + async fetchTranslationAdvice(params: { + phrase: string; + context: string; + message: string; + currentChunks?: Chunk[]; + history?: { role: "user" | "assistant"; text: string }[]; + }): Promise { + return functionProvider.run({ + name: "translationAdvice", + args: { + phrase: params.phrase, + context: params.context || "", + message: params.message, + currentChunks: params.currentChunks || [], + history: params.history || [], + sourceLanguage: "auto", + targetLanguage: this.languageTitle, + }, + }); + } + async translateByDictionaryapi(word: string) { let url = { url: "https://api.dictionaryapi.dev/api/v2/entries/en/" + encodeURI(word), diff --git a/src/console-crane/components/SaveWordSectionV2.vue b/src/console-crane/components/SaveWordSectionV2.vue index 4542d8f..0ccd5c3 100644 --- a/src/console-crane/components/SaveWordSectionV2.vue +++ b/src/console-crane/components/SaveWordSectionV2.vue @@ -1,18 +1,35 @@