diff --git a/package.json b/package.json index 1de5c7d..e6bcbaa 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.16", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aa6584..0b2c207 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.2.2)(react@19.2.0) @@ -642,6 +645,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -655,6 +671,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -664,6 +693,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -2303,6 +2341,15 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2320,6 +2367,15 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) @@ -2327,6 +2383,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-slot@1.2.4(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.2.0)': dependencies: react: 19.2.0 diff --git a/src/components/biz/ai-translate-modal.tsx b/src/components/biz/ai-translate-modal.tsx index b18a502..fd706ec 100644 --- a/src/components/biz/ai-translate-modal.tsx +++ b/src/components/biz/ai-translate-modal.tsx @@ -1,6 +1,14 @@ 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, @@ -9,6 +17,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { requestTranslations } from "@/lib/ai"; +import { ButtonGroup } from "../ui/button-group"; type Props = { open: boolean; @@ -16,13 +25,21 @@ type Props = { languages: string[]; paths: string[]; prompt: string | undefined; - onConfirm: (result: Record>) => Promise | void; + initialSelectedLanguages?: string[]; + getExistingByPath?: (path: string) => Record; + onConfirm: ( + result: Record>, + options: { overwrite: boolean; selectedLanguages: string[] } + ) => Promise | void; }; -function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, onConfirm }: Props) { +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; @@ -31,10 +48,28 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, on 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) { @@ -50,11 +85,15 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, on setError("当前项目暂无目标语言"); return; } + if (selectedLangs.length === 0) { + setError("请选择目标语言"); + return; + } setLoading(true); setError(null); try { - const result = await requestTranslations({ items, languages, prompt }); - await onConfirm(result); + const result = await requestTranslations({ items, languages: selectedLangs, prompt }); + await onConfirm(result, { overwrite, selectedLanguages: selectedLangs }); setInputsByKey({}); onOpenChange(false); } catch (e) { @@ -78,27 +117,114 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, on {`AI 翻译 — 已选 ${paths.length} 条`} -
+
{paths.map((p) => (
-
{p}
- setInputsByKey((old) => ({ ...old, [p]: e.target.value }))} - placeholder="请输入原文本" - aria-label={`原文 ${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.join(", ") || "无"} +
+
+ 请选择目标语言: +
+
+ {languages.map((lang) => { + const checked = selectedLangs.includes(lang); + + return ( + + ); + })} +
+ + {(error || overLimit) && ( -
{error}
+
+ {error} +
)} +