"use client"; import { Brain, CheckCircle2, ChevronLeft, ChevronRight, Copy, Eye, EyeOff, HelpCircle, Loader2, MoreHorizontal, Pencil, Plug, Plus, Search, Trash2, XCircle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { useCallback, useEffect, useState, type ReactNode } from "react"; import { credentialsApi, type Credential, type InterfaceType, type ModelType, } from "@/lib/api"; /** 各资源类型可选的接口类型 */ const interfaceOptionsByType: Record = { LLM: ["openai"], ASR: ["openai", "xfyun", "dashscope"], TTS: ["openai", "xfyun", "dashscope"], Realtime: ["openai", "gemini"], Embedding: ["openai"], }; const modelTypes: ModelType[] = ["LLM", "ASR", "TTS", "Realtime", "Embedding"]; // 列表项类型直接复用 API 契约(camelCase,api_key 已打码) type ModelResource = Credential; type ModelForm = { name: string; modelId: string; type: ModelType; interfaceType: InterfaceType; apiUrl: string; apiKey: string; }; const emptyForm: ModelForm = { name: "", modelId: "", type: "LLM", interfaceType: "openai", apiUrl: "", apiKey: "", }; type TypeFilter = "全部" | ModelType; const typeFilters: TypeFilter[] = ["全部", ...modelTypes]; export function ComponentsModelsPage() { const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("全部"); const [currentPage, setCurrentPage] = useState(1); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); // 编辑时记住原记录,保存时回填 isDefault 等未在表单暴露的字段 const [editingModel, setEditingModel] = useState(null); const [form, setForm] = useState(emptyForm); const [showKey, setShowKey] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [deletingId, setDeletingId] = useState(null); const [duplicatingId, setDuplicatingId] = useState(null); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<"idle" | "ok" | "fail">("idle"); const loadModels = useCallback(async () => { setLoading(true); setLoadError(null); try { setModels(await credentialsApi.list()); } catch (error) { setLoadError(error instanceof Error ? error.message : "加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { // 挂载时拉取列表:这是与后端(外部系统)的同步,loadModels 内的 setLoading 属预期 // eslint-disable-next-line react-hooks/set-state-in-effect void loadModels(); }, [loadModels]); function updateForm(key: K, value: ModelForm[K]) { setForm((prev) => ({ ...prev, [key]: value })); // 任何配置变更后,旧的测试结果不再可信,重置为待测状态 setTestResult("idle"); } async function handleTestConnection() { setTesting(true); setTestResult("idle"); try { // TODO: 接入真实疎通接口(按 form.interfaceType 区分调用方式) await new Promise((resolve) => setTimeout(resolve, 900)); const reachable = Boolean(form.apiUrl.trim() && form.apiKey.trim()); setTestResult(reachable ? "ok" : "fail"); } catch { setTestResult("fail"); } finally { setTesting(false); } } function handleTypeChange(type: ModelType) { const options = interfaceOptionsByType[type]; setForm((prev) => ({ ...prev, type, // 资源类型变化时,若当前接口类型不在新类型的可选项内则重置为首项 interfaceType: options.includes(prev.interfaceType) ? prev.interfaceType : options[0], })); setTestResult("idle"); } function openCreate() { setEditingId(null); setEditingModel(null); setForm(emptyForm); setShowKey(false); setTestResult("idle"); setSaveError(null); setDialogOpen(true); } function openEdit(model: ModelResource) { setEditingId(model.id); setEditingModel(model); setForm({ name: model.name, modelId: model.modelId, type: model.type, interfaceType: model.interfaceType, apiUrl: model.apiUrl, // 后端回传已打码,原样带回 → 不修改则保留旧 key(写时哨兵) apiKey: model.apiKey, }); setShowKey(false); setTestResult("idle"); setSaveError(null); setDialogOpen(true); } async function handleSave() { setSaving(true); setSaveError(null); const payload = { name: form.name.trim(), modelId: form.modelId.trim(), type: form.type, interfaceType: form.interfaceType, apiUrl: form.apiUrl.trim(), apiKey: form.apiKey, // 表单未暴露 isDefault,编辑时沿用原值,新建默认 false isDefault: editingModel?.isDefault ?? false, }; try { if (editingId) { await credentialsApi.update(editingId, payload); } else { await credentialsApi.create(payload); } setDialogOpen(false); await loadModels(); } catch (error) { setSaveError(error instanceof Error ? error.message : "保存失败"); } finally { setSaving(false); } } async function handleDelete(id: string) { setDeletingId(id); try { await credentialsApi.remove(id); await loadModels(); } catch (error) { setLoadError(error instanceof Error ? error.message : "删除失败"); } finally { setDeletingId(null); } } // 服务端整行复制(含真 key,密钥不经浏览器) async function handleDuplicate(id: string) { setDuplicatingId(id); try { await credentialsApi.duplicate(id); await loadModels(); } catch (error) { setLoadError(error instanceof Error ? error.message : "复制失败"); } finally { setDuplicatingId(null); } } const filteredModels = models.filter((model) => { if (typeFilter !== "全部" && model.type !== typeFilter) { return false; } const keyword = searchQuery.trim().toLowerCase(); if (!keyword) { return true; } return [model.name, model.modelId, model.type, model.interfaceType, model.apiUrl] .join(" ") .toLowerCase() .includes(keyword); }); const pageSize = 5; const totalPages = Math.max(1, Math.ceil(filteredModels.length / pageSize)); const safeCurrentPage = Math.min(currentPage, totalPages); const pageStart = (safeCurrentPage - 1) * pageSize; const pageEnd = pageStart + pageSize; const paginatedModels = filteredModels.slice(pageStart, pageEnd); function handleSearchChange(value: string) { setSearchQuery(value); setCurrentPage(1); } function handleFilterChange(filter: TypeFilter) { setTypeFilter(filter); setCurrentPage(1); } const interfaceOptions = interfaceOptionsByType[form.type]; const canSave = form.name.trim() && form.modelId.trim() && form.apiUrl.trim(); return (

模型资源

统一管理 LLM、ASR、TTS、Realtime 与 Embedding 模型,配置各自的接口类型与访问凭证。

{typeFilters.map((filter) => ( ))}
handleSearchChange(event.target.value)} className="h-10 border-hairline-strong bg-background pl-9 text-sm text-foreground placeholder:text-muted-soft" placeholder="搜索模型名称、接口类型或 URL..." />
模型名称
资源类型
接口类型
API URL
操作
{paginatedModels.map((model) => (
{model.name}
{model.modelId}
{model.type}
{model.interfaceType}
{model.apiUrl}
{ event.preventDefault(); void handleDuplicate(model.id); }} > {duplicatingId === model.id ? ( ) : ( )} 复制 { event.preventDefault(); void handleDelete(model.id); }} > {deletingId === model.id ? ( ) : ( )} 删除
))} {loading && (
正在加载模型列表…
)} {!loading && loadError && (
加载失败
{loadError}
)} {!loading && !loadError && filteredModels.length === 0 && (
{models.length === 0 ? "暂无模型资源" : "未找到匹配的模型"}
{models.length === 0 ? "点击右上角「添加模型」创建第一个资源。" : "请调整关键词或筛选条件后再试。"}
)}
{filteredModels.length === 0 ? "没有数据" : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredModels.length)} / 共 ${filteredModels.length} 个模型`}
{Array.from({ length: totalPages }, (_, index) => index + 1).map( (page) => ( ), )}
{editingId ? "编辑模型" : "添加模型"} 填写资源名称、模型 ID、资源类型、接口类型与访问凭证。
资源名称 updateForm("name", event.target.value)} placeholder="请输入资源名称" className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft" />
模型 ID updateForm("modelId", event.target.value) } placeholder="例如 deepseek-chat" className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft" />
资源类型
接口类型
API URL updateForm("apiUrl", event.target.value)} placeholder="https://api.example.com/v1" className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft" />
API Key
updateForm("apiKey", event.target.value)} placeholder="请输入 API Key" className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft" />
{testResult === "ok" && ( 连接成功 )} {testResult === "fail" && ( 连接失败 )} {saveError && ( {saveError} )}
); } function FieldLabel({ htmlFor, children, hint, }: { htmlFor?: string; children: ReactNode; hint: { description: string; example?: string }; }) { return (
); } function HelpHint({ description, example, }: { description: string; example?: string; }) { return (

{description}

{example && (

填写样例: {example}

)}
); }