- consoles/admin/ → layouts/admin-layout/ - consoles/workbench/ → layouts/workbench-layout/ + features/chat/ - pages/ → features/ (dashboard, models, projects, not-found) - components/ → shared/components/ - hooks/ → shared/hooks/ - utils/ → shared/utils/ - 更新所有 import 路径 (src/web/ + tests/web/) - 更新开发文档 (README.md, frontend.md, architecture.md)
237 lines
8.1 KiB
TypeScript
237 lines
8.1 KiB
TypeScript
import { App as AntApp, Button, Checkbox, Col, Form, Input, InputNumber, Modal, Row, Select, Space } from "antd";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import type {
|
|
CreateModelRequest,
|
|
Model,
|
|
ModelCapability,
|
|
ModelTestResponse,
|
|
ProviderOption,
|
|
TestModelRequest,
|
|
UpdateModelRequest,
|
|
} from "../../../../shared/api";
|
|
|
|
interface FormValues {
|
|
capabilities: ModelCapability[];
|
|
contextLength: null | number;
|
|
maxOutputTokens: null | number;
|
|
modelId: string;
|
|
name: string;
|
|
providerId: string;
|
|
}
|
|
|
|
interface ModelFormModalProps {
|
|
editingModel: Model | null;
|
|
onCancel: () => void;
|
|
onCreate: (data: CreateModelRequest) => Promise<unknown>;
|
|
onOpenChange: (open: boolean) => void;
|
|
onUpdate: (args: { data: UpdateModelRequest; id: string }) => Promise<unknown>;
|
|
open: boolean;
|
|
providers: ProviderOption[];
|
|
providersError: Error | null;
|
|
providersLoading: boolean;
|
|
submitting: boolean;
|
|
testModelConnection?: (data: TestModelRequest) => Promise<ModelTestResponse>;
|
|
}
|
|
|
|
const DEFAULT_CAPABILITIES: ModelCapability[] = ["text", "reasoning"];
|
|
|
|
const CAPABILITY_OPTIONS: Array<{ label: string; value: ModelCapability }> = [
|
|
{ label: "文本", value: "text" },
|
|
{ label: "推理", value: "reasoning" },
|
|
{ label: "图片生成", value: "image-generation" },
|
|
{ label: "视频生成", value: "video-generation" },
|
|
{ label: "音频生成", value: "audio-generation" },
|
|
{ label: "图片识别", value: "image-recognition" },
|
|
{ label: "视频识别", value: "video-recognition" },
|
|
{ label: "音频识别", value: "audio-recognition" },
|
|
];
|
|
|
|
export function ModelFormModal({
|
|
editingModel,
|
|
onCancel,
|
|
onCreate,
|
|
onOpenChange,
|
|
onUpdate,
|
|
open,
|
|
providers,
|
|
providersError,
|
|
providersLoading,
|
|
submitting,
|
|
testModelConnection,
|
|
}: ModelFormModalProps) {
|
|
const { message } = AntApp.useApp();
|
|
const [form] = Form.useForm<FormValues>();
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
if (editingModel) {
|
|
form.setFieldsValue({
|
|
capabilities: editingModel.capabilities,
|
|
contextLength: editingModel.contextLength,
|
|
maxOutputTokens: editingModel.maxOutputTokens,
|
|
modelId: editingModel.modelId,
|
|
name: editingModel.name,
|
|
providerId: editingModel.providerId,
|
|
});
|
|
} else {
|
|
form.resetFields();
|
|
form.setFieldsValue({ capabilities: DEFAULT_CAPABILITIES });
|
|
}
|
|
}, [editingModel, form, open]);
|
|
|
|
const handleFinish = async (values: FormValues) => {
|
|
try {
|
|
if (editingModel) {
|
|
const reqData: UpdateModelRequest = {};
|
|
if (values.name !== editingModel.name) reqData.name = values.name;
|
|
if (values.modelId !== editingModel.modelId) reqData.modelId = values.modelId;
|
|
if (values.providerId !== editingModel.providerId) reqData.providerId = values.providerId;
|
|
const capsChanged =
|
|
values.capabilities.length !== editingModel.capabilities.length ||
|
|
values.capabilities.some((c, i) => c !== editingModel.capabilities[i]);
|
|
if (capsChanged) reqData.capabilities = values.capabilities;
|
|
if (values.contextLength !== editingModel.contextLength) reqData.contextLength = values.contextLength;
|
|
if (values.maxOutputTokens !== editingModel.maxOutputTokens) reqData.maxOutputTokens = values.maxOutputTokens;
|
|
await onUpdate({ data: reqData, id: editingModel.id });
|
|
message.success("模型已更新");
|
|
} else {
|
|
const reqData: CreateModelRequest = {
|
|
capabilities: values.capabilities,
|
|
contextLength: values.contextLength ?? undefined,
|
|
maxOutputTokens: values.maxOutputTokens ?? undefined,
|
|
modelId: values.modelId,
|
|
name: values.name,
|
|
providerId: values.providerId,
|
|
};
|
|
await onCreate(reqData);
|
|
message.success("模型已创建");
|
|
}
|
|
onOpenChange(false);
|
|
} catch (err: unknown) {
|
|
if (err instanceof Error) {
|
|
message.error(err.message);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleTest = async () => {
|
|
if (!testModelConnection) return;
|
|
const providerId: unknown = form.getFieldValue("providerId");
|
|
const modelId: unknown = form.getFieldValue("modelId");
|
|
if (typeof providerId !== "string" || !providerId) {
|
|
message.warning("请先选择供应商");
|
|
return;
|
|
}
|
|
if (typeof modelId !== "string" || !modelId) {
|
|
message.warning("请先输入模型 ID");
|
|
return;
|
|
}
|
|
setTesting(true);
|
|
try {
|
|
const result = await testModelConnection({ modelId, providerId });
|
|
if (result.ok) {
|
|
message.success(result.message);
|
|
} else {
|
|
message.error(result.message);
|
|
}
|
|
} catch (err: unknown) {
|
|
message.error((err as Error).message);
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
};
|
|
|
|
const providerOptions = providers.map((p) => ({ label: p.name, value: p.id }));
|
|
|
|
return (
|
|
<Modal
|
|
confirmLoading={submitting}
|
|
destroyOnHidden
|
|
okText="确定"
|
|
onCancel={onCancel}
|
|
onOk={() => void form.submit()}
|
|
open={open}
|
|
title={editingModel ? "编辑模型" : "新建模型"}
|
|
width={600}
|
|
>
|
|
<Form form={form} layout="vertical" onFinish={(values) => void handleFinish(values)}>
|
|
<Form.Item
|
|
label="模型名称"
|
|
name="name"
|
|
rules={[{ message: "请输入模型名称", required: true, whitespace: true }]}
|
|
>
|
|
<Input placeholder="请输入模型名称" />
|
|
</Form.Item>
|
|
<Form.Item label="所属供应商" name="providerId" rules={[{ message: "请选择供应商", required: true }]}>
|
|
<Select
|
|
loading={providersLoading}
|
|
notFoundContent={getProviderNotFoundContent(providersLoading, providersError)}
|
|
optionFilterProp="label"
|
|
options={providerOptions}
|
|
placeholder="请选择供应商"
|
|
showSearch
|
|
status={providersError ? "error" : undefined}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="模型 ID"
|
|
name="modelId"
|
|
rules={[{ message: "请输入模型 ID", required: true, whitespace: true }]}
|
|
>
|
|
<Input placeholder="gpt-4o, claude-3-opus-20240229, deepseek-chat 等" />
|
|
</Form.Item>
|
|
<Form.Item label="能力标签" name="capabilities" rules={[{ message: "请至少选择一个能力标签", required: true }]}>
|
|
<Checkbox.Group>
|
|
<Row gutter={[8, 8]}>
|
|
{CAPABILITY_OPTIONS.map((opt) => (
|
|
<Col key={opt.value} md={8} sm={12} xs={24}>
|
|
<Checkbox value={opt.value}>{opt.label}</Checkbox>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</Checkbox.Group>
|
|
</Form.Item>
|
|
<Row gutter={16}>
|
|
<Col sm={12} xs={24}>
|
|
<Form.Item label="上下文长度" name="contextLength" rules={[positiveIntegerRule("上下文长度")]}>
|
|
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col sm={12} xs={24}>
|
|
<Form.Item label="最大输出 Token" name="maxOutputTokens" rules={[positiveIntegerRule("最大输出 Token")]}>
|
|
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
{testModelConnection && (
|
|
<Form.Item>
|
|
<Space>
|
|
<Button loading={testing} onClick={() => void handleTest()}>
|
|
测试连接
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
)}
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function getProviderNotFoundContent(loading: boolean, error: Error | null): string {
|
|
if (loading) return "正在加载供应商";
|
|
if (error) return `供应商加载失败:${error.message}`;
|
|
return "暂无供应商,请先新建供应商";
|
|
}
|
|
|
|
function positiveIntegerRule(label: string) {
|
|
return {
|
|
validator(_: unknown, value: null | number | undefined) {
|
|
if (value === undefined || value === null) return Promise.resolve();
|
|
if (Number.isInteger(value) && value > 0) return Promise.resolve();
|
|
return Promise.reject(new Error(`${label}必须为正整数`));
|
|
},
|
|
};
|
|
}
|