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,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { ClipboardPaste } from "lucide-react";
type Props = {

View File

@ -1,6 +1,7 @@
import { memo, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
@ -34,13 +35,26 @@ type Props = {
) => 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 [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedLangs, setSelectedLangs] = useState<string[]>([]);
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 overLimit = paths.length > MAX_ITEMS;
@ -51,12 +65,14 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
setInputsByKey(init);
// 初始化默认勾选语言
const initSelected = (initialSelectedLanguages && initialSelectedLanguages.length > 0)
? initialSelectedLanguages
: languages;
const initSelected =
initialSelectedLanguages && initialSelectedLanguages.length > 0
? initialSelectedLanguages
: languages;
setSelectedLangs(initSelected);
setOverwrite(false);
setContextHint("");
setExistingCache({});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -77,7 +93,10 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
setError(`一次最多支持 ${MAX_ITEMS}`);
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)) {
setError("请为所有条目输入原文");
return;
@ -92,9 +111,18 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
}
setLoading(true);
setError(null);
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 });
setInputsByKey({});
onOpenChange(false);
} catch (e) {
@ -158,9 +186,7 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
) : (
(existingCache[p] || []).map((opt) => (
<>
<DropdownMenuLabel>
{opt.lang}
</DropdownMenuLabel>
<DropdownMenuLabel>{opt.lang}</DropdownMenuLabel>
<DropdownMenuItem
key={`${p}:${opt.lang}`}
onClick={() =>
@ -220,6 +246,19 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
<span className="text-sm"></span>
</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) && (
<div className="text-sm text-red-600" role="alert">
{error}
@ -249,5 +288,3 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, mo
}
export const AiTranslateModal = memo(AiTranslateModalImpl);

View File

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