Files
recipe_tool/app/settings/ai/page.tsx
T
hanhan a249fb038c feat: add recipe translation tool with AI translation and settings management
- Add Word document parsing and translation export
- Add database configuration management
- Add AI/LLM translation configuration
- Add translation vocabulary management
- Add export settings (colors, page orientation, headers)
- Separate database and AI configurations
- Add debounced search to prevent database overload
2026-04-12 23:50:54 +08:00

176 lines
6.3 KiB
TypeScript

"use client";
import {useEffect, useState} from "react";
import {Loader2} from "lucide-react";
import {Button} from "@/components/ui/button";
interface AIConfig {
apiKey: string;
baseUrl: string;
model: string;
}
const defaultAIConfig: AIConfig = {
apiKey: "",
baseUrl: "https://api.openai.com/v1",
model: "gpt-3.5-turbo",
};
export default function AISettingsPage() {
const [config, setConfig] = useState<AIConfig>(defaultAIConfig);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<"success" | "error" | null>(null);
// 加载配置
useEffect(() => {
fetch("/api/config")
.then((res) => res.json())
.then((result) => {
if (result.success && result.data) {
setConfig({
apiKey: result.data.ai?.apiKey || defaultAIConfig.apiKey,
baseUrl: result.data.ai?.baseUrl || defaultAIConfig.baseUrl,
model: result.data.ai?.model || defaultAIConfig.model,
});
}
})
.catch(console.error)
.finally(() => setLoading(false));
}, []);
// 保存配置
const handleSave = async () => {
setSaving(true);
try {
const response = await fetch("/api/config", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ai: config}),
});
const result = await response.json();
if (result.success) {
setTestResult("success");
setTimeout(() => setTestResult(null), 3000);
} else {
setTestResult("error");
}
} catch (error) {
console.error("Failed to save config:", error);
setTestResult("error");
} finally {
setSaving(false);
}
};
// 测试 AI 连接
const handleTestAI = async () => {
if (!config.apiKey) return;
// 先保存配置
await handleSave();
setTesting(true);
setTestResult(null);
try {
const response = await fetch(`/api/ai/translate?word=${encodeURIComponent("早餐")}`);
const result = await response.json();
if (result.english) {
setTestResult("success");
} else {
setTestResult("error");
}
} catch {
setTestResult("error");
} finally {
setTesting(false);
}
};
if (loading) {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold">大模型设置</h2>
<p className="text-sm text-muted-foreground">配置 AI 翻译服务</p>
</div>
<div className="h-40 animate-pulse rounded-lg bg-muted"/>
</div>
);
}
return (
<div className="space-y-6">
<div className={"flex items-center justify-between"}>
<div>
<h2 className="text-lg font-semibold">大模型设置</h2>
<p className="text-sm text-muted-foreground">配置 AI 翻译服务</p>
</div>
<div className={"flex items-center justify-between gap-2"}>
<div className="flex items-center gap-4">
<Button onClick={handleTestAI} disabled={testing || saving || !config.apiKey}>
{testing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin"/>
测试中...
</>
) : (
"测试并保存"
)}
</Button>
{testResult === "success" && (
<span className="text-sm text-green-600">配置成功!</span>
)}
{testResult === "error" && (
<span className="text-sm text-destructive">配置失败,请检查 API Key</span>
)}
</div>
<div className="flex gap-3">
<Button onClick={handleSave} disabled={saving}>
{saving ? "保存中..." : "保存配置"}
</Button>
</div>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium">API Key</label>
<input
type="password"
value={config.apiKey}
onChange={(e) => setConfig({...config, apiKey: e.target.value})}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
placeholder="sk-..."
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Base URL</label>
<input
type="text"
value={config.baseUrl}
onChange={(e) => setConfig({...config, baseUrl: e.target.value})}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
placeholder="https://api.openai.com/v1"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">模型</label>
<input
type="text"
value={config.model}
onChange={(e) => setConfig({...config, model: e.target.value})}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
placeholder="gpt-3.5-turbo"
/>
</div>
</div>
</div>
);
}