before refractor

This commit is contained in:
2026-01-05 16:55:34 +08:00
parent 3bc804c5e8
commit bd7eca1bd0
14 changed files with 1062 additions and 396 deletions

View File

@@ -0,0 +1,96 @@
"use client";
import { LightButton } from "@/components/ui/buttons";
import Container from "@/components/ui/Container";
import { useEffect, useState } from "react";
import { Folder } from "../../../../generated/prisma/browser";
import { getFoldersByUserId } from "@/lib/server/services/folderService";
import { Folder as Fd } from "lucide-react";
import { createPair } from "@/lib/server/services/pairService";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
interface AddToFolderProps {
definitionLang: string;
queryLang: string;
standardForm: string;
definition: string;
ipa?: string;
setShow: (show: boolean) => void;
}
const AddToFolder: React.FC<AddToFolderProps> = ({
definitionLang,
queryLang,
standardForm,
definition,
ipa,
setShow,
}) => {
const { data: session } = authClient.useSession();
const [folders, setFolders] = useState<Folder[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!session) return;
const userId = session.user.id as string;
getFoldersByUserId(userId)
.then(setFolders)
.then(() => setLoading(false));
}, [session]);
if (!session) {
return null;
}
return (
<div className="fixed left-0 top-0 z-50 w-screen h-screen bg-black/50 flex justify-center items-center">
<Container className="p-6">
<h1 className="text-xl font-bold mb-4"></h1>
<div className="border border-gray-200 rounded-2xl">
{loading ? (
<span>...</span>
) : folders.length > 0 ? (
folders.map((folder) => (
<button
key={folder.id}
className="p-2 flex items-center justify-start hover:bg-gray-50 gap-2 hover:cursor-pointer w-full border-b border-gray-200"
onClick={() => {
createPair({
text1: standardForm,
text2: definition,
language1: queryLang,
language2: definitionLang,
ipa1: ipa || undefined,
folder: {
connect: {
id: folder.id,
},
},
})
.then(() => {
toast.success(`已保存到文件夹:${folder.name}`);
setShow(false);
})
.catch(() => {
toast.error("保存失败,请稍后重试");
});
}}
>
<Fd />
{folder.name}
</button>
))
) : (
<div className="p-4 text-gray-500"></div>
)}
</div>
<div className="mt-4 flex justify-end gap-2">
<LightButton onClick={() => setShow(false)}></LightButton>
</div>
</Container>
</div>
);
};
export default AddToFolder;

View File

@@ -1,321 +1,398 @@
"use client";
import { useState } from "react";
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: "西班牙语" },
{ 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;
ipa: string;
definition: string;
partOfSpeech: string;
example: string;
};
type DictionaryPhraseEntry = {
definition: string;
example: string;
definition: string;
example: string;
};
type DictionaryErrorResponse = {
error: string;
error: string;
};
type DictionarySuccessResponse = {
standardForm: string;
entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
standardForm: string;
entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
};
type DictionaryResponse = DictionarySuccessResponse | DictionaryErrorResponse;
// 类型守卫:判断是否为单词条目
function isWordEntry(entry: DictionaryWordEntry | DictionaryPhraseEntry): entry is DictionaryWordEntry {
return "ipa" in entry && "partOfSpeech" in entry;
return "ipa" in entry && "partOfSpeech" in entry;
}
// 类型守卫:判断是否为错误响应
function isErrorResponse(response: DictionaryResponse): response is DictionaryErrorResponse {
return "error" in response;
return "error" in response;
}
export default function Dictionary() {
const [searchQuery, setSearchQuery] = useState("");
const [searchResult, setSearchResult] = useState<DictionaryResponse | null>(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 [searchQuery, setSearchQuery] = useState("");
const [searchResult, setSearchResult] = useState<DictionaryResponse | null>(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<number | null>(null);
const [folders, setFolders] = useState<Folder[]>([]);
const { data: session } = authClient.useSession();
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!searchQuery.trim()) return;
// 加载用户的文件夹列表
useEffect(() => {
if (session) {
getFoldersByUserId(session.user.id as string)
.then((loadedFolders) => {
setFolders(loadedFolders);
// 如果有文件夹且未选择,默认选择第一个
if (loadedFolders.length > 0 && !selectedFolderId) {
setSelectedFolderId(loadedFolders[0].id);
}
});
}
}, [session, selectedFolderId]);
setIsSearching(true);
setHasSearched(true);
setSearchResult(null);
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!searchQuery.trim()) return;
try {
// 使用查询语言和释义语言
const result = await lookUp(searchQuery, queryLang, definitionLang);
// 检查是否为错误响应
if (isErrorResponse(result)) {
toast.error(result.error);
setIsSearching(true);
setHasSearched(true);
setSearchResult(null);
} else {
setSearchResult(result);
}
} catch (error) {
console.error("词典查询失败:", error);
toast.error("查询失败,请稍后重试");
setSearchResult(null);
} finally {
setIsSearching(false);
}
};
return (
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
{/* 搜索区域 */}
<div className="flex items-center justify-center px-4 py-12">
<Container className="max-w-3xl w-full p-4">
{/* 页面标题 */}
<div className="text-center mb-8">
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
</h1>
<p className="text-gray-700 text-lg">
</p>
</div>
try {
// 使用查询语言和释义语言
const result = await lookUp(searchQuery, queryLang, definitionLang);
{/* 搜索表单 */}
<form onSubmit={handleSearch} className="flex gap-2">
<input
type="text"
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
placeholder="输入要查询的单词或短语..."
className="flex-1 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
/>
<LightButton
type="submit"
disabled={isSearching || !searchQuery.trim()}
className="px-6 py-3"
>
{isSearching ? "查询中..." : "查询"}
</LightButton>
</form>
// 检查是否为错误响应
if (isErrorResponse(result)) {
toast.error(result.error);
setSearchResult(null);
} else {
setSearchResult(result);
}
} catch (error) {
console.error("词典查询失败:", error);
toast.error("查询失败,请稍后重试");
setSearchResult(null);
} finally {
setIsSearching(false);
}
};
{/* 语言设置 */}
<div className="mt-4 bg-white/20 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-gray-800 font-semibold"></span>
<LightButton
onClick={() => setShowLangSettings(!showLangSettings)}
className="text-sm px-4 py-2"
>
{showLangSettings ? "收起" : "展开"}
</LightButton>
</div>
{showLangSettings && (
<div className="space-y-4">
{/* 查询语言 */}
<div>
<label className="block text-gray-700 text-sm mb-2">
(/)
</label>
<div className="flex flex-wrap gap-2 mb-2">
{POPULAR_LANGUAGES.map((lang) => (
<LightButton
key={lang.code}
selected={queryLang === lang.code}
onClick={() => setQueryLang(lang.code)}
className="text-sm px-3 py-1"
>
{lang.name}
</LightButton>
))}
</div>
<input
type="text"
value={queryLang}
onChange={(e) => 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"
/>
</div>
{/* 释义语言 */}
<div>
<label className="block text-gray-700 text-sm mb-2">
()
</label>
<div className="flex flex-wrap gap-2 mb-2">
{POPULAR_LANGUAGES.map((lang) => (
<LightButton
key={lang.code}
selected={definitionLang === lang.code}
onClick={() => setDefinitionLang(lang.code)}
className="text-sm px-3 py-1"
>
{lang.name}
</LightButton>
))}
</div>
<input
type="text"
value={definitionLang}
onChange={(e) => 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"
/>
</div>
{/* 当前设置显示 */}
<div className="text-center text-gray-700 text-sm pt-2 border-t border-gray-300">
<span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}</span>
<span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}</span>
</div>
</div>
)}
</div>
{/* 搜索提示 */}
<div className="mt-4 text-center text-gray-700 text-sm">
<p>hello, look up, dictionary</p>
</div>
</Container>
</div>
{/* 搜索结果区域 */}
<div className="flex-1 px-4 pb-12">
<Container className="max-w-3xl w-full p-4">
{isSearching && (
<div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
<p className="mt-4 text-white">...</p>
</div>
)}
{!isSearching && hasSearched && !searchResult && (
<div className="text-center py-12 bg-white/20 rounded-lg">
<p className="text-gray-800 text-xl"></p>
<p className="text-gray-600 mt-2"></p>
</div>
)}
{!isSearching && searchResult && !isErrorResponse(searchResult) && (
<div className="space-y-6">
<div className="bg-white rounded-lg p-6 shadow-lg">
{/* 标准形式标题 */}
<div className="mb-6">
<h2 className="text-3xl font-bold text-gray-800 mb-2">
{searchResult.standardForm}
</h2>
{searchResult.standardForm !== searchQuery && (
<p className="text-gray-500 text-sm">
: {searchQuery}
</p>
)}
</div>
{/* 条目列表 */}
<div className="space-y-6">
{searchResult.entries.map((entry, index) => (
<div key={index} className="border-t border-gray-200 pt-4">
{isWordEntry(entry) ? (
// 单词条目
<div>
{/* 音标和词性 */}
<div className="flex items-center gap-3 mb-3">
{entry.ipa && (
<span className="text-gray-600 text-lg">
{entry.ipa}
</span>
)}
{entry.partOfSpeech && (
<span className="px-3 py-1 bg-[#35786f] text-white text-sm rounded-full">
{entry.partOfSpeech}
</span>
)}
</div>
{/* 释义 */}
<div className="mb-3">
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-800">{entry.definition}</p>
</div>
{/* 例句 */}
{entry.example && (
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
{entry.example}
</p>
</div>
)}
</div>
) : (
// 短语条目
<div>
{/* 释义 */}
<div className="mb-3">
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-800">{entry.definition}</p>
</div>
{/* 例句 */}
{entry.example && (
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
{entry.example}
</p>
</div>
)}
</div>
)}
return (
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
{/* 搜索区域 */}
<div className="flex items-center justify-center px-4 py-12">
<Container className="max-w-3xl w-full p-4">
{/* 页面标题 */}
<div className="text-center mb-8">
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
</h1>
<p className="text-gray-700 text-lg">
</p>
</div>
))}
</div>
</div>
</div>
)}
{!hasSearched && (
<div className="text-center py-12 bg-white/20 rounded-lg">
<div className="text-6xl mb-4">📚</div>
<p className="text-gray-800 text-xl mb-2">使</p>
<p className="text-gray-600"></p>
{/* 搜索表单 */}
<form onSubmit={handleSearch} className="flex gap-2">
<input
type="text"
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
placeholder="输入要查询的单词或短语..."
className="flex-1 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
/>
<LightButton
type="submit"
disabled={isSearching || !searchQuery.trim()}
className="px-6 py-3"
>
{isSearching ? "查询中..." : "查询"}
</LightButton>
</form>
{/* 语言设置 */}
<div className="mt-4 bg-white/20 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-gray-800 font-semibold"></span>
<LightButton
onClick={() => setShowLangSettings(!showLangSettings)}
className="text-sm px-4 py-2"
>
{showLangSettings ? "收起" : "展开"}
</LightButton>
</div>
{showLangSettings && (
<div className="space-y-4">
{/* 查询语言 */}
<div>
<label className="block text-gray-700 text-sm mb-2">
(/)
</label>
<div className="flex flex-wrap gap-2 mb-2">
{POPULAR_LANGUAGES.map((lang) => (
<LightButton
key={lang.code}
selected={queryLang === lang.code}
onClick={() => setQueryLang(lang.code)}
className="text-sm px-3 py-1"
>
{lang.name}
</LightButton>
))}
</div>
<input
type="text"
value={queryLang}
onChange={(e) => 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"
/>
</div>
{/* 释义语言 */}
<div>
<label className="block text-gray-700 text-sm mb-2">
()
</label>
<div className="flex flex-wrap gap-2 mb-2">
{POPULAR_LANGUAGES.map((lang) => (
<LightButton
key={lang.code}
selected={definitionLang === lang.code}
onClick={() => setDefinitionLang(lang.code)}
className="text-sm px-3 py-1"
>
{lang.name}
</LightButton>
))}
</div>
<input
type="text"
value={definitionLang}
onChange={(e) => 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"
/>
</div>
{/* 当前设置显示 */}
<div className="text-center text-gray-700 text-sm pt-2 border-t border-gray-300">
<span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}</span>
<span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}</span>
</div>
</div>
)}
</div>
{/* 搜索提示 */}
<div className="mt-4 text-center text-gray-700 text-sm">
<p>hello, look up, dictionary</p>
</div>
</Container>
</div>
)}
</Container>
</div>
</div>
);
{/* 搜索结果区域 */}
<div className="flex-1 px-4 pb-12">
<Container className="max-w-3xl w-full p-4">
{isSearching && (
<div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
<p className="mt-4 text-white">...</p>
</div>
)}
{!isSearching && hasSearched && !searchResult && (
<div className="text-center py-12 bg-white/20 rounded-lg">
<p className="text-gray-800 text-xl"></p>
<p className="text-gray-600 mt-2"></p>
</div>
)}
{!isSearching && searchResult && !isErrorResponse(searchResult) && (
<div className="space-y-6">
<div className="bg-white rounded-lg p-6 shadow-lg">
{/* 标题和保存按钮 */}
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<h2 className="text-3xl font-bold text-gray-800 mb-2">
{searchResult.standardForm}
</h2>
{searchResult.standardForm !== searchQuery && (
<p className="text-gray-500 text-sm">
: {searchQuery}
</p>
)}
</div>
<div className="flex items-center gap-2 ml-4">
{session && folders.length > 0 && (
<select
value={selectedFolderId || ""}
onChange={(e) => setSelectedFolderId(e.target.value ? Number(e.target.value) : null)}
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[#35786f]"
>
{folders.map((folder) => (
<option key={folder.id} value={folder.id}>
{folder.name}
</option>
))}
</select>
)}
<button
onClick={() => {
if (!session) {
toast.error("请先登录");
return;
}
if (!selectedFolderId) {
toast.error("请先创建文件夹");
return;
}
if (!searchResult || isErrorResponse(searchResult)) return;
const entry = searchResult.entries[0];
createPair({
text1: searchResult.standardForm,
text2: entry.definition,
language1: queryLang,
language2: definitionLang,
ipa1: isWordEntry(entry) ? entry.ipa : undefined,
folder: {
connect: {
id: selectedFolderId,
},
},
})
.then(() => {
const folderName = folders.find(f => f.id === selectedFolderId)?.name;
toast.success(`已保存到文件夹:${folderName}`);
})
.catch(() => {
toast.error("保存失败,请稍后重试");
});
}}
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-10 h-10 flex justify-center items-center flex-shrink-0"
title="保存到文件夹"
>
<Plus />
</button>
</div>
</div>
{/* 条目列表 */}
<div className="space-y-6">
{searchResult.entries.map((entry, index) => (
<div key={index} className="border-t border-gray-200 pt-4">
{isWordEntry(entry) ? (
// 单词条目
<div>
{/* 音标和词性 */}
<div className="flex items-center gap-3 mb-3">
{entry.ipa && (
<span className="text-gray-600 text-lg">
{entry.ipa}
</span>
)}
{entry.partOfSpeech && (
<span className="px-3 py-1 bg-[#35786f] text-white text-sm rounded-full">
{entry.partOfSpeech}
</span>
)}
</div>
{/* 释义 */}
<div className="mb-3">
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-800">{entry.definition}</p>
</div>
{/* 例句 */}
{entry.example && (
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
{entry.example}
</p>
</div>
)}
</div>
) : (
// 短语条目
<div>
{/* 释义 */}
<div className="mb-3">
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-800">{entry.definition}</p>
</div>
{/* 例句 */}
{entry.example && (
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-1">
</h3>
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
{entry.example}
</p>
</div>
)}
</div>
)}
</div>
))}
</div>
</div>
</div>
)}
{!hasSearched && (
<div className="text-center py-12 bg-white/20 rounded-lg">
<div className="text-6xl mb-4">📚</div>
<p className="text-gray-800 text-xl mb-2">使</p>
<p className="text-gray-600"></p>
</div>
)}
</Container>
</div>
</div>
);
}

View File

@@ -15,10 +15,10 @@ import SaveList from "./SaveList";
import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl";
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
import { getTTSAudioUrl } from "@/lib/browser/tts";
import { genIPA, genLocale } from "@/lib/server/bigmodel/translatorActions";
import { genIPA, genLanguage } from "@/lib/server/bigmodel/translatorActions";
import { logger } from "@/lib/logger";
import PageLayout from "@/components/ui/PageLayout";
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
export default function TextSpeakerPage() {
const t = useTranslations("text_speaker");
@@ -31,7 +31,7 @@ export default function TextSpeakerPage() {
const [pause, setPause] = useState(true);
const [autopause, setAutopause] = useState(true);
const textRef = useRef("");
const [locale, setLocale] = useState<string | null>(null);
const [language, setLanguage] = useState<string | null>(null);
const [ipa, setIPA] = useState<string>("");
const objurlRef = useRef<string | null>(null);
const [processing, setProcessing] = useState(false);
@@ -95,38 +95,35 @@ export default function TextSpeakerPage() {
} else {
// 第一次播放
try {
let theLocale = locale;
if (!theLocale) {
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
setLocale(tmp_locale);
theLocale = tmp_locale;
let theLanguage = language;
if (!theLanguage) {
const tmp_language = await genLanguage(textRef.current.slice(0, 30));
setLanguage(tmp_language);
theLanguage = tmp_language;
}
const voice = VOICES.find((v) => v.locale.startsWith(theLocale));
if (!voice) throw "Voice not found.";
theLanguage = theLanguage.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase());
objurlRef.current = await getTTSAudioUrl(
// 检查语言是否在 TTS 支持列表中
const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [
"Auto", "Chinese", "English", "German", "Italian", "Portuguese",
"Spanish", "Japanese", "Korean", "French", "Russian"
];
if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) {
theLanguage = "Auto";
}
objurlRef.current = await getTTSUrl(
textRef.current,
voice.short_name,
(() => {
if (speed === 1) return {};
else if (speed < 1)
return {
rate: `-${100 - speed * 100}%`,
};
else
return {
rate: `+${speed * 100 - 100}%`,
};
})(),
theLanguage as TTS_SUPPORTED_LANGUAGES
);
load(objurlRef.current);
play();
} catch (e) {
logger.error("播放音频失败", e);
setPause(true);
setLocale(null);
setLanguage(null);
setProcessing(false);
}
}
@@ -142,7 +139,7 @@ export default function TextSpeakerPage() {
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
textRef.current = e.target.value.trim();
setLocale(null);
setLanguage(null);
setIPA("");
if (objurlRef.current) URL.revokeObjectURL(objurlRef.current);
objurlRef.current = null;
@@ -163,7 +160,7 @@ export default function TextSpeakerPage() {
const handleUseItem = (item: z.infer<typeof TextSpeakerItemSchema>) => {
if (textareaRef.current) textareaRef.current.value = item.text;
textRef.current = item.text;
setLocale(item.locale);
setLanguage(item.locale);
setIPA(item.ipa || "");
if (objurlRef.current) URL.revokeObjectURL(objurlRef.current);
objurlRef.current = null;
@@ -178,11 +175,11 @@ export default function TextSpeakerPage() {
setSaving(true);
try {
let theLocale = locale;
if (!theLocale) {
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
setLocale(tmp_locale);
theLocale = tmp_locale;
let theLanguage = language;
if (!theLanguage) {
const tmp_language = await genLanguage(textRef.current.slice(0, 30));
setLanguage(tmp_language);
theLanguage = tmp_language;
}
let theIPA = ipa;
@@ -205,19 +202,19 @@ export default function TextSpeakerPage() {
} else if (theIPA.length === 0) {
save.push({
text: textRef.current,
locale: theLocale,
locale: theLanguage as string,
});
} else {
save.push({
text: textRef.current,
locale: theLocale,
locale: theLanguage as string,
ipa: theIPA,
});
}
setIntoLocalStorage(save);
} catch (e) {
logger.error("保存到本地存储失败", e);
setLocale(null);
setLanguage(null);
} finally {
setSaving(false);
}

View File

@@ -7,7 +7,6 @@ import { VOICES } from "@/config/locales";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { TranslationHistorySchema } from "@/lib/interfaces";
import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators";
import { getTTSAudioUrl } from "@/lib/browser/tts";
import { logger } from "@/lib/logger";
import { Plus, Trash } from "lucide-react";
import { useTranslations } from "next-intl";
@@ -24,6 +23,7 @@ import FolderSelector from "./FolderSelector";
import { createPair } from "@/lib/server/services/pairService";
import { shallowEqual } from "@/lib/utils";
import { authClient } from "@/lib/auth-client";
import { getTTSUrl } from "@/lib/server/bigmodel/tts";
export default function TranslatorPage() {
const t = useTranslations("translator");
@@ -50,13 +50,8 @@ export default function TranslatorPage() {
const tts = async (text: string, locale: string) => {
if (lastTTS.current.text !== text) {
const shortName = VOICES.find((v) => v.locale === locale)?.short_name;
if (!shortName) {
toast.error("Voice not found");
return;
}
try {
const url = await getTTSAudioUrl(text, shortName);
const url = await getTTSUrl(text, locale);
await load(url);
lastTTS.current.text = text;
lastTTS.current.url = url;

View File

@@ -5,7 +5,7 @@ import AuthForm from "./AuthForm";
export default async function AuthPage(
props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined; }>
searchParams: Promise<{ [key: string]: string | string[] | undefined; }>;
}
) {
const searchParams = await props.searchParams;

View File

@@ -21,8 +21,8 @@ export interface TextPair {
id: number;
text1: string;
text2: string;
locale1: string;
locale2: string;
language1: string;
language2: string;
}
export default function InFolder({ folderId }: { folderId: number }) {
@@ -146,8 +146,8 @@ export default function InFolder({ folderId }: { folderId: number }) {
await createPair({
text1: text1,
text2: text2,
locale1: locale1,
locale2: locale2,
language1: locale1,
language2: locale2,
folder: {
connect: {
id: folderId,

View File

@@ -25,11 +25,11 @@ export default function TextPairCard({
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="px-2 py-1 bg-gray-100 rounded-md">
{textPair.locale1.toUpperCase()}
{textPair.language1.toUpperCase()}
</span>
<span></span>
<span className="px-2 py-1 bg-gray-100 rounded-md">
{textPair.locale2.toUpperCase()}
{textPair.language2.toUpperCase()}
</span>
</div>

View File

@@ -23,8 +23,8 @@ export default function UpdateTextPairModal({
const t = useTranslations("folder_id");
const input1Ref = useRef<HTMLInputElement>(null);
const input2Ref = useRef<HTMLInputElement>(null);
const [locale1, setLocale1] = useState(textPair.locale1);
const [locale2, setLocale2] = useState(textPair.locale2);
const [locale1, setLocale1] = useState(textPair.language1);
const [locale2, setLocale2] = useState(textPair.language2);
if (!isOpen) return null;