Feat: 单次翻译质量优化提示
This commit is contained in:
parent
6cdc3699cf
commit
8335970e2e
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 的翻译纯文本。",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue