fix:优化项目内容

This commit is contained in:
Daniel
2026-03-18 17:01:10 +08:00
parent da63282a10
commit 27dc89e251
64 changed files with 3421 additions and 4982 deletions

View File

@@ -0,0 +1,357 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
aiSettingsApi,
type AIConfigListItem,
type AIConfigCreate,
type AIConfigUpdate,
} from "@/lib/api/client";
import { Loader2, Zap, Plus, Pencil, Trash2, CheckCircle } from "lucide-react";
import { toast } from "sonner";
const PROVIDERS = ["OpenAI", "DeepSeek", "Custom"];
export default function SettingsAIPage() {
const [list, setList] = useState<AIConfigListItem[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState<string | null>(null);
const [formName, setFormName] = useState("");
const [provider, setProvider] = useState("OpenAI");
const [apiKey, setApiKey] = useState("");
const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
const [baseUrl, setBaseUrl] = useState("");
const [modelName, setModelName] = useState("gpt-4o-mini");
const [temperature, setTemperature] = useState("0.2");
const [systemPromptOverride, setSystemPromptOverride] = useState("");
const loadList = useCallback(async () => {
try {
const data = await aiSettingsApi.list();
setList(data);
} catch {
toast.error("加载模型列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void loadList();
}, [loadList]);
const openAdd = () => {
setEditingId(null);
setFormName("");
setProvider("OpenAI");
setApiKey("");
setApiKeyConfigured(false);
setBaseUrl("");
setModelName("gpt-4o-mini");
setTemperature("0.2");
setSystemPromptOverride("");
setDialogOpen(true);
};
const openEdit = async (id: string) => {
try {
const c = await aiSettingsApi.getById(id);
setEditingId(id);
setFormName(c.name || "");
setProvider(c.provider || "OpenAI");
setApiKey("");
setApiKeyConfigured(!!(c.api_key && c.api_key.length > 0));
setBaseUrl(c.base_url || "");
setModelName(c.model_name || "gpt-4o-mini");
setTemperature(String(c.temperature ?? 0.2));
setSystemPromptOverride(c.system_prompt_override || "");
setDialogOpen(true);
} catch {
toast.error("加载配置失败");
}
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
if (editingId) {
const payload: AIConfigUpdate = {
name: formName.trim() || undefined,
provider: provider || undefined,
base_url: baseUrl.trim() || undefined,
model_name: modelName.trim() || undefined,
temperature: parseFloat(temperature),
system_prompt_override: systemPromptOverride.trim() || undefined,
};
if (apiKey.trim()) payload.api_key = apiKey.trim();
await aiSettingsApi.update(editingId, payload);
toast.success("已更新");
} else {
const payload: AIConfigCreate = {
name: formName.trim() || undefined,
provider: provider || undefined,
api_key: apiKey.trim() || undefined,
base_url: baseUrl.trim() || undefined,
model_name: modelName.trim() || undefined,
temperature: parseFloat(temperature),
system_prompt_override: systemPromptOverride.trim() || undefined,
};
await aiSettingsApi.create(payload);
toast.success("已添加");
}
setDialogOpen(false);
await loadList();
window.dispatchEvent(new CustomEvent("ai-settings-changed"));
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleActivate = async (id: string) => {
try {
await aiSettingsApi.activate(id);
toast.success("已选用该模型");
await loadList();
window.dispatchEvent(new CustomEvent("ai-settings-changed"));
} catch (e) {
toast.error(e instanceof Error ? e.message : "选用失败");
}
};
const handleDelete = async (id: string) => {
if (!confirm("确定删除该模型配置?")) return;
try {
await aiSettingsApi.delete(id);
toast.success("已删除");
await loadList();
window.dispatchEvent(new CustomEvent("ai-settings-changed"));
} catch (e) {
toast.error(e instanceof Error ? e.message : "删除失败");
}
};
const handleTest = async (id: string) => {
setTesting(id);
try {
const res = await aiSettingsApi.test(id);
toast.success(res.message ? `连接成功:${res.message}` : "连接成功");
} catch (e) {
toast.error(e instanceof Error ? e.message : "连接失败");
} finally {
setTesting(null);
}
};
if (loading) {
return (
<div className="p-6 flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
);
}
return (
<div className="p-6 max-w-4xl">
<div className="mb-4">
<Link to="/settings" className="text-sm text-muted-foreground hover:text-foreground">
</Link>
</div>
<Card>
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle></CardTitle>
<p className="text-sm text-muted-foreground mt-1">
AI 使
</p>
</div>
<Button onClick={openAdd}>
<Plus className="h-4 w-4" />
<span className="ml-2"></span>
</Button>
</div>
</CardHeader>
<CardContent>
{list.length === 0 ? (
<p className="text-sm text-muted-foreground py-4">
API Key
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>API Key</TableHead>
<TableHead className="w-[200px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">
{item.name || "未命名"}
{item.is_active && (
<span className="ml-2 text-xs text-primary inline-flex items-center gap-0.5">
<CheckCircle className="h-3.5 w-3.5" />
</span>
)}
</TableCell>
<TableCell>{item.provider}</TableCell>
<TableCell>{item.model_name || "—"}</TableCell>
<TableCell>{item.api_key_configured ? "已配置" : "未配置"}</TableCell>
<TableCell>
<div className="flex items-center gap-1 flex-wrap">
{!item.is_active && (
<Button variant="outline" size="sm" onClick={() => void handleActivate(item.id)}>
</Button>
)}
<Button variant="ghost" size="sm" onClick={() => void openEdit(item.id)} title="编辑">
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => void handleTest(item.id)}
disabled={testing === item.id}
title="测试连接"
>
{testing === item.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Zap className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => void handleDelete(item.id)}
title="删除"
className="text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingId ? "编辑模型配置" : "添加模型配置"}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSave} className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="ai-name">便</Label>
<Input
id="ai-name"
value={formName}
onChange={(e) => setFormName(e.target.value)}
placeholder="如OpenAI 生产、DeepSeek 备用"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="provider"></Label>
<Select value={provider} onValueChange={setProvider}>
<SelectTrigger id="provider">
<SelectValue placeholder="选择提供商" />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map((p) => (
<SelectItem key={p} value={p}>
{p}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="api_key">API Key</Label>
<Input
id="api_key"
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={apiKeyConfigured ? "已配置,输入新值以修改" : "请输入 API Key"}
autoComplete="off"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="base_url">Base URL</Label>
<Input
id="base_url"
type="url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="https://api.openai.com/v1"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="model_name"></Label>
<Input
id="model_name"
value={modelName}
onChange={(e) => setModelName(e.target.value)}
placeholder="gpt-4o-mini / deepseek-chat 等"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="temperature">Temperature (02)</Label>
<Input
id="temperature"
type="number"
min={0}
max={2}
step={0.1}
value={temperature}
onChange={(e) => setTemperature(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="system_prompt_override"></Label>
<textarea
id="system_prompt_override"
className="flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
value={systemPromptOverride}
onChange={(e) => setSystemPromptOverride(e.target.value)}
placeholder="留空则使用默认"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
</Button>
<Button type="submit" disabled={saving}>
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
{editingId ? "保存" : "添加"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}