diff --git a/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql b/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql new file mode 100644 index 0000000..a24a98c --- /dev/null +++ b/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql @@ -0,0 +1,94 @@ +/* + Warnings: + + - You are about to drop the column `dictionary_phrase_id` on the `dictionary_lookups` table. All the data in the column will be lost. + - You are about to drop the column `dictionary_word_id` on the `dictionary_lookups` table. All the data in the column will be lost. + - You are about to drop the `dictionary_phrase_entries` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `dictionary_phrases` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `dictionary_word_entries` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `dictionary_words` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_phrase_id_fkey"; + +-- DropForeignKey +ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_word_id_fkey"; + +-- DropForeignKey +ALTER TABLE "dictionary_phrase_entries" DROP CONSTRAINT "dictionary_phrase_entries_phrase_id_fkey"; + +-- DropForeignKey +ALTER TABLE "dictionary_word_entries" DROP CONSTRAINT "dictionary_word_entries_word_id_fkey"; + +-- DropIndex +DROP INDEX "dictionary_lookups_text_query_lang_definition_lang_idx"; + +-- AlterTable +ALTER TABLE "dictionary_lookups" DROP COLUMN "dictionary_phrase_id", +DROP COLUMN "dictionary_word_id", +ADD COLUMN "dictionary_item_id" INTEGER, +ADD COLUMN "normalized_text" TEXT NOT NULL DEFAULT ''; + +-- DropTable +DROP TABLE "dictionary_phrase_entries"; + +-- DropTable +DROP TABLE "dictionary_phrases"; + +-- DropTable +DROP TABLE "dictionary_word_entries"; + +-- DropTable +DROP TABLE "dictionary_words"; + +-- CreateTable +CREATE TABLE "dictionary_items" ( + "id" SERIAL NOT NULL, + "frequency" INTEGER NOT NULL DEFAULT 1, + "standard_form" TEXT NOT NULL, + "query_lang" TEXT NOT NULL, + "definition_lang" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "dictionary_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "dictionary_entries" ( + "id" SERIAL NOT NULL, + "item_id" INTEGER NOT NULL, + "ipa" TEXT, + "definition" TEXT NOT NULL, + "part_of_speech" TEXT, + "example" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "dictionary_entries_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "dictionary_items_standard_form_idx" ON "dictionary_items"("standard_form"); + +-- CreateIndex +CREATE INDEX "dictionary_items_query_lang_definition_lang_idx" ON "dictionary_items"("query_lang", "definition_lang"); + +-- CreateIndex +CREATE UNIQUE INDEX "dictionary_items_standard_form_query_lang_definition_lang_key" ON "dictionary_items"("standard_form", "query_lang", "definition_lang"); + +-- CreateIndex +CREATE INDEX "dictionary_entries_item_id_idx" ON "dictionary_entries"("item_id"); + +-- CreateIndex +CREATE INDEX "dictionary_entries_created_at_idx" ON "dictionary_entries"("created_at"); + +-- CreateIndex +CREATE INDEX "dictionary_lookups_normalized_text_idx" ON "dictionary_lookups"("normalized_text"); + +-- AddForeignKey +ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_item_id_fkey" FOREIGN KEY ("dictionary_item_id") REFERENCES "dictionary_items"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "dictionary_entries" ADD CONSTRAINT "dictionary_entries_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "dictionary_items"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c402a6e..0eb2714 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -107,27 +107,27 @@ model Folder { } model DictionaryLookUp { - id Int @id @default(autoincrement()) - userId String? @map("user_id") - text String - queryLang String @map("query_lang") - definitionLang String @map("definition_lang") - createdAt DateTime @default(now()) @map("created_at") - dictionaryWordId Int? @map("dictionary_word_id") - dictionaryPhraseId Int? @map("dictionary_phrase_id") + id Int @id @default(autoincrement()) + userId String? @map("user_id") + text String + normalizedText String @default("") @map("normalized_text") + queryLang String @map("query_lang") + definitionLang String @map("definition_lang") + createdAt DateTime @default(now()) @map("created_at") + dictionaryItemId Int? @map("dictionary_item_id") - user User? @relation(fields: [userId], references: [id]) - dictionaryWord DictionaryWord? @relation(fields: [dictionaryWordId], references: [id], onDelete: SetNull) - dictionaryPhrase DictionaryPhrase? @relation(fields: [dictionaryPhraseId], references: [id], onDelete: SetNull) + user User? @relation(fields: [userId], references: [id]) + dictionaryItem DictionaryItem? @relation(fields: [dictionaryItemId], references: [id], onDelete: SetNull) @@index([userId]) @@index([createdAt]) - @@index([text, queryLang, definitionLang]) + @@index([normalizedText]) @@map("dictionary_lookups") } -model DictionaryWord { +model DictionaryItem { id Int @id @default(autoincrement()) + frequency Int @default(1) standardForm String @map("standard_form") queryLang String @map("query_lang") definitionLang String @map("definition_lang") @@ -135,59 +135,29 @@ model DictionaryWord { updatedAt DateTime @updatedAt @map("updated_at") lookups DictionaryLookUp[] - entries DictionaryWordEntry[] + entries DictionaryEntry[] + @@unique([standardForm, queryLang, definitionLang]) @@index([standardForm]) @@index([queryLang, definitionLang]) - @@map("dictionary_words") + @@map("dictionary_items") } -model DictionaryPhrase { - id Int @id @default(autoincrement()) - standardForm String @map("standard_form") - queryLang String @map("query_lang") - definitionLang String @map("definition_lang") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - lookups DictionaryLookUp[] - entries DictionaryPhraseEntry[] - - @@index([standardForm]) - @@index([queryLang, definitionLang]) - @@map("dictionary_phrases") -} - -model DictionaryWordEntry { +model DictionaryEntry { id Int @id @default(autoincrement()) - wordId Int @map("word_id") - ipa String + itemId Int @map("item_id") + ipa String? definition String - partOfSpeech String @map("part_of_speech") + partOfSpeech String? @map("part_of_speech") example String createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - word DictionaryWord @relation(fields: [wordId], references: [id], onDelete: Cascade) + item DictionaryItem @relation(fields: [itemId], references: [id], onDelete: Cascade) - @@index([wordId]) + @@index([itemId]) @@index([createdAt]) - @@map("dictionary_word_entries") -} - -model DictionaryPhraseEntry { - id Int @id @default(autoincrement()) - phraseId Int @map("phrase_id") - definition String - example String - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - phrase DictionaryPhrase @relation(fields: [phraseId], references: [id], onDelete: Cascade) - - @@index([phraseId]) - @@index([createdAt]) - @@map("dictionary_phrase_entries") + @@map("dictionary_entries") } model TranslationHistory { diff --git a/src/app/(features)/dictionary/DictionaryEntry.tsx b/src/app/(features)/dictionary/DictionaryEntry.tsx index a76f3b5..5d09954 100644 --- a/src/app/(features)/dictionary/DictionaryEntry.tsx +++ b/src/app/(features)/dictionary/DictionaryEntry.tsx @@ -1,75 +1,42 @@ -import { DictWordEntry, DictPhraseEntry } from "./types"; +import { TSharedEntry } from "@/shared"; interface DictionaryEntryProps { - entry: DictWordEntry | DictPhraseEntry; + entry: TSharedEntry; } export function DictionaryEntry({ entry }: DictionaryEntryProps) { - // 检查是否有 ipa 字段来判断是否为单词条目 - const isWordEntry = "ipa" in entry && "partOfSpeech" in entry; - - if (isWordEntry) { - // 单词条目 - const wordEntry = entry as DictWordEntry; - return ( -
- {/* 音标和词性 */} -
- {wordEntry.ipa && ( - - [{wordEntry.ipa}] - - )} - {wordEntry.partOfSpeech && ( - - {wordEntry.partOfSpeech} - - )} -
- - {/* 释义 */} -
-

- 释义 -

-

{wordEntry.definition}

-
- - {/* 例句 */} - {wordEntry.example && ( -
-

- 例句 -

-

- {wordEntry.example} -

-
- )} -
- ); - } - - // 短语条目 - const phraseEntry = entry as DictPhraseEntry; return (
+ {/* 音标和词性 */} +
+ {entry.ipa && ( + + [{entry.ipa}] + + )} + {entry.partOfSpeech && ( + + {entry.partOfSpeech} + + )} +
+ {/* 释义 */}

释义

-

{phraseEntry.definition}

+

{entry.definition}

{/* 例句 */} - {phraseEntry.example && ( + {entry.example && (

例句

- {phraseEntry.example} + {entry.example}

)} diff --git a/src/app/(features)/dictionary/DictionaryPage.tsx b/src/app/(features)/dictionary/DictionaryPage.tsx deleted file mode 100644 index 3dfbe1e..0000000 --- a/src/app/(features)/dictionary/DictionaryPage.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import Container from "@/components/ui/Container"; -import { authClient } from "@/lib/auth-client"; -import { Folder } from "../../../../generated/prisma/browser"; -import { getFoldersByUserId } from "@/lib/server/services/folderService"; -import { DictLookUpResponse } from "./types"; -import { SearchForm } from "./SearchForm"; -import { SearchResult } from "./SearchResult"; -import { useTranslations } from "next-intl"; -import { POPULAR_LANGUAGES } from "./constants"; -import { performDictionaryLookup } from "./utils"; - -export default function Dictionary() { - const t = useTranslations("dictionary"); - const [searchQuery, setSearchQuery] = useState(""); - const [searchResult, setSearchResult] = useState(null); - const [isSearching, setIsSearching] = useState(false); - const [hasSearched, setHasSearched] = useState(false); - const [queryLang, setQueryLang] = useState("english"); - const [definitionLang, setDefinitionLang] = useState("chinese"); - const [selectedFolderId, setSelectedFolderId] = useState(null); - const [folders, setFolders] = useState([]); - const { data: session } = authClient.useSession(); - - // 加载用户的文件夹列表 - useEffect(() => { - if (session) { - getFoldersByUserId(session.user.id as string) - .then((loadedFolders) => { - setFolders(loadedFolders); - // 如果有文件夹且未选择,默认选择第一个 - if (loadedFolders.length > 0 && !selectedFolderId) { - setSelectedFolderId(loadedFolders[0].id); - } - }); - } - }, [session, selectedFolderId]); - - // 将 code 转换为 nativeName - const getNativeName = (code: string) => { - return POPULAR_LANGUAGES.find(l => l.code === code)?.nativeName || code; - }; - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - if (!searchQuery.trim()) return; - - setIsSearching(true); - setHasSearched(true); - setSearchResult(null); - - const result = await performDictionaryLookup( - { - text: searchQuery, - queryLang: getNativeName(queryLang), - definitionLang: getNativeName(definitionLang) - }, - t - ); - - if (result.success && result.data) { - setSearchResult(result.data); - } else { - setSearchResult(null); - } - - setIsSearching(false); - }; - - return ( -
- {/* 搜索区域 */} -
- - - -
- - {/* 搜索结果区域 */} -
- - {isSearching && ( -
-
-

{t("loading")}

-
- )} - - {!isSearching && hasSearched && !searchResult && ( -
-

{t("noResults")}

-

{t("tryOtherWords")}

-
- )} - - {!isSearching && searchResult && ( - - )} - - {!hasSearched && ( -
-
📚
-

{t("welcomeTitle")}

-

{t("welcomeHint")}

-
- )} -
-
-
- ); -} diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx index f445774..cf351a8 100644 --- a/src/app/(features)/dictionary/SearchResult.tsx +++ b/src/app/(features)/dictionary/SearchResult.tsx @@ -2,26 +2,20 @@ import { Plus, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; import { Folder } from "../../../../generated/prisma/browser"; -import { createPair } from "@/lib/server/services/pairService"; -import { - DictWordResponse, - DictPhraseResponse, - isDictWordResponse, - DictWordEntry, -} from "./types"; import { DictionaryEntry } from "./DictionaryEntry"; import { useTranslations } from "next-intl"; import { performDictionaryLookup } from "./utils"; +import { TSharedItem } from "@/shared"; interface SearchResultProps { - searchResult: DictWordResponse | DictPhraseResponse; + searchResult: TSharedItem; searchQuery: string; queryLang: string; definitionLang: string; folders: Folder[]; selectedFolderId: number | null; onFolderSelect: (folderId: number | null) => void; - onResultUpdate: (newResult: DictWordResponse | DictPhraseResponse) => void; + onResultUpdate: (newResult: TSharedItem) => void; onSearchingChange: (isSearching: boolean) => void; getNativeName: (code: string) => string; } @@ -54,8 +48,8 @@ export function SearchResult({ t ); - if (result.success && result.data) { - onResultUpdate(result.data); + if (result) { + onResultUpdate(result); } onSearchingChange(false); diff --git a/src/app/(features)/dictionary/index.ts b/src/app/(features)/dictionary/index.ts deleted file mode 100644 index 8cb51b8..0000000 --- a/src/app/(features)/dictionary/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 类型定义 -export * from "./types"; - -// 常量 -export * from "./constants"; - -// 组件 -export { default as DictionaryPage } from "./DictionaryPage"; -export { SearchForm } from "./SearchForm"; -export { SearchResult } from "./SearchResult"; -export { DictionaryEntry } from "./DictionaryEntry"; diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index dd8e42b..1a9c1cd 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -1 +1,127 @@ -export { default } from "./DictionaryPage"; +"use client"; + +import { useState, useEffect } from "react"; +import Container from "@/components/ui/Container"; +import { authClient } from "@/lib/auth-client"; +import { Folder } from "../../../../generated/prisma/browser"; +import { SearchForm } from "./SearchForm"; +import { SearchResult } from "./SearchResult"; +import { useTranslations } from "next-intl"; +import { POPULAR_LANGUAGES } from "./constants"; +import { performDictionaryLookup } from "./utils"; +import { TSharedItem } from "@/shared"; + +export default function DictionaryPage() { + const t = useTranslations("dictionary"); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResult, setSearchResult] = useState(null); + const [isSearching, setIsSearching] = useState(false); + const [hasSearched, setHasSearched] = useState(false); + const [queryLang, setQueryLang] = useState("english"); + const [definitionLang, setDefinitionLang] = useState("chinese"); + const [selectedFolderId, setSelectedFolderId] = useState(null); + const [folders, setFolders] = useState([]); + const { data: session } = authClient.useSession(); + + // 加载用户的文件夹列表 + useEffect(() => { + if (session) { + getFoldersByUserId(session.user.id as string) + .then((loadedFolders) => { + setFolders(loadedFolders); + // 如果有文件夹且未选择,默认选择第一个 + if (loadedFolders.length > 0 && !selectedFolderId) { + setSelectedFolderId(loadedFolders[0].id); + } + }); + } + }, [session, selectedFolderId]); + + // 将 code 转换为 nativeName + const getNativeName = (code: string) => { + return POPULAR_LANGUAGES.find(l => l.code === code)?.nativeName || code; + }; + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + + setIsSearching(true); + setHasSearched(true); + setSearchResult(null); + + const result = await performDictionaryLookup( + { + text: searchQuery, + queryLang: getNativeName(queryLang), + definitionLang: getNativeName(definitionLang), + forceRelook: false + }, + t + ); + setSearchResult(result); + setIsSearching(false); + }; + + return ( +
+ {/* 搜索区域 */} +
+ + + +
+ + {/* 搜索结果区域 */} +
+ + {isSearching && ( +
+
+

{t("loading")}

+
+ )} + + {!isSearching && hasSearched && !searchResult && ( +
+

{t("noResults")}

+

{t("tryOtherWords")}

+
+ )} + + {!isSearching && searchResult && ( + + )} + + {!hasSearched && ( +
+
📚
+

{t("welcomeTitle")}

+

{t("welcomeHint")}

+
+ )} +
+
+
+ ); +} diff --git a/src/app/(features)/dictionary/types.ts b/src/app/(features)/dictionary/types.ts deleted file mode 100644 index 5df1500..0000000 --- a/src/app/(features)/dictionary/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -// 从 shared 文件夹导出所有词典类型和类型守卫 -export * from "@/lib/shared"; diff --git a/src/app/(features)/dictionary/utils.ts b/src/app/(features)/dictionary/utils.ts index f7893af..857f77c 100644 --- a/src/app/(features)/dictionary/utils.ts +++ b/src/app/(features)/dictionary/utils.ts @@ -1,51 +1,25 @@ import { toast } from "sonner"; -import { lookUp } from "@/lib/server/bigmodel/dictionaryActions"; -import { - DictWordResponse, - DictPhraseResponse, -} from "./types"; +import { lookUpDictionaryAction } from "@/modules/dictionary/dictionary-action"; +import { DictionaryActionInputDto, DictionaryActionOutputDto } from "@/modules/dictionary"; +import { TSharedItem } from "@/shared"; -interface LookupOptions { - text: string; - queryLang: string; - definitionLang: string; - forceRelook?: boolean; -} - -interface LookupResult { - success: boolean; - data?: DictWordResponse | DictPhraseResponse; - error?: string; -} - -/** - * 执行词典查询的通用函数 - * @param options - 查询选项 - * @param t - 翻译函数 - * @returns 查询结果 - */ export async function performDictionaryLookup( - options: LookupOptions, + options: DictionaryActionInputDto, t?: (key: string) => string -): Promise { - const { text, queryLang, definitionLang, forceRelook = false } = options; +): Promise { + const { text, queryLang, definitionLang, forceRelook = false, userId } = options; + const result = await lookUpDictionaryAction({ + text, + queryLang, + definitionLang, + forceRelook, + userId + }); - try { - const result = await lookUp({ - text, - queryLang, - definitionLang, - forceRelook - }); + if (!result.success || !result.data) return null; - // 成功时显示提示(仅强制重新查询时) - if (forceRelook && t) { - toast.success(t("relookupSuccess")); - } - - return { success: true, data: result }; - } catch (error) { - toast.error(String(error)); - return { success: false, error: String(error) }; + if (forceRelook && t) { + toast.success(t("relookupSuccess")); } + return result.data; } diff --git a/src/app/(features)/memorize/Memorize.tsx b/src/app/(features)/memorize/Memorize.tsx index d9522cb..9c7c222 100644 --- a/src/app/(features)/memorize/Memorize.tsx +++ b/src/app/(features)/memorize/Memorize.tsx @@ -2,10 +2,10 @@ import { useState } from "react"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; -import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts"; +import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; import { useTranslations } from "next-intl"; import localFont from "next/font/local"; -import { isNonNegativeInteger, SeededRandom } from "@/lib/utils"; +import { isNonNegativeInteger, SeededRandom } from "@/utils/random"; import { Pair } from "../../../../generated/prisma/browser"; const myFont = localFont({ diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx index a2d4707..339dc5e 100644 --- a/src/app/(features)/memorize/page.tsx +++ b/src/app/(features)/memorize/page.tsx @@ -3,7 +3,7 @@ import { getTranslations } from "next-intl/server"; import { getFoldersWithTotalPairsByUserId, } from "@/lib/server/services/folderService"; -import { isNonNegativeInteger } from "@/lib/utils"; +import { isNonNegativeInteger } from "@/utils/random"; import FolderSelector from "./FolderSelector"; import Memorize from "./Memorize"; import { getPairsByFolderId } from "@/lib/server/services/pairService"; diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index 6758e8f..bd3e539 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -14,10 +14,10 @@ import SaveList from "./SaveList"; import { useTranslations } from "next-intl"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; -import { genIPA, genLanguage } from "@/lib/server/bigmodel/translatorActions"; +import { genIPA, genLanguage } from "@/modules/translator/translator-action"; import { logger } from "@/lib/logger"; import PageLayout from "@/components/ui/PageLayout"; -import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts"; +import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; export default function TextSpeakerPage() { const t = useTranslations("text_speaker"); diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx index 34f852e..c4443b6 100644 --- a/src/app/(features)/translator/page.tsx +++ b/src/app/(features)/translator/page.tsx @@ -12,14 +12,14 @@ import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; import z from "zod"; import AddToFolder from "./AddToFolder"; -import { translateText } from "@/lib/server/bigmodel/translatorActions"; +import { translateText } from "@/modules/translator/translator-action"; import type { TranslateTextOutput } from "@/lib/server/services/types"; import { toast } from "sonner"; import FolderSelector from "./FolderSelector"; import { createPair } from "@/lib/server/services/pairService"; -import { shallowEqual } from "@/lib/utils"; +import { shallowEqual } from "@/utils/random"; import { authClient } from "@/lib/auth-client"; -import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts"; +import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; export default function TranslatorPage() { const t = useTranslations("translator"); diff --git a/src/app/auth/AuthForm.tsx b/src/app/auth/AuthForm.tsx index 0e7f2a7..00a9706 100644 --- a/src/app/auth/AuthForm.tsx +++ b/src/app/auth/AuthForm.tsx @@ -2,7 +2,7 @@ import { useState, useActionState, startTransition } from "react"; import { useTranslations } from "next-intl"; -import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth"; +import { signInAction, signUpAction, SignUpState } from "@/modules/user/user-action"; import Container from "@/components/ui/Container"; import Input from "@/components/ui/Input"; import { LightButton } from "@/components/ui/buttons"; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 267f47a..e11b8fc 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -87,7 +87,7 @@ export default function Button({ const selectedClass = variant === "secondary" && selected ? "bg-gray-100" : ""; // Background color for primary variant - const backgroundColor = variant === "primary" ? COLORS.primary : undefined; + const backgroundColor = variant === "primary" ? '#35786f' : undefined; // Combine all classes const combinedClasses = ` diff --git a/src/lib/server/bigmodel/dictionary/README.md b/src/lib/bigmodel/dictionary/README.md similarity index 100% rename from src/lib/server/bigmodel/dictionary/README.md rename to src/lib/bigmodel/dictionary/README.md diff --git a/src/lib/server/bigmodel/dictionary/index.ts b/src/lib/bigmodel/dictionary/index.ts similarity index 100% rename from src/lib/server/bigmodel/dictionary/index.ts rename to src/lib/bigmodel/dictionary/index.ts diff --git a/src/lib/server/bigmodel/dictionary/orchestrator.ts b/src/lib/bigmodel/dictionary/orchestrator.ts similarity index 88% rename from src/lib/server/bigmodel/dictionary/orchestrator.ts rename to src/lib/bigmodel/dictionary/orchestrator.ts index c2ecff5..b720787 100644 --- a/src/lib/server/bigmodel/dictionary/orchestrator.ts +++ b/src/lib/bigmodel/dictionary/orchestrator.ts @@ -1,21 +1,15 @@ -import { DictLookUpResponse } from "@/lib/shared"; - +import { LookUpServiceOutputDto } from "@/modules/dictionary/dictionary-service-dto"; import { analyzeInput } from "./stage1-inputAnalysis"; import { determineSemanticMapping } from "./stage2-semanticMapping"; import { generateStandardForm } from "./stage3-standardForm"; import { generateEntries } from "./stage4-entriesGeneration"; +import { LookUpError } from "@/lib/errors"; -/** - * 词典查询主编排器 - * - * 将多个独立的 LLM 调用串联起来,每个阶段都有代码层面的数据验证 - * 只要有一环失败,直接返回错误 - */ export async function executeDictionaryLookup( text: string, queryLang: string, definitionLang: string -): Promise { +): Promise { try { // ========== 阶段 1:输入分析 ========== console.log("[阶段1] 开始输入分析..."); @@ -80,7 +74,7 @@ export async function executeDictionaryLookup( console.log("[阶段4] 词条生成完成:", entriesResult); // ========== 组装最终结果 ========== - const finalResult: DictLookUpResponse = { + const finalResult: LookUpServiceOutputDto = { standardForm: standardFormResult.standardForm, entries: entriesResult.entries, }; @@ -91,8 +85,7 @@ export async function executeDictionaryLookup( } catch (error) { console.error("[错误] 词典查询失败:", error); - // 任何阶段失败都返回错误(包含 reason) const errorMessage = error instanceof Error ? error.message : "未知错误"; - throw errorMessage; + throw new LookUpError(errorMessage); } } diff --git a/src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts similarity index 97% rename from src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts rename to src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts index eb6b183..45c677f 100644 --- a/src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts +++ b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts @@ -1,5 +1,5 @@ import { getAnswer } from "../zhipu"; -import { parseAIGeneratedJSON } from "@/lib/utils"; +import { parseAIGeneratedJSON } from "@/utils/json"; import { InputAnalysisResult } from "./types"; /** diff --git a/src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts similarity index 98% rename from src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts rename to src/lib/bigmodel/dictionary/stage2-semanticMapping.ts index 353811e..cd99fa1 100644 --- a/src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts +++ b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts @@ -1,5 +1,5 @@ import { getAnswer } from "../zhipu"; -import { parseAIGeneratedJSON } from "@/lib/utils"; +import { parseAIGeneratedJSON } from "@/utils/json"; import { SemanticMappingResult } from "./types"; /** diff --git a/src/lib/server/bigmodel/dictionary/stage3-standardForm.ts b/src/lib/bigmodel/dictionary/stage3-standardForm.ts similarity index 98% rename from src/lib/server/bigmodel/dictionary/stage3-standardForm.ts rename to src/lib/bigmodel/dictionary/stage3-standardForm.ts index 0da253d..0e9162e 100644 --- a/src/lib/server/bigmodel/dictionary/stage3-standardForm.ts +++ b/src/lib/bigmodel/dictionary/stage3-standardForm.ts @@ -1,5 +1,5 @@ import { getAnswer } from "../zhipu"; -import { parseAIGeneratedJSON } from "@/lib/utils"; +import { parseAIGeneratedJSON } from "@/utils/json"; import { StandardFormResult } from "./types"; /** diff --git a/src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts similarity index 98% rename from src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts rename to src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts index 9285b45..a2f269d 100644 --- a/src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts +++ b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts @@ -1,5 +1,5 @@ import { getAnswer } from "../zhipu"; -import { parseAIGeneratedJSON } from "@/lib/utils"; +import { parseAIGeneratedJSON } from "@/utils/json"; import { EntriesGenerationResult } from "./types"; /** diff --git a/src/lib/server/bigmodel/dictionary/types.ts b/src/lib/bigmodel/dictionary/types.ts similarity index 100% rename from src/lib/server/bigmodel/dictionary/types.ts rename to src/lib/bigmodel/dictionary/types.ts diff --git a/src/lib/server/bigmodel/tts.ts b/src/lib/bigmodel/tts.ts similarity index 100% rename from src/lib/server/bigmodel/tts.ts rename to src/lib/bigmodel/tts.ts diff --git a/src/lib/server/bigmodel/zhipu.ts b/src/lib/bigmodel/zhipu.ts similarity index 100% rename from src/lib/server/bigmodel/zhipu.ts rename to src/lib/bigmodel/zhipu.ts diff --git a/src/lib/browser/localStorageOperators.ts b/src/lib/browser/localStorageOperators.ts deleted file mode 100644 index f55daaf..0000000 --- a/src/lib/browser/localStorageOperators.ts +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { - TranslationHistoryArraySchema, - TranslationHistorySchema, -} from "@/lib/interfaces"; -import z from "zod"; -import { shallowEqual } from "../utils"; -import { logger } from "@/lib/logger"; - -export const getLocalStorageOperator = ( - key: string, - schema: T, -) => { - return { - get: (): z.infer => { - try { - if (!globalThis.localStorage) return [] as z.infer; - const item = globalThis.localStorage.getItem(key); - - if (!item) return [] as z.infer; - - const rawData = JSON.parse(item) as z.infer; - const result = schema.safeParse(rawData); - - if (result.success) { - return result.data; - } else { - logger.error( - "Invalid data structure in localStorage:", - result.error, - ); - return [] as z.infer; - } - } catch (e) { - logger.error(`Failed to parse ${key} data:`, e); - return [] as z.infer; - } - }, - set: (data: z.infer) => { - if (!globalThis.localStorage) return; - globalThis.localStorage.setItem(key, JSON.stringify(data)); - return data; - }, - }; -}; - - -const MAX_HISTORY_LENGTH = 50; -export const tlso = getLocalStorageOperator< - typeof TranslationHistoryArraySchema ->("translator", TranslationHistoryArraySchema); - -export const tlsoPush = (item: z.infer) => { - const oldHistory = tlso.get(); - if (oldHistory.some((v) => shallowEqual(v, item))) return oldHistory; - - const newHistory = [...oldHistory, item].slice(-MAX_HISTORY_LENGTH); - tlso.set(newHistory); - - return newHistory; -}; diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..283f085 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,2 @@ +export class ValidateError extends Error { }; +export class LookUpError extends Error { }; \ No newline at end of file diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts deleted file mode 100644 index 7c502a7..0000000 --- a/src/lib/interfaces.ts +++ /dev/null @@ -1,56 +0,0 @@ -import z from "zod"; - -export interface Word { - word: string; - x: number; - y: number; -} -export interface Letter { - letter: string; - letter_name_ipa: string; - letter_sound_ipa: string; - roman_letter?: string; -} -export type SupportedAlphabets = - | "japanese" - | "english" - | "esperanto" - | "uyghur"; -export const TextSpeakerItemSchema = z.object({ - text: z.string(), - ipa: z.string().optional(), - language: z.string(), -}); -export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema); - -export const WordDataSchema = z.object({ - languages: z - .tuple([z.string(), z.string()]) - .refine(([first, second]) => first !== second, { - message: "Languages must be different", - }), - wordPairs: z - .array(z.tuple([z.string(), z.string()])) - .min(1, "At least one word pair is required") - .refine( - (pairs) => { - return pairs.every( - ([first, second]) => first.trim() !== "" && second.trim() !== "", - ); - }, - { - message: "Word pairs cannot contain empty strings", - }, - ), -}); - -export const TranslationHistorySchema = z.object({ - text1: z.string(), - text2: z.string(), - language1: z.string(), - language2: z.string(), -}); - -export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema); - -export type WordData = z.infer; diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index 70b6dfc..0000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 统一的日志工具 - * 在生产环境中可以通过环境变量控制日志级别 - */ - -type LogLevel = 'info' | 'warn' | 'error'; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -export const logger = { - error: (message: string, error?: unknown) => { - if (isDevelopment) { - console.error(message, error); - } - // 在生产环境中,这里可以发送到错误追踪服务(如 Sentry) - }, - - warn: (message: string, data?: unknown) => { - if (isDevelopment) { - console.warn(message, data); - } - }, - - info: (message: string, data?: unknown) => { - if (isDevelopment) { - console.info(message, data); - } - }, -}; diff --git a/src/lib/server/bigmodel/dictionaryActions.ts b/src/lib/server/bigmodel/dictionaryActions.ts deleted file mode 100644 index 40014d9..0000000 --- a/src/lib/server/bigmodel/dictionaryActions.ts +++ /dev/null @@ -1,140 +0,0 @@ -"use server"; - -import { executeDictionaryLookup } from "./dictionary"; -import { createLookUp, createPhrase, createWord, createPhraseEntry, createWordEntry, selectLastLookUp } from "../services/dictionaryService"; -import { DictLookUpRequest, DictWordResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared"; -import { lookUpValidation } from "@/lib/shared/validations/dictionaryValidations"; - -const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => { - if (isDictPhraseResponse(res)) { - // 先创建 Phrase - const phrase = await createPhrase({ - standardForm: res.standardForm, - queryLang: req.queryLang, - definitionLang: req.definitionLang, - }); - - // 创建 Lookup - await createLookUp({ - userId: req.userId, - text: req.text, - queryLang: req.queryLang, - definitionLang: req.definitionLang, - dictionaryPhraseId: phrase.id, - }); - - // 创建 Entries - for (const entry of res.entries) { - await createPhraseEntry({ - phraseId: phrase.id, - definition: entry.definition, - example: entry.example, - }); - } - } else if (isDictWordResponse(res)) { - // 先创建 Word - const word = await createWord({ - standardForm: (res as DictWordResponse).standardForm, - queryLang: req.queryLang, - definitionLang: req.definitionLang, - }); - - // 创建 Lookup - await createLookUp({ - userId: req.userId, - text: req.text, - queryLang: req.queryLang, - definitionLang: req.definitionLang, - dictionaryWordId: word.id, - }); - - // 创建 Entries - for (const entry of (res as DictWordResponse).entries) { - await createWordEntry({ - wordId: word.id, - ipa: entry.ipa, - definition: entry.definition, - partOfSpeech: entry.partOfSpeech, - example: entry.example, - }); - } - } -}; - -/** - * 查询单词或短语 - * - * 使用模块化的词典查询系统,将提示词拆分为6个阶段: - * - 阶段0:基础系统提示 - * - 阶段1:输入解析与语言识别 - * - 阶段2:跨语言语义映射决策 - * - 阶段3:standardForm 生成与规范化 - * - 阶段4:释义与词条生成 - * - 阶段5:错误处理 - * - 阶段6:最终输出封装 - */ -export const lookUp = async (req: DictLookUpRequest): Promise => { - const { - text, - queryLang, - forceRelook = false, - definitionLang, - userId - } = req; - - lookUpValidation(req); - - const lastLookUp = await selectLastLookUp({ - text, - queryLang, - definitionLang - }); - - if (forceRelook || !lastLookUp) { - // 使用新的模块化查询系统 - const response = await executeDictionaryLookup( - text, - queryLang, - definitionLang - ); - - saveResult({ - text, - queryLang, - definitionLang, - userId, - forceRelook - }, response); - - return response; - } else { - // 从数据库返回缓存的结果 - if (lastLookUp.dictionaryWordId) { - createLookUp({ - userId: userId, - text: text, - queryLang: queryLang, - definitionLang: definitionLang, - dictionaryWordId: lastLookUp.dictionaryWordId, - }); - return { - standardForm: lastLookUp.dictionaryWord!.standardForm, - entries: lastLookUp.dictionaryWord!.entries - }; - } else if (lastLookUp.dictionaryPhraseId) { - createLookUp({ - userId: userId, - text: text, - queryLang: queryLang, - definitionLang: definitionLang, - dictionaryPhraseId: lastLookUp.dictionaryPhraseId - }); - return { - standardForm: lastLookUp.dictionaryPhrase!.standardForm, - entries: lastLookUp.dictionaryPhrase!.entries - }; - } else { - throw "错误D101"; - } - } -}; diff --git a/src/lib/server/services/dictionaryService.ts b/src/lib/server/services/dictionaryService.ts deleted file mode 100644 index 7a157b8..0000000 --- a/src/lib/server/services/dictionaryService.ts +++ /dev/null @@ -1,62 +0,0 @@ -"use server"; - -import { - CreateDictionaryLookUpInput, - DictionaryLookUpQuery, - CreateDictionaryPhraseInput, - CreateDictionaryPhraseEntryInput, - CreateDictionaryWordInput, - CreateDictionaryWordEntryInput -} from "./types"; -import prisma from "../../db"; - -export async function selectLastLookUp(content: DictionaryLookUpQuery) { - return prisma.dictionaryLookUp.findFirst({ - where: content, - include: { - dictionaryPhrase: { - include: { - entries: true - } - }, - dictionaryWord: { - include: { - entries: true - } - } - }, - orderBy: { - createdAt: 'desc' - } - }); -} - -export async function createPhraseEntry(content: CreateDictionaryPhraseEntryInput) { - return prisma.dictionaryPhraseEntry.create({ - data: content - }); -} - -export async function createWordEntry(content: CreateDictionaryWordEntryInput) { - return prisma.dictionaryWordEntry.create({ - data: content - }); -} - -export async function createPhrase(content: CreateDictionaryPhraseInput) { - return prisma.dictionaryPhrase.create({ - data: content - }); -} - -export async function createWord(content: CreateDictionaryWordInput) { - return prisma.dictionaryWord.create({ - data: content - }); -} - -export async function createLookUp(content: CreateDictionaryLookUpInput) { - return prisma.dictionaryLookUp.create({ - data: content - }); -} diff --git a/src/lib/server/services/pairService.ts b/src/lib/server/services/pairService.ts deleted file mode 100644 index c909288..0000000 --- a/src/lib/server/services/pairService.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import { CreatePairInput, UpdatePairInput } from "./types"; -import prisma from "../../db"; - -export async function createPair(data: CreatePairInput) { - return prisma.pair.create({ - data: data, - }); -} - -export async function deletePairById(id: number) { - return prisma.pair.delete({ - where: { - id: id, - }, - }); -} - -export async function updatePairById( - id: number, - data: UpdatePairInput, -) { - return prisma.pair.update({ - where: { - id: id, - }, - data: data, - }); -} - -export async function getPairCountByFolderId(folderId: number) { - return prisma.pair.count({ - where: { - folderId: folderId, - }, - }); -} - -export async function getPairsByFolderId(folderId: number) { - return prisma.pair.findMany({ - where: { - folderId: folderId, - }, - }); -} diff --git a/src/lib/server/services/userService.ts b/src/lib/server/services/userService.ts deleted file mode 100644 index 608c3fd..0000000 --- a/src/lib/server/services/userService.ts +++ /dev/null @@ -1,29 +0,0 @@ -import prisma from "@/lib/db"; -import { randomUUID } from "crypto"; - -export async function createUserIfNotExists(email: string, name?: string | null) { - const user = await prisma.user.upsert({ - where: { - email: email, - }, - update: {}, - create: { - id: randomUUID(), - email: email, - name: name || "New User", - }, - }); - return user; -} - -export async function getUserIdByEmail(email: string) { - const user = await prisma.user.findUnique({ - where: { - email: email, - }, - select: { - id: true, - }, - }); - return user ? user.id : null; -} diff --git a/src/lib/shared/dictionaryTypes.ts b/src/lib/shared/dictionaryTypes.ts deleted file mode 100644 index 7b4d5f0..0000000 --- a/src/lib/shared/dictionaryTypes.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type DictLookUpRequest = { - text: string, - queryLang: string, - definitionLang: string, - userId?: string, - forceRelook?: boolean; -}; - -export type DictWordEntry = { - ipa: string; - definition: string; - partOfSpeech: string; - example: string; -}; - -export type DictPhraseEntry = { - definition: string; - example: string; -}; - -export type DictWordResponse = { - standardForm: string; - entries: DictWordEntry[]; -}; - -export type DictPhraseResponse = { - standardForm: string; - entries: DictPhraseEntry[]; -}; - -export type DictLookUpResponse = - | DictWordResponse - | DictPhraseResponse; - -// 类型守卫:判断是否为单词响应 -export function isDictWordResponse( - response: DictLookUpResponse -): response is DictWordResponse { - const entries = (response as DictWordResponse | DictPhraseResponse).entries; - return entries.length > 0 && "ipa" in entries[0] && "partOfSpeech" in entries[0]; -} - -// 类型守卫:判断是否为短语响应 -export function isDictPhraseResponse( - response: DictLookUpResponse -): response is DictPhraseResponse { - const entries = (response as DictWordResponse | DictPhraseResponse).entries; - return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]); -} diff --git a/src/lib/shared/index.ts b/src/lib/shared/index.ts deleted file mode 100644 index 30779b4..0000000 --- a/src/lib/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dictionaryTypes"; diff --git a/src/lib/shared/validations/dictionaryValidations.ts b/src/lib/shared/validations/dictionaryValidations.ts deleted file mode 100644 index e4dc2c3..0000000 --- a/src/lib/shared/validations/dictionaryValidations.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DictLookUpRequest } from "@/lib/shared"; - -export const lookUpValidation = (req: DictLookUpRequest) => { - const { - text, - queryLang, - definitionLang, - } = req; - - if (text.length > 30) - throw Error("The input should not exceed 30 characters."); - if (queryLang.length > 20) - throw Error("The query language should not exceed 20 characters."); - if (definitionLang.length > 20) - throw Error("The definition language should not exceed 20 characters."); - if (queryLang.length > 20) - throw Error("The query language should not exceed 20 characters."); - if (queryLang.length > 20) - throw Error("The query language should not exceed 20 characters."); - if (queryLang.length > 20) - throw Error("The query language should not exceed 20 characters."); -}; diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts deleted file mode 100644 index 33eacfe..0000000 --- a/src/lib/theme/colors.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 主题配色常量 - * 集中管理应用的品牌颜色 - * - * 注意:Tailwind CSS 已有的标准颜色(gray、red 等)请直接使用 Tailwind 类名 - * 这里只定义项目独有的品牌色 - */ -export const COLORS = { - // ===== 主色调 ===== - /** 主绿色 - 应用主题色,用于页面背景、主要按钮 */ - primary: '#35786f', - /** 悬停绿色 - 按钮悬停状态 */ - primaryHover: '#2d5f58' -} as const; diff --git a/src/modules/dictionary/dictionary-action-dto.ts b/src/modules/dictionary/dictionary-action-dto.ts new file mode 100644 index 0000000..881a4af --- /dev/null +++ b/src/modules/dictionary/dictionary-action-dto.ts @@ -0,0 +1,30 @@ +import { ValidateError } from "@/lib/errors"; +import { TSharedItem } from "@/shared"; +import z from "zod"; + +const DictionaryActionInputDtoSchema = z.object({ + text: z.string().min(1, 'Empty text.').max(30, 'Text too long.'), + queryLang: z.string().min(1, 'Query lang too short.').max(20, 'Query lang too long.'), + forceRelook: z.boolean(), + definitionLang: z.string().min(1, 'Definition lang too short.').max(20, 'Definition lang too long.'), + userId: z.string().optional() +}); + +export type DictionaryActionInputDto = z.infer; + +export const validateDictionaryActionInput = (dto: DictionaryActionInputDto): DictionaryActionInputDto => { + const result = DictionaryActionInputDtoSchema.safeParse(dto); + if (result.success) return result.data; + + const errorMessages = result.error.issues.map((issue) => + `${issue.path.join('.')}: ${issue.message}` + ).join('; '); + + throw new ValidateError(`Validation failed: ${errorMessages}`); +}; + +export type DictionaryActionOutputDto = { + message: string, + success: boolean; + data?: TSharedItem; +}; diff --git a/src/modules/dictionary/dictionary-action.ts b/src/modules/dictionary/dictionary-action.ts new file mode 100644 index 0000000..c508d2a --- /dev/null +++ b/src/modules/dictionary/dictionary-action.ts @@ -0,0 +1,27 @@ +"use server"; + +import { DictionaryActionInputDto, DictionaryActionOutputDto, validateDictionaryActionInput } from "./dictionary-action-dto"; +import { ValidateError } from "@/lib/errors"; +import { lookUpService } from "./dictionary-service"; + +export const lookUpDictionaryAction = async (dto: DictionaryActionInputDto): Promise => { + try { + return { + message: 'success', + success: true, + data: await lookUpService(validateDictionaryActionInput(dto)) + }; + } catch (e) { + if (e instanceof ValidateError) { + return { + success: false, + message: e.message + }; + } + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +}; diff --git a/src/modules/dictionary/dictionary-repository-dto.ts b/src/modules/dictionary/dictionary-repository-dto.ts new file mode 100644 index 0000000..add7e7a --- /dev/null +++ b/src/modules/dictionary/dictionary-repository-dto.ts @@ -0,0 +1,38 @@ +import { TSharedItem } from "@/shared"; + +export type CreateDictionaryLookUpInputDto = { + userId?: string; + text: string; + queryLang: string; + definitionLang: string; + dictionaryItemId?: number; +}; + +export type SelectLastLookUpResultOutputDto = TSharedItem & {id: number} | null; + +export type CreateDictionaryItemInputDto = { + standardForm: string; + queryLang: string; + definitionLang: string; +}; + +export type CreateDictionaryEntryInputDto = { + itemId: number; + ipa?: string; + definition: string; + partOfSpeech?: string; + example: string; +}; + +export type CreateDictionaryEntryWithoutItemIdInputDto = { + ipa?: string; + definition: string; + partOfSpeech?: string; + example: string; +}; + +export type SelectLastLookUpResultInputDto = { + text: string, + queryLang: string, + definitionLang: string; +}; diff --git a/src/modules/dictionary/dictionary-repository.ts b/src/modules/dictionary/dictionary-repository.ts new file mode 100644 index 0000000..fb41555 --- /dev/null +++ b/src/modules/dictionary/dictionary-repository.ts @@ -0,0 +1,86 @@ +import { stringNormalize } from "@/utils/string"; +import { + CreateDictionaryEntryInputDto, + CreateDictionaryEntryWithoutItemIdInputDto, + CreateDictionaryItemInputDto, + CreateDictionaryLookUpInputDto, + SelectLastLookUpResultInputDto, + SelectLastLookUpResultOutputDto, +} from "./dictionary-repository-dto"; +import prisma from "@/lib/db"; + +export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto): Promise { + const result = await prisma.dictionaryLookUp.findFirst({ + where: { + normalizedText: stringNormalize(dto.text), + queryLang: dto.queryLang, + definitionLang: dto.definitionLang, + dictionaryItemId: { + not: null + } + }, + include: { + dictionaryItem: { + include: { + entries: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + if (result && result.dictionaryItem) { + const item = result.dictionaryItem; + return { + id: item.id, + standardForm: item.standardForm, + entries: item.entries.filter(v => !!v).map(v => { + return { + ipa: v.ipa || undefined, + definition: v.definition, + partOfSpeech: v.partOfSpeech || undefined, + example: v.example + }; + }) + }; + } + return null; +} + +export async function createLookUp(content: CreateDictionaryLookUpInputDto) { + return (await prisma.dictionaryLookUp.create({ + data: { ...content, normalizedText: stringNormalize(content.text) } + })).id; +} + +export async function createLookUpWithItemAndEntries( + itemData: CreateDictionaryItemInputDto, + lookUpData: CreateDictionaryLookUpInputDto, + entries: CreateDictionaryEntryWithoutItemIdInputDto[] +) { + return await prisma.$transaction(async (tx) => { + const item = await tx.dictionaryItem.create({ + data: itemData + }); + + await tx.dictionaryLookUp.create({ + data: { + ...lookUpData, + normalizedText: stringNormalize(lookUpData.text), + dictionaryItemId: item.id + } + }); + + for (const entry of entries) { + await tx.dictionaryEntry.create({ + data: { + ...entry, + itemId: item.id + } + }); + } + + return item.id; + }); +} diff --git a/src/modules/dictionary/dictionary-service-dto.ts b/src/modules/dictionary/dictionary-service-dto.ts new file mode 100644 index 0000000..bc90218 --- /dev/null +++ b/src/modules/dictionary/dictionary-service-dto.ts @@ -0,0 +1,11 @@ +import { TSharedItem } from "@/shared"; + +export type LookUpServiceInputDto = { + text: string, + queryLang: string, + definitionLang: string, + forceRelook: boolean, + userId?: string; +}; + +export type LookUpServiceOutputDto = TSharedItem; diff --git a/src/modules/dictionary/dictionary-service.ts b/src/modules/dictionary/dictionary-service.ts new file mode 100644 index 0000000..5591e5a --- /dev/null +++ b/src/modules/dictionary/dictionary-service.ts @@ -0,0 +1,61 @@ +import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary"; +import { createLookUp, createLookUpWithItemAndEntries, selectLastLookUpResult } from "./dictionary-repository"; +import { LookUpServiceInputDto } from "./dictionary-service-dto"; + +export const lookUpService = async (dto: LookUpServiceInputDto) => { + const { + text, + queryLang, + userId, + definitionLang, + forceRelook + } = dto; + + const lastLookUpResult = await selectLastLookUpResult({ + text, + queryLang, + definitionLang, + }); + + if (forceRelook || !lastLookUpResult) { + const response = await executeDictionaryLookup( + text, + queryLang, + definitionLang + ); + + // 使用事务确保数据一致性 + createLookUpWithItemAndEntries( + { + standardForm: response.standardForm, + queryLang, + definitionLang + }, + { + userId, + text, + queryLang, + definitionLang, + }, + response.entries + ).catch(error => { + console.error('Failed to save dictionary data:', error); + }); + + return response; + } else { + createLookUp({ + userId: userId, + text: text, + queryLang: queryLang, + definitionLang: definitionLang, + dictionaryItemId: lastLookUpResult.id + }).catch(error => { + console.error('Failed to save dictionary data:', error); + }); + return { + standardForm: lastLookUpResult.standardForm, + entries: lastLookUpResult.entries + }; + } +}; \ No newline at end of file diff --git a/src/modules/dictionary/index.ts b/src/modules/dictionary/index.ts new file mode 100644 index 0000000..b6517a0 --- /dev/null +++ b/src/modules/dictionary/index.ts @@ -0,0 +1,2 @@ +export * from "./dictionary-action"; +export * from "./dictionary-action-dto"; diff --git a/src/modules/folder/folder-aciton.ts b/src/modules/folder/folder-aciton.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/folder/folder-action-dto.ts b/src/modules/folder/folder-action-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/folder/folder-service-dto.ts b/src/modules/folder/folder-service-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/server/services/folderService.ts b/src/modules/folder/folder-service.ts similarity index 91% rename from src/lib/server/services/folderService.ts rename to src/modules/folder/folder-service.ts index 5c00dfc..9ce74d3 100644 --- a/src/lib/server/services/folderService.ts +++ b/src/modules/folder/folder-service.ts @@ -1,7 +1,5 @@ -"use server"; - -import { CreateFolderInput, UpdateFolderInput } from "./types"; -import prisma from "../../db"; +import { CreateFolderInput, UpdateFolderInput } from "../translator/translator-dto"; +import prisma from "@/lib/db"; export async function getFoldersByUserId(userId: string) { return prisma.folder.findMany({ diff --git a/src/modules/pair/pair-action-dto.ts b/src/modules/pair/pair-action-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/pair/pair-action.ts b/src/modules/pair/pair-action.ts new file mode 100644 index 0000000..6ae8d13 --- /dev/null +++ b/src/modules/pair/pair-action.ts @@ -0,0 +1 @@ +"use server"; \ No newline at end of file diff --git a/src/modules/pair/pair-repository.ts b/src/modules/pair/pair-repository.ts new file mode 100644 index 0000000..6d4b63b --- /dev/null +++ b/src/modules/pair/pair-repository.ts @@ -0,0 +1,44 @@ +import { CreatePairInput, UpdatePairInput } from "../translator/translator-dto"; +import prisma from "@/lib/db"; + +export async function createPair(data: CreatePairInput) { + return (await prisma.pair.create({ + data: data, + })).id; +} + +export async function deletePairById(id: number) { + await prisma.pair.delete({ + where: { + id: id, + }, + }); +} + +export async function updatePairById( + id: number, + data: UpdatePairInput, +) { + await prisma.pair.update({ + where: { + id: id, + }, + data: data, + }); +} + +export async function getPairCountByFolderId(folderId: number) { + return prisma.pair.count({ + where: { + folderId: folderId, + }, + }); +} + +export async function getPairsByFolderId(folderId: number) { + return prisma.pair.findMany({ + where: { + folderId: folderId, + }, + }); +} diff --git a/src/modules/pair/pair-service-dto.ts b/src/modules/pair/pair-service-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/pair/pair-service.ts b/src/modules/pair/pair-service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/translator/translator-action-dto.ts b/src/modules/translator/translator-action-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/server/bigmodel/translatorActions.ts b/src/modules/translator/translator-action.ts similarity index 98% rename from src/lib/server/bigmodel/translatorActions.ts rename to src/modules/translator/translator-action.ts index 9f8e47f..fbf686b 100644 --- a/src/lib/server/bigmodel/translatorActions.ts +++ b/src/modules/translator/translator-action.ts @@ -1,8 +1,8 @@ "use server"; -import { getAnswer } from "./zhipu"; -import { selectLatestTranslation, createTranslationHistory } from "../services/translatorService"; -import { TranslateTextInput, TranslateTextOutput, TranslationLLMResponse } from "../services/types"; +import { getAnswer } from "@/lib/bigmodel/zhipu"; +import { selectLatestTranslation, createTranslationHistory } from "./translator-service"; +import { TranslateTextInput, TranslateTextOutput, TranslationLLMResponse } from "./translator-dto"; /** * @deprecated 请使用 translateText 函数代替 diff --git a/src/lib/server/services/types.ts b/src/modules/translator/translator-dto.ts similarity index 65% rename from src/lib/server/services/types.ts rename to src/modules/translator/translator-dto.ts index 0094ca2..5ac0671 100644 --- a/src/lib/server/services/types.ts +++ b/src/modules/translator/translator-dto.ts @@ -50,51 +50,6 @@ export interface TranslationHistoryQuery { targetLanguage: string; } -// Dictionary 相关 -export interface CreateDictionaryLookUpInput { - userId?: string; - text: string; - queryLang: string; - definitionLang: string; - dictionaryWordId?: number; - dictionaryPhraseId?: number; -} - -export interface DictionaryLookUpQuery { - userId?: string; - text?: string; - queryLang?: string; - definitionLang?: string; - dictionaryWordId?: number; - dictionaryPhraseId?: number; -} - -export interface CreateDictionaryWordInput { - standardForm: string; - queryLang: string; - definitionLang: string; -} - -export interface CreateDictionaryPhraseInput { - standardForm: string; - queryLang: string; - definitionLang: string; -} - -export interface CreateDictionaryWordEntryInput { - wordId: number; - ipa: string; - definition: string; - partOfSpeech: string; - example: string; -} - -export interface CreateDictionaryPhraseEntryInput { - phraseId: number; - definition: string; - example: string; -} - // 翻译相关 - 统一翻译函数 export interface TranslateTextInput { sourceText: string; diff --git a/src/modules/translator/translator-repository.ts b/src/modules/translator/translator-repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/translator/translator-service-dto.ts b/src/modules/translator/translator-service-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/server/services/translatorService.ts b/src/modules/translator/translator-service.ts similarity index 92% rename from src/lib/server/services/translatorService.ts rename to src/modules/translator/translator-service.ts index d5e8423..679d757 100644 --- a/src/lib/server/services/translatorService.ts +++ b/src/modules/translator/translator-service.ts @@ -1,7 +1,7 @@ "use server"; -import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./types"; -import prisma from "../../db"; +import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./translator-dto"; +import prisma from "@/lib/db"; /** * 创建翻译历史记录 diff --git a/src/modules/user/user-action-dto.ts b/src/modules/user/user-action-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/actions/auth.ts b/src/modules/user/user-action.ts similarity index 100% rename from src/lib/actions/auth.ts rename to src/modules/user/user-action.ts diff --git a/src/modules/user/user-repository.ts b/src/modules/user/user-repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/user/user-service-dto.ts b/src/modules/user/user-service-dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/user/user-service.ts b/src/modules/user/user-service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/dictionary-type.ts b/src/shared/dictionary-type.ts new file mode 100644 index 0000000..4d41b1d --- /dev/null +++ b/src/shared/dictionary-type.ts @@ -0,0 +1,11 @@ +export type TSharedEntry = { + definition: string, + example: string, + partOfSpeech?: string; + ipa?: string; +}; + +export type TSharedItem = { + standardForm: string, + entries: TSharedEntry[]; +}; \ No newline at end of file diff --git a/src/shared/folder-related-type.ts b/src/shared/folder-related-type.ts new file mode 100644 index 0000000..4950db4 --- /dev/null +++ b/src/shared/folder-related-type.ts @@ -0,0 +1,3 @@ +export type TSharedPair = { + +}; \ No newline at end of file diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..7c3ea2a --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1 @@ +export * from './dictionary-type'; \ No newline at end of file diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 0000000..234aa98 --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,25 @@ +export function parseAIGeneratedJSON(aiResponse: string): T { + // 匹配 ```json ... ``` 包裹的内容 + const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/); + + if (jsonMatch && jsonMatch[1]) { + try { + return JSON.parse(jsonMatch[1].trim()); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to parse JSON: ${error.message}`); + } else if (typeof error === 'string') { + throw new Error(`Failed to parse JSON: ${error}`); + } else { + throw new Error('Failed to parse JSON: Unknown error'); + } + } + } + + // 如果没有找到json代码块,尝试直接解析整个字符串 + try { + return JSON.parse(aiResponse.trim()); + } catch (error) { + throw new Error('No valid JSON found in the response'); + } +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/utils/random.ts similarity index 81% rename from src/lib/utils.ts rename to src/utils/random.ts index a3390dd..1e35e58 100644 --- a/src/lib/utils.ts +++ b/src/utils/random.ts @@ -1,3 +1,4 @@ + export function isNonNegativeInteger(str: string): boolean { return /^\d+$/.test(str); } @@ -5,19 +6,19 @@ export function isNonNegativeInteger(str: string): boolean { export function shallowEqual(obj1: T, obj2: T): boolean { const keys1 = Object.keys(obj1) as Array; const keys2 = Object.keys(obj2) as Array; - + // 首先检查键的数量是否相同 if (keys1.length !== keys2.length) { return false; } - + // 然后逐个比较键值对 for (const key of keys1) { if (obj1[key] !== obj2[key]) { return false; } } - + return true; } export class SeededRandom { @@ -147,29 +148,3 @@ export class SeededRandom { return shuffled; } } - -export function parseAIGeneratedJSON(aiResponse: string): T { - // 匹配 ```json ... ``` 包裹的内容 - const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/); - - if (jsonMatch && jsonMatch[1]) { - try { - return JSON.parse(jsonMatch[1].trim()); - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to parse JSON: ${error.message}`); - } else if (typeof error === 'string') { - throw new Error(`Failed to parse JSON: ${error}`); - } else { - throw new Error('Failed to parse JSON: Unknown error'); - } - } - } - - // 如果没有找到json代码块,尝试直接解析整个字符串 - try { - return JSON.parse(aiResponse.trim()); - } catch (error) { - throw new Error('No valid JSON found in the response'); - } -} \ No newline at end of file diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..bd805c9 --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1 @@ +export const stringNormalize = (s: string) => s.trim().toLowerCase(); \ No newline at end of file