import { memo, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { requestTranslations } from "@/lib/ai"; import { ButtonGroup } from "../ui/button-group"; type Props = { open: boolean; onOpenChange: (next: boolean) => void; languages: string[]; paths: string[]; prompt: string | undefined; initialSelectedLanguages?: string[]; getExistingByPath?: (path: string) => Record; onConfirm: ( result: Record>, options: { overwrite: boolean; selectedLanguages: string[] } ) => Promise | void; }; function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, initialSelectedLanguages, getExistingByPath, onConfirm }: Props) { const [inputsByKey, setInputsByKey] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedLangs, setSelectedLangs] = useState([]); const [overwrite, setOverwrite] = useState(false); const [existingCache, setExistingCache] = useState>({}); const MAX_ITEMS = 50; const overLimit = paths.length > MAX_ITEMS; useEffect(() => { if (open) { const init: Record = {}; for (const p of paths) init[p] = inputsByKey[p] ?? ""; setInputsByKey(init); // 初始化默认勾选语言 const initSelected = (initialSelectedLanguages && initialSelectedLanguages.length > 0) ? initialSelectedLanguages : languages; setSelectedLangs(initSelected); setOverwrite(false); setExistingCache({}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, paths.join("|")]); function loadExistingForPath(path: string) { if (existingCache[path]) return; const map = getExistingByPath?.(path) ?? {}; const arr = Object.entries(map) .map(([lang, text]) => ({ lang, text })) .filter((it) => typeof it.text === "string" && it.text.trim() !== ""); setExistingCache((old) => ({ ...old, [path]: arr })); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (overLimit) { setError(`一次最多支持 ${MAX_ITEMS} 条`); return; } const items = paths.map((key) => ({ key, text: (inputsByKey[key] ?? "").trim() })); if (items.some((it) => !it.text)) { setError("请为所有条目输入原文"); return; } if (languages.length === 0) { setError("当前项目暂无目标语言"); return; } if (selectedLangs.length === 0) { setError("请选择目标语言"); return; } setLoading(true); setError(null); try { const result = await requestTranslations({ items, languages: selectedLangs, prompt }); await onConfirm(result, { overwrite, selectedLanguages: selectedLangs }); setInputsByKey({}); onOpenChange(false); } catch (e) { setError((e as Error)?.message ?? "AI 翻译失败"); } finally { setLoading(false); } } return ( { if (!loading) { onOpenChange(v); if (!v) setError(null); } }} > {`AI 翻译 — 已选 ${paths.length} 条`}
{paths.map((p) => (
{p}
setInputsByKey((old) => ({ ...old, [p]: e.target.value })) } placeholder="请输入原文本" aria-label={`原文 ${p}`} /> { if (open) loadExistingForPath(p); }} > {(existingCache[p]?.length ?? 0) === 0 ? ( 无可用内容 ) : ( (existingCache[p] || []).map((opt) => ( <> {opt.lang} setInputsByKey((old) => ({ ...old, [p]: opt.text, })) } title={opt.text} > {opt.text} )) )}
))}
请选择目标语言:
{languages.map((lang) => { const checked = selectedLangs.includes(lang); return ( ); })}
{(error || overLimit) && (
{error}
)}
); } export const AiTranslateModal = memo(AiTranslateModalImpl);