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)
This commit is contained in:
236
src/web/features/models/components/ModelFormModal.tsx
Normal file
236
src/web/features/models/components/ModelFormModal.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
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}必须为正整数`));
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user