Files
doc-forge-reborn/web/src/pages/ModelList.tsx
T
2026-07-08 20:02:29 +08:00

256 lines
7.8 KiB
TypeScript

import { useEffect, useState, useCallback } from "react";
import {
Table, Button, Modal, Form, Input, Select, Switch,
Space, message, Popconfirm, Tag,
} from "antd";
import { PlusOutlined, EditOutlined, DeleteOutlined, ExperimentOutlined } from "@ant-design/icons";
import type { AIModel } from "../types";
import * as modelApi from "../api/models";
const PROVIDERS = [
{ label: "OpenAI", value: "openai" },
{ label: "Azure", value: "azure" },
{ label: "自定义", value: "custom" },
];
export default function ModelList() {
const [models, setModels] = useState<AIModel[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingModel, setEditingModel] = useState<AIModel | null>(null);
const [form] = Form.useForm();
const fetchModels = useCallback(async () => {
setLoading(true);
try {
const data = await modelApi.listModels();
setModels(data);
} catch {
message.error("获取模型列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchModels();
}, [fetchModels]);
const handleCreate = () => {
setEditingModel(null);
setModalOpen(true);
};
const handleEdit = (model: AIModel) => {
setEditingModel(model);
setModalOpen(true);
};
useEffect(() => {
if (!modalOpen) return;
// 延迟确保 Form DOM 已渲染
const timer = setTimeout(() => {
if (editingModel) {
const extra = editingModel.extra_params || {};
form.setFieldsValue({
name: editingModel.name,
provider: editingModel.provider,
model_name: extra.model || "",
endpoint: editingModel.endpoint,
api_key: "",
extra_params: JSON.stringify(extra, null, 2),
is_enabled: editingModel.is_enabled,
remark: editingModel.remark,
});
} else {
form.resetFields();
form.setFieldsValue({ provider: "openai", is_enabled: true, extra_params: "{}" });
}
}, 0);
return () => clearTimeout(timer);
}, [modalOpen, editingModel]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
let extraParams = {};
try {
extraParams = JSON.parse(values.extra_params || "{}");
} catch {
message.error("扩展参数 JSON 格式错误");
return;
}
const payload = {
...values,
extra_params: { ...extraParams, ...(values.model_name ? { model: values.model_name } : {}) },
};
delete payload.model_name;
if (editingModel) {
if (!payload.api_key) delete payload.api_key;
await modelApi.updateModel(editingModel.id, payload);
message.success("更新成功");
} else {
await modelApi.createModel(payload);
message.success("创建成功");
}
setModalOpen(false);
fetchModels();
} catch (err: unknown) {
if (err instanceof Error) message.error(err.message);
}
};
const handleDelete = async (id: string) => {
await modelApi.deleteModel(id);
message.success("删除成功");
fetchModels();
};
const handleToggle = async (id: string, enabled: boolean) => {
await modelApi.toggleModel(id, enabled);
fetchModels();
};
const handleTest = async (id: string, name: string) => {
message.loading({ content: `正在测试 ${name}...`, key: "test" });
try {
const res = await modelApi.testModel(id);
if (res.success) {
message.success({ content: `${name} 测试通过`, key: "test" });
Modal.info({ title: `测试结果 - ${name}`, content: res.result, width: 600 });
} else {
message.error({ content: `测试失败: ${res.error}`, key: "test", duration: 5 });
}
} catch (err: unknown) {
message.error({ content: err instanceof Error ? err.message : "测试失败", key: "test", duration: 5 });
}
};
const columns = [
{ title: "名称", dataIndex: "name", key: "name", width: 150 },
{
title: "供应商",
dataIndex: "provider",
key: "provider",
width: 80,
render: (v: string) => {
const label = PROVIDERS.find((p) => p.value === v)?.label || v;
return <Tag>{label}</Tag>;
},
},
{
title: "模型名",
key: "model_name",
width: 140,
render: (_: unknown, r: AIModel) => <Tag>{(r.extra_params as Record<string,unknown>)?.model as string || "-"}</Tag>,
},
{ title: "接口地址", dataIndex: "endpoint", key: "endpoint", ellipsis: true },
{
title: "启用",
dataIndex: "is_enabled",
key: "is_enabled",
width: 80,
render: (v: boolean, record: AIModel) => (
<Switch
checked={v}
size="small"
onChange={(checked) => handleToggle(record.id, checked)}
/>
),
},
{
title: "备注",
dataIndex: "remark",
key: "remark",
width: 150,
ellipsis: true,
render: (v: string | null) => v || "-",
},
{
title: "操作",
key: "actions",
width: 160,
render: (_: unknown, record: AIModel) => (
<Space>
<Button
type="link"
size="small"
icon={<ExperimentOutlined />}
onClick={() => handleTest(record.id, record.name)}
/>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
/>
<Popconfirm
title="确定删除此模型?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: "flex", justifyContent: "space-between" }}>
<h2></h2>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</div>
<Table
columns={columns}
dataSource={models}
rowKey="id"
loading={loading}
pagination={false}
/>
<Modal
title={editingModel ? "编辑模型" : "新建模型"}
open={modalOpen}
onCancel={() => { setModalOpen(false); form.resetFields(); }}
onOk={handleSubmit}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="模型名称" />
</Form.Item>
<Form.Item name="provider" label="供应商" rules={[{ required: true }]}>
<Select options={PROVIDERS} />
</Form.Item>
<Form.Item name="model_name" label="模型名" rules={[{ required: true }]}>
<Input placeholder="例如 deepseek-chat / gpt-4 / gpt-3.5-turbo" />
</Form.Item>
<Form.Item name="endpoint" label="接口地址" rules={[{ required: true }]}>
<Input placeholder="https://api.openai.com/v1/chat/completions" />
</Form.Item>
<Form.Item
name="api_key"
label="API Key"
rules={editingModel ? [] : [{ required: true, message: "请输入 API Key" }]}
>
<Input.Password placeholder={editingModel ? "留空则不修改" : "sk-..."} />
</Form.Item>
<Form.Item name="extra_params" label="扩展参数 (JSON)">
<Input.TextArea rows={4} placeholder='{"temperature": 0.7, "max_tokens": 2000}' />
</Form.Item>
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</div>
);
}