import { Button, Collapse, Input, InputNumber, Select, Tag } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; import { useMemo, useState } from "react"; import { Switch } from "@/components/ui/base/switch"; import { ToolButton } from "@/components/ui/base/buttons"; import { ListFilter, ListPlus, Power, PowerOff } from "lucide-react"; import type { WorkflowFieldMapping } from "@/stores/use-config-store"; import { workflowFieldChoiceValues, workflowFieldConfigurationError, workflowFieldNumberBounds, workflowFieldPresetOptions } from "@/lib/model-capabilities"; import { EmptyState } from "@/components/ui/product/empty-state"; const sourceOptions = [ { label: "保留工作流默认值", value: "" }, { label: "任务提示词", value: "prompt" }, { label: "系统提示词", value: "systemPrompt" }, { label: "参考图片", value: "referenceImage" }, { label: "参考视频", value: "referenceVideo" }, { label: "参考音频", value: "referenceAudio" }, { label: "蒙版", value: "mask" }, { label: "画面尺寸", value: "size" }, { label: "画面宽高比", value: "aspectRatio" }, { label: "画面宽度", value: "width" }, { label: "画面高度", value: "height" }, { label: "生成数量", value: "count" }, { label: "生成质量", value: "quality" }, { label: "透明背景", value: "transparentBackground" }, { label: "视频时长", value: "videoSeconds" }, { label: "视频质量", value: "vquality" }, { label: "视频生成音频", value: "videoGenerateAudio" }, { label: "视频水印", value: "videoWatermark" }, { label: "音色", value: "audioVoice" }, { label: "音频格式", value: "audioFormat" }, { label: "音频语速", value: "audioSpeed" }, { label: "音频指令", value: "audioInstructions" }, ] as const; const fieldTypeOptions = [ { label: "文本", value: "TEXT" }, { label: "数字", value: "NUMBER" }, { label: "滑块", value: "SLIDER" }, { label: "开关", value: "BOOLEAN" }, { label: "下拉", value: "SELECT" }, { label: "图片", value: "IMAGE" }, { label: "视频", value: "VIDEO" }, { label: "音频", value: "AUDIO" }, ] as const; type WorkflowFieldMappingEditorProps = { fields: WorkflowFieldMapping[]; onChange: (fields: WorkflowFieldMapping[]) => void; disabled?: boolean; }; export function WorkflowFieldMappingEditor({ fields, onChange, disabled = false }: WorkflowFieldMappingEditorProps) { const [showOnlyEnabled, setShowOnlyEnabled] = useState(false); const enabledCount = useMemo(() => fields.filter((field) => field.enabled !== false).length, [fields]); const controllableFields = useMemo(() => fields.filter((field) => field.safeToOverride !== false), [fields]); const enabledControllableCount = useMemo(() => controllableFields.filter((field) => field.enabled !== false).length, [controllableFields]); const visibleFields = useMemo(() => fields.flatMap((field, index) => (showOnlyEnabled && field.enabled === false ? [] : [{ field, index }])), [fields, showOnlyEnabled]); const updateField = (index: number, patch: Partial) => { onChange(fields.map((field, fieldIndex) => (fieldIndex === index ? { ...field, ...patch } : field))); }; const updateAllFields = (enabled: boolean) => { onChange(fields.map((field) => (field.safeToOverride === false ? field : { ...field, enabled }))); }; if (!fields.length) { return ; } return (

共 {fields.length} 个字段,已开启 {enabledCount} 个。字段调整会立即保存到当前工作流。

} active={showOnlyEnabled} label={showOnlyEnabled ? "显示全部" : "仅显示已开启"} onClick={() => setShowOnlyEnabled((current) => !current)} />
{ const source = String(field.source || ""); const sourceLabel = sourceOptions.find((item) => item.value === source)?.label || source || "保留工作流默认值"; const enabled = field.enabled !== false; const fieldType = workflowEditorFieldType(field); const numeric = fieldType === "NUMBER" || fieldType === "SLIDER"; const choices = workflowFieldChoiceValues(field); const presetOptions = workflowFieldPresetOptions(field); const safeToOverride = field.safeToOverride !== false; const fieldDisabled = disabled || !enabled || !safeToOverride; const configurationError = workflowFieldConfigurationError(field); const roleLabel = field.role === "prompt" ? "提示词" : field.role === "media" ? "素材" : field.role === "internal" ? "内部参数" : "业务参数"; return { key: field.id || `${field.nodeId}::${field.fieldName}::${index}`, label: (
{field.label || field.fieldName} {field.nodeId}.{field.fieldName} {sourceLabel} {safeToOverride ? roleLabel : "禁止覆盖"}
), extra: ( event.stopPropagation()}> updateField(index, { enabled: checked })} /> ), children: (
{fieldType === "SELECT" || choices.length || presetOptions.length ? ( ) : null} {numeric ? ( <> {configurationError ?
{configurationError}
: null} ) : null} {sourceUsesIndex(source) ? ( ) : null} {fieldType === "NUMBER" ? ( ) : null} {!source ? ( ) : null}
), }; })} /> {showOnlyEnabled && !visibleFields.length ? : null}
); } function DefaultValueInput({ field, disabled, onChange }: { field: WorkflowFieldMapping; disabled: boolean; onChange: (value: unknown) => void }) { const fieldType = String(field.fieldType || "").toUpperCase(); const value = field.fieldValue ?? field.value ?? field.default; const bounds = workflowFieldNumberBounds(field); const choices = workflowFieldChoiceValues(field); if (fieldType === "BOOLEAN" || typeof value === "boolean") { return ( ({ label: workflowOptionText(option), value: workflowOptionValue(option) }))} onChange={onChange} />; } if (fieldType === "NUMBER" || typeof value === "number" || bounds.min !== undefined || bounds.max !== undefined || bounds.step !== undefined) { return ; } return onChange(event.target.value)} />; } function sourcePatch(field: WorkflowFieldMapping, source: string): Partial { if (source === "referenceImage") return { source, bindPrompt: false, sourceAutomatic: false, imageOrder: Math.max(1, Number(field.imageOrder) || 1), sourceIndex: undefined }; if (source === "referenceVideo" || source === "referenceAudio") return { source, bindPrompt: false, sourceAutomatic: false, sourceIndex: Math.max(0, Number(field.sourceIndex) || 0), imageOrder: undefined }; return { source, bindPrompt: false, sourceAutomatic: false, sourceIndex: undefined, imageOrder: undefined, required: source ? field.required : false }; } function sourceUsesIndex(source: string) { return source === "referenceImage" || source === "referenceVideo" || source === "referenceAudio"; } function workflowEditorFieldType(field: WorkflowFieldMapping) { const configured = String(field.fieldType || "") .trim() .toUpperCase(); if (["FLOAT", "INT", "INTEGER"].includes(configured)) return "NUMBER"; if (fieldTypeOptions.some((item) => item.value === configured)) return configured; const value = field.fieldValue ?? field.value ?? field.default; if (typeof value === "boolean") return "BOOLEAN"; if (typeof value === "number") return "NUMBER"; return "TEXT"; } function parseWorkflowOptions(value: string) { return value .split(/\r?\n|,/) .map((item) => item.trim()) .filter(Boolean); } function workflowOptionValue(value: unknown): string | number { if (value && typeof value === "object" && !Array.isArray(value)) { const option = value as Record; for (const key of ["value", "id", "key", "name", "label"]) { if (option[key] !== undefined && option[key] !== null) return typeof option[key] === "number" ? option[key] : String(option[key]); } } return typeof value === "number" ? value : String(value ?? ""); } function workflowOptionText(value: unknown) { return String(workflowOptionValue(value)); } function numberOrUndefined(value: unknown) { if (value === undefined || value === null || String(value).trim() === "") return undefined; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } function displayValue(value: unknown) { if (value === undefined || value === null) return ""; if (typeof value === "string") return value; try { return JSON.stringify(value); } catch { return String(value); } }