113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import { memo, useState } from "react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { requestTranslations } from "@/lib/ai";
|
||
|
||
type Props = {
|
||
open: boolean;
|
||
onOpenChange: (next: boolean) => void;
|
||
languages: string[];
|
||
path: string;
|
||
prompt: string | undefined;
|
||
onConfirm: (result: Record<string, string>) => Promise<void> | void;
|
||
};
|
||
|
||
function AiTranslateModalImpl({ open, onOpenChange, languages, path, prompt, onConfirm }: Props) {
|
||
const [text, setText] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
async function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
const payload = text.trim();
|
||
if (!payload) {
|
||
setError("请输入待翻译文本");
|
||
return;
|
||
}
|
||
if (languages.length === 0) {
|
||
setError("当前项目暂无目标语言");
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const result = await requestTranslations({ text: payload, languages, prompt });
|
||
// 二次校验 keys 完整性
|
||
for (const l of languages) {
|
||
if (!Object.prototype.hasOwnProperty.call(result, l)) {
|
||
throw new Error("AI 返回的 key 不完整");
|
||
}
|
||
}
|
||
await onConfirm(result);
|
||
setText("");
|
||
onOpenChange(false);
|
||
} catch (e) {
|
||
setError((e as Error)?.message ?? "AI 翻译失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog
|
||
open={open}
|
||
onOpenChange={(v) => {
|
||
if (!loading) {
|
||
onOpenChange(v);
|
||
if (!v) setError(null);
|
||
}
|
||
}}
|
||
>
|
||
<DialogContent className="max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle>{`AI 翻译 — ${path || "条目"}`}</DialogTitle>
|
||
</DialogHeader>
|
||
<form onSubmit={handleSubmit} className="space-y-3">
|
||
<div>
|
||
<label className="text-sm text-muted-foreground">待翻译文本</label>
|
||
<Input
|
||
value={text}
|
||
onChange={(e) => setText(e.target.value)}
|
||
placeholder="请输入原文本"
|
||
aria-label="待翻译文本"
|
||
/>
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
目标语言:{languages.join(", ") || "无"}
|
||
</div>
|
||
{error && (
|
||
<div className="text-sm text-red-600" role="alert">{error}</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => {
|
||
onOpenChange(false);
|
||
setError(null);
|
||
}}
|
||
disabled={loading}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button type="submit" disabled={loading}>
|
||
{loading ? "翻译中..." : "生成翻译"}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|
||
|
||
export const AiTranslateModal = memo(AiTranslateModalImpl);
|
||
|
||
|