Feat: 单次翻译质量优化提示

This commit is contained in:
奇趣保罗 2026-02-13 17:48:38 +08:00
parent 6cdc3699cf
commit 8335970e2e
3 changed files with 61 additions and 22 deletions

View File

@ -9,7 +9,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { toast } from "sonner";
import { ClipboardPaste } from "lucide-react"; import { ClipboardPaste } from "lucide-react";
type Props = { type Props = {

View File

@ -1,6 +1,7 @@
import { memo, useEffect, useState } from "react"; import { memo, useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
DropdownMenu, DropdownMenu,
@ -34,13 +35,26 @@ type Props = {
) => Promise<void> | void; ) => Promise<void> | void;
}; };
function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, model, initialSelectedLanguages, getExistingByPath, onConfirm }: Props) { function AiTranslateModalImpl({
open,
onOpenChange,
languages,
paths,
prompt,
model,
initialSelectedLanguages,
getExistingByPath,
onConfirm,
}: Props) {
const [inputsByKey, setInputsByKey] = useState<Record<string, string>>({}); const [inputsByKey, setInputsByKey] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedLangs, setSelectedLangs] = useState<string[]>([]); const [selectedLangs, setSelectedLangs] = useState<string[]>([]);
const [overwrite, setOverwrite] = useState(false); const [overwrite, setOverwrite] = useState(false);
const [existingCache, setExistingCache] = useState<Record<string, { lang: string; text: string }[]>>({}); const [contextHint, setContextHint] = useState("");
const [existingCache, setExistingCache] = useState<
Record<string, { lang: string; text: string }[]>
>({});
const MAX_ITEMS = 50; const MAX_ITEMS = 50;
const overLimit = paths.length > MAX_ITEMS; const overLimit = paths.length > MAX_ITEMS;
@ -51,12 +65,14 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
setInputsByKey(init); setInputsByKey(init);
// 初始化默认勾选语言 // 初始化默认勾选语言
const initSelected = (initialSelectedLanguages && initialSelectedLanguages.length > 0) const initSelected =
initialSelectedLanguages && initialSelectedLanguages.length > 0
? initialSelectedLanguages ? initialSelectedLanguages
: languages; : languages;
setSelectedLangs(initSelected); setSelectedLangs(initSelected);
setOverwrite(false); setOverwrite(false);
setContextHint("");
setExistingCache({}); setExistingCache({});
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -77,7 +93,10 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
setError(`一次最多支持 ${MAX_ITEMS}`); setError(`一次最多支持 ${MAX_ITEMS}`);
return; return;
} }
const items = paths.map((key) => ({ key, text: (inputsByKey[key] ?? "").trim() })); const items = paths.map((key) => ({
key,
text: (inputsByKey[key] ?? "").trim(),
}));
if (items.some((it) => !it.text)) { if (items.some((it) => !it.text)) {
setError("请为所有条目输入原文"); setError("请为所有条目输入原文");
return; return;
@ -92,9 +111,18 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
} }
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const result = await requestTranslations({ items, languages: selectedLangs, prompt, model }); const result = await requestTranslations({
items,
languages: selectedLangs,
prompt,
model,
contextHint: contextHint.trim() || undefined,
});
await onConfirm(result, { overwrite, selectedLanguages: selectedLangs }); await onConfirm(result, { overwrite, selectedLanguages: selectedLangs });
setInputsByKey({}); setInputsByKey({});
onOpenChange(false); onOpenChange(false);
} catch (e) { } catch (e) {
@ -158,9 +186,7 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
) : ( ) : (
(existingCache[p] || []).map((opt) => ( (existingCache[p] || []).map((opt) => (
<> <>
<DropdownMenuLabel> <DropdownMenuLabel>{opt.lang}</DropdownMenuLabel>
{opt.lang}
</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
key={`${p}:${opt.lang}`} key={`${p}:${opt.lang}`}
onClick={() => onClick={() =>
@ -220,6 +246,19 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
<span className="text-sm"></span> <span className="text-sm"></span>
</label> </label>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">
</div>
<Textarea
value={contextHint}
onChange={(e) => setContextHint(e.target.value)}
placeholder="例如:这是一个电商 App 的结算页面,「立即购买」希望翻译得更有行动号召力"
rows={2}
className="resize-none text-sm"
/>
</div>
{(error || overLimit) && ( {(error || overLimit) && (
<div className="text-sm text-red-600" role="alert"> <div className="text-sm text-red-600" role="alert">
{error} {error}
@ -249,5 +288,3 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
} }
export const AiTranslateModal = memo(AiTranslateModalImpl); export const AiTranslateModal = memo(AiTranslateModalImpl);

View File

@ -5,6 +5,7 @@ export type AiTranslateParams = {
languages: string[]; languages: string[];
model?: string; model?: string;
prompt?: string; prompt?: string;
contextHint?: string;
}; };
function getClient() { function getClient() {
@ -22,6 +23,7 @@ export async function requestTranslations({
languages, languages,
model, model,
prompt, prompt,
contextHint,
}: AiTranslateParams): Promise<Record<string, Record<string, string>>> { }: AiTranslateParams): Promise<Record<string, Record<string, string>>> {
const client = getClient(); const client = getClient();
const mdl = const mdl =
@ -38,6 +40,7 @@ export async function requestTranslations({
`请使用当地人的习惯用语,将多条原文同时翻译为这些目标语言:${targetList},注意英语的首字母务必是大写。`, `请使用当地人的习惯用语,将多条原文同时翻译为这些目标语言:${targetList},注意英语的首字母务必是大写。`,
"翻译偏好:", "翻译偏好:",
prompt, prompt,
...(contextHint ? ["用户补充的语境与翻译期望:", contextHint] : []),
"严格要求:", "严格要求:",
"- 仅返回一个 JSON 对象,不包含任何其他内容(例如说明、代码块标记、注释)。", "- 仅返回一个 JSON 对象,不包含任何其他内容(例如说明、代码块标记、注释)。",
"- JSON 的顶层键必须严格等于目标语言代码;其值必须是一个对象,键为条目 key值为该 key 的翻译纯文本。", "- JSON 的顶层键必须严格等于目标语言代码;其值必须是一个对象,键为条目 key值为该 key 的翻译纯文本。",