Files
Alfred/src/web/features/models/components/ModelFormModal.tsx
lanyuanxiaoyao b1dec691e9 refactor(web): 前端目录重构 — consoles/pages → layouts/features + shared
- 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)
2026-06-02 23:17:28 +08:00

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}必须为正整数`));
},
};
}