// ============================================================ // ModelInputForm — 动态模型输入表单 // 根据选中模型的 param_schema + ui_config 动态渲染表单控件 // ============================================================ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Form, Input, InputNumber, Select, Slider, Button, Typography, Empty, Upload, Switch, theme as antTheme, message, } from 'antd'; import { SendOutlined, UploadOutlined, InboxOutlined, } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload'; import { useHomeContext } from '@/contexts/home-context'; import { useAppContext } from '@/contexts/app-context'; import { useSubmitTask } from '@/hooks/use-api'; import { emit, EVENTS } from '@/utils/event-bus'; import type { ModelsListResponseBody, TaskInfoItemResponseBody } from '@/services/modules'; const { Text, Title } = Typography; const { TextArea } = Input; const { Dragger } = Upload; // ---------- 参数类型映射 ---------- /** 从 ui_config 中解析的参数描述 */ interface ParamDescriptor { /** React key(item.id,保证唯一) */ key: string; /** API 字段名(item.map_to) */ name: string; label: string; /** 控件类型 */ controlType: 'text' | 'number' | 'textarea' | 'select' | 'slider' | 'switch' | 'image-upload'; defaultValue?: unknown; required?: boolean; placeholder?: string; /** 数值型:最小值 */ min?: number; /** 数值型:最大值 */ max?: number; /** 数值型:步长 */ step?: number; /** select 型:选项列表 */ options?: Array<{ label: string; value: string | number }>; } /** 从 ui_config 对象中解析控件类型 */ function inferControlType(config: Record): ParamDescriptor['controlType'] { const type = config.type as string | undefined; if (!type) return 'text'; switch (type) { case 'number': case 'integer': case 'float': return 'number'; case 'textarea': case 'text_area': case 'prompt': return 'textarea'; case 'select': case 'dropdown': case 'enum': return 'select'; case 'slider': case 'range': return 'slider'; case 'switch': case 'boolean': case 'bool': return 'switch'; case 'image': case 'image-upload': case 'file': return 'image-upload'; default: return 'text'; } } /** * 从 param_schema 构建参数描述符列表 * * 后端 param_schema 格式为 ParamSchemaItem[](对象数组), * 每项的 ui 字段内嵌了控件类型、标签、校验规则等 UI 配置。 */ function buildParamDescriptors( paramSchema: ModelsListResponseBody['param_schema'], ): ParamDescriptor[] { return paramSchema.map((item) => { const config = (item.ui || {}) as Record; const controlType = inferControlType(config); return { key: item.id, name: item.map_to, label: (config.label as string) || item.map_to, controlType, defaultValue: config.default ?? config.defaultValue, required: (config.required as boolean) || false, placeholder: (config.placeholder as string) || `请输入${config.label || item.map_to}`, min: config.min as number | undefined, max: config.max as number | undefined, step: config.step as number | undefined, options: (config.options as ParamDescriptor['options']) || undefined, }; }); } // ---------- 组件 ---------- export function ModelInputForm() { const { token } = antTheme.useToken(); const { isLoggedIn } = useAppContext(); const { selectedModel } = useHomeContext(); const [form] = Form.useForm(); const [referenceFiles, setReferenceFiles] = useState([]); // 提交突变 Hook(自动处理 loading / error / 日志) const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({ onSuccess: useCallback( (_data: TaskInfoItemResponseBody) => { message.success('任务已提交'); // 通过事件总线通知 TaskHistory 刷新(替代 Context 中的 refreshTaskSignal) emit(EVENTS.TASK_SUBMITTED); }, [], ), }); // 选中模型时重置表单(仅当有选中模型时操作,避免 form 未连接警告) useEffect(() => { if (selectedModel) { form.resetFields(); } setReferenceFiles([]); }, [selectedModel?.id]); // eslint-disable-line react-hooks/exhaustive-deps // 参数描述符 const paramDescriptors = useMemo(() => { if (!selectedModel) return []; return buildParamDescriptors(selectedModel.param_schema || []); }, [selectedModel]); // 提交 const handleSubmit = async () => { if (!isLoggedIn) { message.warning('请先登录后再提交任务'); return; } if (!selectedModel) return; try { const values = await form.validateFields(); // 分离固定字段和模型动态参数 const { prompt, negative_prompt, num_outputs, ...dynamicParams } = values; // 构建提交参数(全部转为字符串值) const params: Record = { prompt: String(prompt ?? ''), negative_prompt: String(negative_prompt ?? ''), num_outputs: String(num_outputs ?? 1), }; // 动态参数统一转字符串 for (const [key, value] of Object.entries(dynamicParams)) { if (value !== undefined && value !== null) { params[key] = String(value); } } // 参考图 if (referenceFiles.length > 0) { const refUrls = referenceFiles .filter((f) => f.status === 'done') .map((f) => f.response?.url || f.url || f.name) .filter(Boolean) as string[]; if (refUrls.length > 0) { params.reference_images = JSON.stringify(refUrls); } } // 使用 mutation Hook 提交(自动处理 loading / error / 日志) await submitTask({ model_id: selectedModel.id, params }); } catch (err) { if (err && typeof err === 'object' && 'errorFields' in err) { // 表单验证错误,antd 会自动提示 return; } // 错误消息由 Hook 自动通过 errorMessage 提供 message.error(errorMessage || '任务提交失败,请重试'); } }; // 渲染单个参数控件 const renderParamControl = (desc: ParamDescriptor) => { const commonProps = { placeholder: desc.placeholder, style: { width: '100%' }, }; switch (desc.controlType) { case 'number': return ( ); case 'textarea': return