import { useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { api } from "@/lib/api"; import { Loader2, Plus, RefreshCw, Search, Trash2, Pencil, Eye, EyeOff, Brain } from "lucide-react"; import { useAuthStore } from "@/store/authStore"; interface ModelConfig { id: string; name?: string; provider: string; model: string; model_type?: string; base_model?: string; protocol_type?: string; api_key?: string; api_base?: string; extra_headers?: Record; is_active: boolean; } const defaultForm: Omit = { name: "", provider: "openai", model: "", model_type: "LLM", base_model: "", protocol_type: "OpenAI", api_key: "", api_base: "", extra_headers: {}, is_active: true, }; export function ModelConfigs() { const { user } = useAuthStore(); const isAdmin = !!user?.is_admin; const [configs, setConfigs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [keyword, setKeyword] = useState(""); const [dialogOpen, setDialogOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); const [showApiKey, setShowApiKey] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState(""); const [extraConfigText, setExtraConfigText] = useState("{}"); const [form, setForm] = useState>(defaultForm); const fetchConfigs = async () => { setIsLoading(true); try { const data = await api.get("/api/v1/llm"); setConfigs(data); } catch (e) { console.error(e); } finally { setIsLoading(false); } }; useEffect(() => { fetchConfigs(); }, []); const filteredConfigs = useMemo(() => { const value = keyword.trim().toLowerCase(); if (!value) return configs; return configs.filter((item) => [item.name, item.model, item.provider, item.base_model].filter(Boolean).some((v) => String(v).toLowerCase().includes(value)) ); }, [configs, keyword]); const openCreate = () => { setEditingId(null); setForm(defaultForm); setExtraConfigText("{}"); setError(""); setShowApiKey(false); setDialogOpen(true); }; const openEdit = (item: ModelConfig) => { setEditingId(item.id); setForm({ name: item.name || "", provider: item.provider || "openai", model: item.model || "", model_type: item.model_type || "LLM", base_model: item.base_model || "", protocol_type: item.protocol_type || "OpenAI", api_key: item.api_key || "", api_base: item.api_base || "", extra_headers: item.extra_headers || {}, is_active: item.is_active, }); setExtraConfigText(JSON.stringify(item.extra_headers || {}, null, 2)); setError(""); setShowApiKey(false); setDialogOpen(true); }; const [isTesting, setIsTesting] = useState(false); const handleTestConnection = async () => { if (!form.model || !form.provider || !form.api_base) { setError("请先填写必要信息(供应商、模型ID、API域名)"); return; } setIsTesting(true); setError(""); try { let extraHeaders: Record = {}; if (extraConfigText.trim()) { try { const parsed = JSON.parse(extraConfigText); if (parsed && typeof parsed === "object") extraHeaders = parsed; } catch (err) { setError("额外配置必须是有效的JSON"); setIsTesting(false); return; } } const payload = { provider: form.provider, model: form.model, api_key: form.api_key, api_base: form.api_base, extra_headers: extraHeaders }; await api.post("/api/v1/llm/test", payload); alert("连接测试成功!"); } catch (e: any) { setError(e.message || "连接测试失败"); } finally { setIsTesting(false); } }; const handleSave = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!form.model || !form.provider || !form.api_base) { setError("请填写必填项"); return; } setIsSaving(true); setError(""); try { let extraHeaders: Record = {}; if (extraConfigText.trim()) { try { const parsed = JSON.parse(extraConfigText); if (parsed && typeof parsed === "object") extraHeaders = parsed; } catch (err) { setError("额外配置必须是有效的JSON"); setIsSaving(false); return; } } const payload = { ...form, extra_headers: extraHeaders, name: form.name || form.model, model_type: form.model_type || "大语言模型", base_model: form.base_model || form.model, protocol_type: form.protocol_type || "OpenAI", }; if (editingId) { await api.put(`/api/v1/llm/${editingId}`, payload); } else { const id = `${Date.now()}`; await api.post("/api/v1/llm", { ...payload, id }); } setDialogOpen(false); await fetchConfigs(); } catch (e: any) { setError(e.message || "保存配置失败"); } finally { setIsSaving(false); } }; const handleDelete = async (id: string) => { if (!window.confirm("确认删除该模型吗?")) return; try { await api.delete(`/api/v1/llm/${id}`); await fetchConfigs(); } catch (e) { console.error(e); } }; const handleSetDefault = async (item: ModelConfig) => { if (!isAdmin || item.is_active) return; try { await api.put(`/api/v1/llm/${item.id}`, { is_active: true }); await fetchConfigs(); } catch (e) { console.error(e); } }; if (!isAdmin) { return (
无权限访问此页面,请使用管理员账号登录。
); } return (
模型配置
setKeyword(e.target.value)} placeholder="搜索模型..." className="w-[200px] pl-9 h-8 text-sm" />
{isLoading ? (
) : ( 模型名称 供应商 模型标识 模型类型 状态 操作 {filteredConfigs.length === 0 ? ( 暂无模型数据 ) : ( filteredConfigs.map((item) => ( {item.name || item.model} {item.provider} {item.model} {item.model_type || "大语言模型"} handleSetDefault(item)} className={`inline-flex px-2 py-1 rounded-full text-xs font-medium cursor-pointer transition-colors ${item.is_active ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200'}`} title={item.is_active ? "当前默认模型" : "点击设为默认"} > {item.is_active ? '默认' : '设为默认'} )) )}
)}
{editingId ? "编辑模型" : "添加模型"}
{error &&
{error}
}
setForm((p) => ({ ...p, name: e.target.value }))} placeholder="如:GPT-4" />
setForm((p) => ({ ...p, model: e.target.value }))} placeholder="如:gpt-4-turbo" required />
setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder="如:https://api.openai.com/v1" required />
setForm((p) => ({ ...p, api_key: e.target.value }))} className="pr-10" placeholder="不修改请留空" />