From 504ecd259d85bf930e860ad15aae97ca89f79aa9 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Tue, 6 Jan 2026 19:11:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BF=BB=E8=AF=91=E7=BC=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 30 +++ prisma/schema.prisma | 44 +++- .../(features)/dictionary/SearchResult.tsx | 6 +- src/app/(features)/translator/AddToFolder.tsx | 6 +- src/app/(features)/translator/page.tsx | 207 +++++++----------- src/app/folders/FoldersClient.tsx | 2 +- src/app/folders/[folder_id]/InFolder.tsx | 6 +- src/app/folders/[folder_id]/TextPairCard.tsx | 4 +- .../[folder_id]/UpdateTextPairModal.tsx | 4 +- src/lib/browser/localStorageOperators.ts | 1 + src/lib/server/bigmodel/dictionaryActions.ts | 104 ++++----- src/lib/server/bigmodel/translatorActions.ts | 160 ++++++++++++++ src/lib/server/services/dictionaryService.ts | 34 +-- src/lib/server/services/folderService.ts | 18 +- src/lib/server/services/pairService.ts | 18 +- src/lib/server/services/translatorService.ts | 31 +++ src/lib/server/services/types.ts | 122 +++++++++++ src/lib/server/services/userService.ts | 5 +- 18 files changed, 556 insertions(+), 246 deletions(-) create mode 100644 prisma/migrations/20260106105236_create_translation_history/migration.sql create mode 100644 src/lib/server/services/translatorService.ts create mode 100644 src/lib/server/services/types.ts diff --git a/prisma/migrations/20260106105236_create_translation_history/migration.sql b/prisma/migrations/20260106105236_create_translation_history/migration.sql new file mode 100644 index 0000000..7371156 --- /dev/null +++ b/prisma/migrations/20260106105236_create_translation_history/migration.sql @@ -0,0 +1,30 @@ +-- CreateTable +CREATE TABLE "translation_history" ( + "id" SERIAL NOT NULL, + "user_id" TEXT, + "source_text" TEXT NOT NULL, + "source_language" VARCHAR(20) NOT NULL, + "target_language" VARCHAR(20) NOT NULL, + "translated_text" TEXT NOT NULL, + "source_ipa" TEXT, + "target_ipa" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "translation_history_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "translation_history_user_id_idx" ON "translation_history"("user_id"); + +-- CreateIndex +CREATE INDEX "translation_history_created_at_idx" ON "translation_history"("created_at"); + +-- CreateIndex +CREATE INDEX "translation_history_source_text_target_language_idx" ON "translation_history"("source_text", "target_language"); + +-- CreateIndex +CREATE INDEX "translation_history_translated_text_source_language_target__idx" ON "translation_history"("translated_text", "source_language", "target_language"); + +-- AddForeignKey +ALTER TABLE "translation_history" ADD CONSTRAINT "translation_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 77a719b..a04b695 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,17 +8,18 @@ datasource db { } model User { - id String @id - name String - email String - emailVerified Boolean @default(false) - image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sessions Session[] - accounts Account[] - folders Folder[] - dictionaryLookUps DictionaryLookUp[] + id String @id + name String + email String + emailVerified Boolean @default(false) + image String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sessions Session[] + accounts Account[] + folders Folder[] + dictionaryLookUps DictionaryLookUp[] + translationHistories TranslationHistory[] @@unique([email]) @@map("user") @@ -188,3 +189,24 @@ model DictionaryPhraseEntry { @@index([createdAt]) @@map("dictionary_phrase_entries") } + +model TranslationHistory { + id Int @id @default(autoincrement()) + userId String? @map("user_id") + sourceText String @map("source_text") + sourceLanguage String @map("source_language") @db.VarChar(20) + targetLanguage String @map("target_language") @db.VarChar(20) + translatedText String @map("translated_text") + sourceIpa String? @map("source_ipa") + targetIpa String? @map("target_ipa") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([userId]) + @@index([createdAt]) + @@index([sourceText, targetLanguage]) + @@index([translatedText, sourceLanguage, targetLanguage]) + @@map("translation_history") +} diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx index 37498e7..a4d81bc 100644 --- a/src/app/(features)/dictionary/SearchResult.tsx +++ b/src/app/(features)/dictionary/SearchResult.tsx @@ -85,11 +85,7 @@ export function SearchResult({ language1: queryLang, language2: definitionLang, ipa1: isDictWordResponse(searchResult) && (entry as DictWordEntry).ipa ? (entry as DictWordEntry).ipa : undefined, - folder: { - connect: { - id: selectedFolderId, - }, - }, + folderId: selectedFolderId, }) .then(() => { const folderName = folders.find(f => f.id === selectedFolderId)?.name || "Unknown"; diff --git a/src/app/(features)/translator/AddToFolder.tsx b/src/app/(features)/translator/AddToFolder.tsx index 0d25875..ab296af 100644 --- a/src/app/(features)/translator/AddToFolder.tsx +++ b/src/app/(features)/translator/AddToFolder.tsx @@ -59,11 +59,7 @@ const AddToFolder: React.FC = ({ item, setShow }) => { text2: item.text2, language1: item.language1, language2: item.language2, - folder: { - connect: { - id: folder.id, - }, - }, + folderId: folder.id, }) .then(() => { toast.success(t("success")); diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx index 687213f..34f852e 100644 --- a/src/app/(features)/translator/page.tsx +++ b/src/app/(features)/translator/page.tsx @@ -12,11 +12,8 @@ import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; import z from "zod"; import AddToFolder from "./AddToFolder"; -import { - genIPA, - genLocale, - genTranslation, -} from "@/lib/server/bigmodel/translatorActions"; +import { translateText } from "@/lib/server/bigmodel/translatorActions"; +import type { TranslateTextOutput } from "@/lib/server/services/types"; import { toast } from "sonner"; import FolderSelector from "./FolderSelector"; import { createPair } from "@/lib/server/services/pairService"; @@ -28,11 +25,14 @@ export default function TranslatorPage() { const t = useTranslations("translator"); const taref = useRef(null); - const [lang, setLang] = useState("chinese"); - const [tresult, setTresult] = useState(""); - const [genIpa, setGenIpa] = useState(true); - const [ipaTexts, setIpaTexts] = useState(["", ""]); + const [targetLanguage, setTargetLanguage] = useState("Chinese"); + const [translationResult, setTranslationResult] = useState(null); + const [needIpa, setNeedIpa] = useState(true); const [processing, setProcessing] = useState(false); + const [lastTranslation, setLastTranslation] = useState<{ + sourceText: string; + targetLanguage: string; + } | null>(null); const { load, play } = useAudioPlayer(); const [history, setHistory] = useState[]>(() => tlso.get()); const [showAddToFolder, setShowAddToFolder] = useState(false); @@ -76,108 +76,66 @@ export default function TranslatorPage() { }; const translate = async () => { - if (!taref.current) return; - if (processing) return; + if (!taref.current || processing) return; setProcessing(true); - const text1 = taref.current.value; + const sourceText = taref.current.value; - const llmres: { - text1: string | null; - text2: string | null; - language1: string | null; - language2: string | null; - ipa1: string | null; - ipa2: string | null; - } = { - text1: text1, - text2: null, - language1: null, - language2: null, - ipa1: null, - ipa2: null, - }; + // 判断是否需要强制重新翻译 + // 只有当源文本和目标语言都与上次相同时,才强制重新翻译 + const forceRetranslate = + lastTranslation?.sourceText === sourceText && + lastTranslation?.targetLanguage === targetLanguage; - let historyUpdated = false; - - // 检查更新历史记录 - const checkUpdateLocalStorage = () => { - if (historyUpdated) return; - if (llmres.text1 && llmres.text2 && llmres.language1 && llmres.language2) { - setHistory( - tlsoPush({ - text1: llmres.text1, - text2: llmres.text2, - language1: llmres.language1, - language2: llmres.language2, - }), - ); - if (autoSave && autoSaveFolderId) { - createPair({ - text1: llmres.text1, - text2: llmres.text2, - language1: llmres.language1, - language2: llmres.language2, - folder: { - connect: { - id: autoSaveFolderId, - }, - }, - }) - .then(() => { - toast.success( - llmres.text1 + "保存到文件夹" + autoSaveFolderId + "成功", - ); - }) - .catch((error) => { - toast.error( - llmres.text1 + - "保存到文件夹" + - autoSaveFolderId + - "失败:" + - error.message, - ); - }); - } - historyUpdated = true; - } - }; - // 更新局部翻译状态 - const updateState = (stateName: keyof typeof llmres, value: string) => { - llmres[stateName] = value; - checkUpdateLocalStorage(); - }; - - genTranslation(text1, lang) - .then(async (text2) => { - updateState("text2", text2); - setTresult(text2); - // 生成两个locale - genLocale(text1).then((locale) => { - updateState("language1", locale); - }); - genLocale(text2).then((locale) => { - updateState("language2", locale); - }); - // 生成俩IPA - if (genIpa) { - genIPA(text1).then((ipa1) => { - setIpaTexts((prev) => [ipa1, prev[1]]); - updateState("ipa1", ipa1); - }); - genIPA(text2).then((ipa2) => { - setIpaTexts((prev) => [prev[0], ipa2]); - updateState("ipa2", ipa2); - }); - } - }) - .catch(() => { - toast.error("Translation failed"); - }) - .finally(() => { - setProcessing(false); + try { + const result = await translateText({ + sourceText, + targetLanguage, + forceRetranslate, + needIpa, + userId: session?.user?.id, }); + + setTranslationResult(result); + setLastTranslation({ + sourceText, + targetLanguage, + }); + + // 更新本地历史记录 + const historyItem = { + text1: result.sourceText, + text2: result.translatedText, + language1: result.sourceLanguage, + language2: result.targetLanguage, + }; + setHistory(tlsoPush(historyItem)); + + // 自动保存到文件夹 + if (autoSave && autoSaveFolderId) { + createPair({ + text1: result.sourceText, + text2: result.translatedText, + language1: result.sourceLanguage, + language2: result.targetLanguage, + ipa1: result.sourceIpa || undefined, + ipa2: result.targetIpa || undefined, + folderId: autoSaveFolderId, + }) + .then(() => { + toast.success(`${sourceText} 保存到文件夹 ${autoSaveFolderId} 成功`); + }) + .catch((error) => { + toast.error(`保存失败: ${error.message}`); + }); + } + } catch (error) { + toast.error("翻译失败,请重试"); + console.error("翻译错误:", error); + } finally { + setProcessing(false); + } }; return ( @@ -196,7 +154,7 @@ export default function TranslatorPage() { }} >
- {ipaTexts[0]} + {translationResult?.sourceIpa || ""}
{ const t = taref.current?.value; if (!t) return; - tts(t, tlso.get().find((v) => v.text1 === t)?.language1 || ""); + tts(t, translationResult?.sourceLanguage || ""); }} >
@@ -222,8 +180,8 @@ export default function TranslatorPage() {
{t("detectLanguage")} setGenIpa((prev) => !prev)} + selected={needIpa} + onClick={() => setNeedIpa((prev) => !prev)} > {t("generateIPA")} @@ -234,25 +192,26 @@ export default function TranslatorPage() {
{/* ICard2 Component */}
-
{tresult}
+
{translationResult?.translatedText || ""}
- {ipaTexts[1]} + {translationResult?.targetIpa || ""}
{ - await navigator.clipboard.writeText(tresult); + await navigator.clipboard.writeText(translationResult?.translatedText || ""); }} > { + if (!translationResult) return; tts( - tresult, - tlso.get().find((v) => v.text2 === tresult)?.language2 || "", + translationResult.translatedText, + translationResult.targetLanguage, ); }} > @@ -261,29 +220,29 @@ export default function TranslatorPage() {
{t("translateInto")} setLang("chinese")} + selected={targetLanguage === "Chinese"} + onClick={() => setTargetLanguage("Chinese")} > {t("chinese")} setLang("english")} + selected={targetLanguage === "English"} + onClick={() => setTargetLanguage("English")} > {t("english")} setLang("italian")} + selected={targetLanguage === "Italian"} + onClick={() => setTargetLanguage("Italian")} > {t("italian")} { const newLang = prompt(t("enterLanguage")); if (newLang) { - setLang(newLang); + setTargetLanguage(newLang); } }} > @@ -341,6 +300,10 @@ export default function TranslatorPage() {