diff --git a/prisma/migrations/20260106061255_remove_unique_constraint/migration.sql b/prisma/migrations/20260106061255_remove_unique_constraint/migration.sql
new file mode 100644
index 0000000..ffbe1ce
--- /dev/null
+++ b/prisma/migrations/20260106061255_remove_unique_constraint/migration.sql
@@ -0,0 +1,8 @@
+-- DropIndex
+DROP INDEX "dictionary_phrases_standard_form_query_lang_definition_lang_key";
+
+-- DropIndex
+DROP INDEX "dictionary_words_standard_form_query_lang_definition_lang_key";
+
+-- RenameIndex
+ALTER INDEX "pairs_folder_id_locale1_locale2_text1_key" RENAME TO "pairs_folder_id_language1_language2_text1_key";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e4df3ce..77a719b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -136,7 +136,6 @@ model DictionaryWord {
lookups DictionaryLookUp[]
entries DictionaryWordEntry[]
- @@unique([standardForm, queryLang, definitionLang])
@@index([standardForm])
@@index([queryLang, definitionLang])
@@map("dictionary_words")
@@ -153,7 +152,6 @@ model DictionaryPhrase {
lookups DictionaryLookUp[]
entries DictionaryPhraseEntry[]
- @@unique([standardForm, queryLang, definitionLang])
@@index([standardForm])
@@index([queryLang, definitionLang])
@@map("dictionary_phrases")
diff --git a/src/app/(features)/dictionary/DictionaryEntry.tsx b/src/app/(features)/dictionary/DictionaryEntry.tsx
new file mode 100644
index 0000000..b06495f
--- /dev/null
+++ b/src/app/(features)/dictionary/DictionaryEntry.tsx
@@ -0,0 +1,78 @@
+import { DictWordEntry, DictPhraseEntry } from "./types";
+
+interface DictionaryEntryProps {
+ entry: DictWordEntry | DictPhraseEntry;
+}
+
+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 (
+
+ {/* 释义 */}
+
+
+ 释义
+
+
{phraseEntry.definition}
+
+
+ {/* 例句 */}
+ {phraseEntry.example && (
+
+
+ 例句
+
+
+ {phraseEntry.example}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(features)/dictionary/DictionaryPage.tsx b/src/app/(features)/dictionary/DictionaryPage.tsx
new file mode 100644
index 0000000..c5ff2e5
--- /dev/null
+++ b/src/app/(features)/dictionary/DictionaryPage.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Container from "@/components/ui/Container";
+import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
+import { toast } from "sonner";
+import { authClient } from "@/lib/auth-client";
+import { Folder } from "../../../../generated/prisma/browser";
+import { getFoldersByUserId } from "@/lib/server/services/folderService";
+import { DictLookUpResponse, isDictErrorResponse } from "./types";
+import { SearchForm } from "./SearchForm";
+import { SearchResult } from "./SearchResult";
+
+export default function 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]);
+
+ const handleSearch = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!searchQuery.trim()) return;
+
+ setIsSearching(true);
+ setHasSearched(true);
+ setSearchResult(null);
+
+ try {
+ // 使用查询语言和释义语言
+ // const result = await lookUp(searchQuery, queryLang, definitionLang);
+ const result = await lookUp({
+ text: searchQuery,
+ definitionLang: definitionLang,
+ queryLang: queryLang,
+ forceRelook: false
+ })
+
+ // 检查是否为错误响应
+ if (isDictErrorResponse(result)) {
+ toast.error(result.error);
+ setSearchResult(null);
+ } else {
+ setSearchResult(result);
+ }
+ } catch (error) {
+ console.error("词典查询失败:", error);
+ toast.error("查询失败,请稍后重试");
+ setSearchResult(null);
+ } finally {
+ setIsSearching(false);
+ }
+ };
+
+ return (
+
+ {/* 搜索区域 */}
+
+
+
+
+
+
+ {/* 搜索结果区域 */}
+
+
+ {isSearching && (
+
+ )}
+
+ {!isSearching && hasSearched && !searchResult && (
+
+ )}
+
+ {!isSearching && searchResult && !isDictErrorResponse(searchResult) && (
+
+ )}
+
+ {!hasSearched && (
+
+
📚
+
欢迎使用词典
+
在上方搜索框中输入单词或短语开始查询
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(features)/dictionary/SearchForm.tsx b/src/app/(features)/dictionary/SearchForm.tsx
new file mode 100644
index 0000000..46cc10c
--- /dev/null
+++ b/src/app/(features)/dictionary/SearchForm.tsx
@@ -0,0 +1,124 @@
+import { LightButton } from "@/components/ui/buttons";
+import { POPULAR_LANGUAGES } from "./constants";
+
+interface SearchFormProps {
+ searchQuery: string;
+ onSearchQueryChange: (query: string) => void;
+ isSearching: boolean;
+ onSearch: (e: React.FormEvent) => void;
+ queryLang: string;
+ onQueryLangChange: (lang: string) => void;
+ definitionLang: string;
+ onDefinitionLangChange: (lang: string) => void;
+}
+
+export function SearchForm({
+ searchQuery,
+ onSearchQueryChange,
+ isSearching,
+ onSearch,
+ queryLang,
+ onQueryLangChange,
+ definitionLang,
+ onDefinitionLangChange,
+}: SearchFormProps) {
+ return (
+ <>
+ {/* 页面标题 */}
+
+
+ 词典
+
+
+ 查询单词和短语,提供详细的释义和例句
+
+
+
+ {/* 搜索表单 */}
+
+
+ {/* 语言设置 */}
+
+
+ 语言设置
+
+
+
+ {/* 查询语言 */}
+
+
+
+ {POPULAR_LANGUAGES.map((lang) => (
+ onQueryLangChange(lang.code)}
+ className="text-sm px-3 py-1"
+ >
+ {lang.name}
+
+ ))}
+
+
onQueryLangChange(e.target.value)}
+ placeholder="或输入其他语言..."
+ className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
+ />
+
+
+ {/* 释义语言 */}
+
+
+
+ {POPULAR_LANGUAGES.map((lang) => (
+ onDefinitionLangChange(lang.code)}
+ className="text-sm px-3 py-1"
+ >
+ {lang.name}
+
+ ))}
+
+
onDefinitionLangChange(e.target.value)}
+ placeholder="或输入其他语言..."
+ className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
+ />
+
+
+ {/* 当前设置显示 */}
+
+ 当前设置:查询 {POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}
+ ,释义 {POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx
new file mode 100644
index 0000000..d4c50b4
--- /dev/null
+++ b/src/app/(features)/dictionary/SearchResult.tsx
@@ -0,0 +1,155 @@
+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 { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
+import {
+ DictWordResponse,
+ DictPhraseResponse,
+ isDictWordResponse,
+ DictWordEntry,
+ isDictErrorResponse,
+} from "./types";
+import { DictionaryEntry } from "./DictionaryEntry";
+import { POPULAR_LANGUAGES } from "./constants";
+
+interface SearchResultProps {
+ searchResult: DictWordResponse | DictPhraseResponse;
+ searchQuery: string;
+ queryLang: string;
+ definitionLang: string;
+ folders: Folder[];
+ selectedFolderId: number | null;
+ onFolderSelect: (folderId: number | null) => void;
+ onResultUpdate: (newResult: DictWordResponse | DictPhraseResponse) => void;
+ onSearchingChange: (isSearching: boolean) => void;
+}
+
+export function SearchResult({
+ searchResult,
+ searchQuery,
+ queryLang,
+ definitionLang,
+ folders,
+ selectedFolderId,
+ onFolderSelect,
+ onResultUpdate,
+ onSearchingChange,
+}: SearchResultProps) {
+ const { data: session } = authClient.useSession();
+
+ const handleRelookup = async () => {
+ onSearchingChange(true);
+
+ try {
+ const result = await lookUp({
+ text: searchQuery,
+ definitionLang: definitionLang,
+ queryLang: queryLang,
+ forceRelook: true
+ });
+
+ if (isDictErrorResponse(result)) {
+ toast.error(result.error);
+ } else {
+ onResultUpdate(result);
+ toast.success("已重新查询");
+ }
+ } catch (error) {
+ console.error("词典重新查询失败:", error);
+ toast.error("查询失败,请稍后重试");
+ } finally {
+ onSearchingChange(false);
+ }
+ };
+
+ const handleSave = () => {
+ if (!session) {
+ toast.error("请先登录");
+ return;
+ }
+ if (!selectedFolderId) {
+ toast.error("请先创建文件夹");
+ return;
+ }
+
+ const entry = searchResult.entries[0];
+ createPair({
+ text1: searchResult.standardForm,
+ text2: entry.definition,
+ language1: queryLang,
+ language2: definitionLang,
+ ipa1: isDictWordResponse(searchResult) && (entry as DictWordEntry).ipa ? (entry as DictWordEntry).ipa : undefined,
+ folder: {
+ connect: {
+ id: selectedFolderId,
+ },
+ },
+ })
+ .then(() => {
+ const folderName = folders.find(f => f.id === selectedFolderId)?.name;
+ toast.success(`已保存到文件夹:${folderName}`);
+ })
+ .catch(() => {
+ toast.error("保存失败,请稍后重试");
+ });
+ };
+
+ return (
+
+
+ {/* 标题和保存按钮 */}
+
+
+
+ {searchResult.standardForm}
+
+
+
+ {session && folders.length > 0 && (
+
+ )}
+
+
+
+
+ {/* 条目列表 */}
+
+ {searchResult.entries.map((entry, index) => (
+
+
+
+ ))}
+
+
+ {/* 重新查询按钮 */}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(features)/dictionary/constants.ts b/src/app/(features)/dictionary/constants.ts
new file mode 100644
index 0000000..e0c6791
--- /dev/null
+++ b/src/app/(features)/dictionary/constants.ts
@@ -0,0 +1,10 @@
+export const POPULAR_LANGUAGES = [
+ { code: "english", name: "英语" },
+ { code: "chinese", name: "中文" },
+ { code: "japanese", name: "日语" },
+ { code: "korean", name: "韩语" },
+ { code: "french", name: "法语" },
+ { code: "german", name: "德语" },
+ { code: "italian", name: "意大利语" },
+ { code: "spanish", name: "西班牙语" },
+] as const;
diff --git a/src/app/(features)/dictionary/index.ts b/src/app/(features)/dictionary/index.ts
new file mode 100644
index 0000000..8cb51b8
--- /dev/null
+++ b/src/app/(features)/dictionary/index.ts
@@ -0,0 +1,11 @@
+// 类型定义
+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 4fbc9f7..dd8e42b 100644
--- a/src/app/(features)/dictionary/page.tsx
+++ b/src/app/(features)/dictionary/page.tsx
@@ -1,398 +1 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import Container from "@/components/ui/Container";
-import { LightButton } from "@/components/ui/buttons";
-import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
-import { toast } from "sonner";
-import { Plus } from "lucide-react";
-import { authClient } from "@/lib/auth-client";
-import { Folder } from "../../../../generated/prisma/browser";
-import { getFoldersByUserId } from "@/lib/server/services/folderService";
-import { createPair } from "@/lib/server/services/pairService";
-
-// 主流语言列表
-const POPULAR_LANGUAGES = [
- { code: "english", name: "英语" },
- { code: "chinese", name: "中文" },
- { code: "japanese", name: "日语" },
- { code: "korean", name: "韩语" },
- { code: "french", name: "法语" },
- { code: "german", name: "德语" },
- { code: "italian", name: "意大利语" },
- { code: "spanish", name: "西班牙语" },
-];
-
-type DictionaryWordEntry = {
- ipa: string;
- definition: string;
- partOfSpeech: string;
- example: string;
-};
-
-type DictionaryPhraseEntry = {
- definition: string;
- example: string;
-};
-
-type DictionaryErrorResponse = {
- error: string;
-};
-
-type DictionarySuccessResponse = {
- standardForm: string;
- entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
-};
-
-type DictionaryResponse = DictionarySuccessResponse | DictionaryErrorResponse;
-
-// 类型守卫:判断是否为单词条目
-function isWordEntry(entry: DictionaryWordEntry | DictionaryPhraseEntry): entry is DictionaryWordEntry {
- return "ipa" in entry && "partOfSpeech" in entry;
-}
-
-// 类型守卫:判断是否为错误响应
-function isErrorResponse(response: DictionaryResponse): response is DictionaryErrorResponse {
- return "error" in response;
-}
-
-export default function 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 [showLangSettings, setShowLangSettings] = useState(false);
- 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]);
-
- const handleSearch = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!searchQuery.trim()) return;
-
- setIsSearching(true);
- setHasSearched(true);
- setSearchResult(null);
-
- try {
- // 使用查询语言和释义语言
- const result = await lookUp(searchQuery, queryLang, definitionLang);
-
- // 检查是否为错误响应
- if (isErrorResponse(result)) {
- toast.error(result.error);
- setSearchResult(null);
- } else {
- setSearchResult(result);
- }
- } catch (error) {
- console.error("词典查询失败:", error);
- toast.error("查询失败,请稍后重试");
- setSearchResult(null);
- } finally {
- setIsSearching(false);
- }
- };
-
- return (
-
- {/* 搜索区域 */}
-
-
- {/* 页面标题 */}
-
-
- 词典
-
-
- 查询单词和短语,提供详细的释义和例句
-
-
-
- {/* 搜索表单 */}
-
-
- {/* 语言设置 */}
-
-
- 语言设置
- setShowLangSettings(!showLangSettings)}
- className="text-sm px-4 py-2"
- >
- {showLangSettings ? "收起" : "展开"}
-
-
-
- {showLangSettings && (
-
- {/* 查询语言 */}
-
-
-
- {POPULAR_LANGUAGES.map((lang) => (
- setQueryLang(lang.code)}
- className="text-sm px-3 py-1"
- >
- {lang.name}
-
- ))}
-
-
setQueryLang(e.target.value)}
- placeholder="或输入其他语言..."
- className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
- />
-
-
- {/* 释义语言 */}
-
-
-
- {POPULAR_LANGUAGES.map((lang) => (
- setDefinitionLang(lang.code)}
- className="text-sm px-3 py-1"
- >
- {lang.name}
-
- ))}
-
-
setDefinitionLang(e.target.value)}
- placeholder="或输入其他语言..."
- className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
- />
-
-
- {/* 当前设置显示 */}
-
- 当前设置:查询 {POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}
- ,释义 {POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}
-
-
- )}
-
-
- {/* 搜索提示 */}
-
-
试试搜索:hello, look up, dictionary
-
-
-
-
- {/* 搜索结果区域 */}
-
-
- {isSearching && (
-
- )}
-
- {!isSearching && hasSearched && !searchResult && (
-
- )}
-
- {!isSearching && searchResult && !isErrorResponse(searchResult) && (
-
-
- {/* 标题和保存按钮 */}
-
-
-
- {searchResult.standardForm}
-
- {searchResult.standardForm !== searchQuery && (
-
- 原始输入: {searchQuery}
-
- )}
-
-
- {session && folders.length > 0 && (
-
- )}
-
-
-
-
- {/* 条目列表 */}
-
- {searchResult.entries.map((entry, index) => (
-
- {isWordEntry(entry) ? (
- // 单词条目
-
- {/* 音标和词性 */}
-
- {entry.ipa && (
-
- {entry.ipa}
-
- )}
- {entry.partOfSpeech && (
-
- {entry.partOfSpeech}
-
- )}
-
-
- {/* 释义 */}
-
-
- 释义
-
-
{entry.definition}
-
-
- {/* 例句 */}
- {entry.example && (
-
-
- 例句
-
-
- {entry.example}
-
-
- )}
-
- ) : (
- // 短语条目
-
- {/* 释义 */}
-
-
- 释义
-
-
{entry.definition}
-
-
- {/* 例句 */}
- {entry.example && (
-
-
- 例句
-
-
- {entry.example}
-
-
- )}
-
- )}
-
- ))}
-
-
-
- )}
-
- {!hasSearched && (
-
-
📚
-
欢迎使用词典
-
在上方搜索框中输入单词或短语开始查询
-
- )}
-
-
-
- );
-}
+export { default } from "./DictionaryPage";
diff --git a/src/app/(features)/dictionary/types.ts b/src/app/(features)/dictionary/types.ts
new file mode 100644
index 0000000..5df1500
--- /dev/null
+++ b/src/app/(features)/dictionary/types.ts
@@ -0,0 +1,2 @@
+// 从 shared 文件夹导出所有词典类型和类型守卫
+export * from "@/lib/shared";
diff --git a/src/lib/server/bigmodel/dictionaryActions.ts b/src/lib/server/bigmodel/dictionaryActions.ts
index a92433b..ea89dc8 100644
--- a/src/lib/server/bigmodel/dictionaryActions.ts
+++ b/src/lib/server/bigmodel/dictionaryActions.ts
@@ -2,99 +2,183 @@
import { parseAIGeneratedJSON } from "@/lib/utils";
import { getAnswer } from "./zhipu";
+import { createLookUp, createPhrase, createWord, selectLastLookUp } from "../services/dictionaryService";
+import { DictLookUpRequest, DictWordResponse, isDictErrorResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
-type DictionaryWordEntry = {
- ipa: string;
- definition: string;
- partOfSpeech: string;
- example: string;
+const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
+ if (isDictErrorResponse(res)) return;
+ else if (isDictPhraseResponse(res)) {
+ return createPhrase({
+ standardForm: res.standardForm,
+ queryLang: req.queryLang,
+ definitionLang: req.definitionLang,
+ lookups: {
+ create: {
+ user: req.userId ? {
+ connect: {
+ id: req.userId
+ }
+ } : undefined,
+ text: req.text,
+ queryLang: req.queryLang,
+ definitionLang: req.definitionLang
+ }
+ },
+ entries: {
+ createMany: {
+ data: res.entries
+ }
+ }
+ });
+ } else if (isDictWordResponse(res)) {
+ return createWord({
+ standardForm: (res as DictWordResponse).standardForm,
+ queryLang: req.queryLang,
+ definitionLang: req.definitionLang,
+ lookups: {
+ create: {
+ user: req.userId ? {
+ connect: {
+ id: req.userId
+ }
+ } : undefined,
+ text: req.text,
+ queryLang: req.queryLang,
+ definitionLang: req.definitionLang
+ }
+ },
+ entries: {
+ createMany: {
+ data: (res as DictWordResponse).entries
+ }
+ }
+ });
+ }
};
-type DictionaryPhraseEntry = {
- definition: string;
- example: string;
-};
-
-type DictionaryErrorResponse = {
- error: string;
-};
-
-type DictionarySuccessResponse = {
- standardForm: string;
- entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
-};
-
-export const lookUp = async (
- text: string,
- queryLang: string,
- definitionLang: string
-): Promise => {
- const response = await getAnswer([
- {
- role: "system",
- content: `
-你是一个词典工具,返回单词/短语的JSON解释。
+export const lookUp = async ({
+ text,
+ queryLang,
+ definitionLang,
+ userId,
+ forceRelook = false
+}: DictLookUpRequest): Promise => {
+ try {
+ const lastLookUp = await selectLastLookUp({
+ text,
+ queryLang,
+ definitionLang
+ });
+ if (forceRelook || !lastLookUp) {
+ const response = await getAnswer([
+ {
+ role: "system",
+ content: `
+你是一个词典工具,返回单词或短语的 JSON 解释结果。
查询语言:${queryLang}
释义语言:${definitionLang}
-用户输入在标签内。判断是单词还是短语。
+用户输入在 标签内,判断是单词还是短语。
-如果输入有效,返回JSON对象,格式为:
+语言规则:
+
+若输入语言与查询语言一致,直接查询。
+
+若不一致但语义清晰(如“吃”“跑”“睡觉”),先理解其语义,再映射到查询语言中最常见、最标准的对应词或短语(如 查询语言=意大利语,输入“吃” → mangiare)。
+
+若语义不清晰或存在明显歧义,视为无效输入。
+
+standardForm 规则:
+返回查询语言下的标准形式(英语动词原形、日语基本形、罗曼语族不定式等)。如无法确定,则与输入相同。
+
+有效输入时返回:
{
- "standardForm": "字符串,该语言下的正确形式",
- "entries": [数组,包含一个或多个条目]
+"standardForm": "标准形式",
+"entries": [...]
}
-如果是单词,条目格式:
+单词条目格式:
{
- "ipa": "音标(如适用)",
- "definition": "释义",
- "partOfSpeech": "词性",
- "example": "例句"
+"ipa": "音标(如适用)",
+"definition": "释义(使用 ${definitionLang})",
+"partOfSpeech": "词性",
+"example": "例句(使用 ${queryLang})"
}
-如果是短语,条目格式:
+短语条目格式:
{
- "definition": "短语释义",
- "example": "例句"
+"definition": "释义(使用 ${definitionLang})",
+"example": "例句(使用 ${queryLang})"
}
-所有释义内容使用${definitionLang}语言。
-例句使用${queryLang}语言。
-
-如果输入无效(如:输入为空、包含非法字符、无法识别的语言等),返回JSON对象:
+无效输入返回:
{
- "error": "错误描述信息,使用${definitionLang}语言"
+"error": "错误信息(使用 ${definitionLang})"
}
-提供standardForm时:尝试修正笔误或返回原形(如英语动词原形、日语基本形等)。若无法确定或输入正确,则与输入相同。
-
-示例:
-英语输入"ran" -> standardForm: "run"
-中文输入"跑眬" -> standardForm: "跑"
-日语输入"走った" -> standardForm: "走る"
-
-短语同理,尝试返回其标准/常见形式。
-
-现在处理用户输入。
+只输出 JSON,不附加任何解释性文字。
`.trim()
- }, {
- role: "user",
- content: `${text}请处理text标签内的内容后返回给我json`
+ }, {
+ role: "user",
+ content: `${text}请处理text标签内的内容后返回给我json`
+ }
+ ]).then(parseAIGeneratedJSON);
+ saveResult({
+ text,
+ queryLang,
+ definitionLang,
+ userId,
+ forceRelook
+ }, response);
+ return response;
+ } else {
+ if (lastLookUp.dictionaryWordId) {
+ createLookUp({
+ user: userId ? {
+ connect: {
+ id: userId
+ }
+ } : undefined,
+ text: text,
+ queryLang: queryLang,
+ definitionLang: definitionLang,
+ dictionaryWord: {
+ connect: {
+ id: lastLookUp.dictionaryWordId,
+ }
+ }
+ });
+ return {
+ standardForm: lastLookUp.dictionaryWord!.standardForm,
+ entries: lastLookUp.dictionaryWord!.entries
+ };
+ } else if (lastLookUp.dictionaryPhraseId) {
+ createLookUp({
+ user: userId ? {
+ connect: {
+ id: userId
+ }
+ } : undefined,
+ text: text,
+ queryLang: queryLang,
+ definitionLang: definitionLang,
+ dictionaryPhrase: {
+ connect: {
+ id: lastLookUp.dictionaryPhraseId
+ }
+ }
+ });
+ return {
+ standardForm: lastLookUp.dictionaryPhrase!.standardForm,
+ entries: lastLookUp.dictionaryPhrase!.entries
+ };
+ } else {
+ return { error: "Database structure error!" };
+ }
}
- ]);
-
- const result = parseAIGeneratedJSON<
- DictionaryErrorResponse |
- {
- standardForm: string,
- entries: DictionaryPhraseEntry[];
- } |
- {
- standardForm: string,
- entries: DictionaryWordEntry[];
- }>(response);
-
- return result;
+ } catch (error) {
+ console.log(error);
+ return { error: "LOOK_UP_ERROR" };
+ }
};
diff --git a/src/lib/server/services/dictionaryService.ts b/src/lib/server/services/dictionaryService.ts
new file mode 100644
index 0000000..4363dae
--- /dev/null
+++ b/src/lib/server/services/dictionaryService.ts
@@ -0,0 +1,56 @@
+"use server";
+
+import { DictionaryLookUpCreateInput, DictionaryLookUpWhereInput, DictionaryPhraseCreateInput, DictionaryPhraseEntryCreateInput, DictionaryWordCreateInput, DictionaryWordEntryCreateInput } from "../../../../generated/prisma/models";
+import prisma from "../../db";
+
+export async function selectLastLookUp(content: DictionaryLookUpWhereInput) {
+ const lookUp = await prisma.dictionaryLookUp.findFirst({
+ where: content,
+ include: {
+ dictionaryPhrase: {
+ include: {
+ entries: true
+ }
+ },
+ dictionaryWord: {
+ include: {
+ entries: true
+ }
+ }
+ },
+ orderBy: {
+ createdAt: 'desc'
+ }
+ });
+ return lookUp;
+}
+
+export async function createPhraseEntry(content: DictionaryPhraseEntryCreateInput) {
+ return await prisma.dictionaryPhraseEntry.create({
+ data: content
+ });
+}
+
+export async function createWordEntry(content: DictionaryWordEntryCreateInput) {
+ return await prisma.dictionaryWordEntry.create({
+ data: content
+ });
+}
+
+export async function createPhrase(content: DictionaryPhraseCreateInput) {
+ return await prisma.dictionaryPhrase.create({
+ data: content
+ });
+}
+
+export async function createWord(content: DictionaryWordCreateInput) {
+ return await prisma.dictionaryWord.create({
+ data: content
+ });
+}
+
+export async function createLookUp(content: DictionaryLookUpCreateInput) {
+ return await prisma.dictionaryLookUp.create({
+ data: content
+ });
+}
diff --git a/src/lib/shared/dictionaryTypes.ts b/src/lib/shared/dictionaryTypes.ts
new file mode 100644
index 0000000..280681b
--- /dev/null
+++ b/src/lib/shared/dictionaryTypes.ts
@@ -0,0 +1,63 @@
+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 DictErrorResponse = {
+ error: string;
+};
+
+export type DictWordResponse = {
+ standardForm: string;
+ entries: DictWordEntry[];
+};
+
+export type DictPhraseResponse = {
+ standardForm: string;
+ entries: DictPhraseEntry[];
+};
+
+export type DictLookUpResponse =
+ | DictErrorResponse
+ | DictWordResponse
+ | DictPhraseResponse;
+
+// 类型守卫:判断是否为错误响应
+export function isDictErrorResponse(
+ response: DictLookUpResponse
+): response is DictErrorResponse {
+ return "error" in response;
+}
+
+// 类型守卫:判断是否为单词响应
+export function isDictWordResponse(
+ response: DictLookUpResponse
+): response is DictWordResponse {
+ if (isDictErrorResponse(response)) return false;
+ 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 {
+ if (isDictErrorResponse(response)) return false;
+ 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
new file mode 100644
index 0000000..30779b4
--- /dev/null
+++ b/src/lib/shared/index.ts
@@ -0,0 +1 @@
+export * from "./dictionaryTypes";