"use client" import type { ReactNode } from "react" import { Trash2Icon } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { OptionCombobox } from "@/components/option-combobox" import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" import { cn } from "@/lib/utils" import { VariableSelector } from "./variable-selector" import { buildVariableSpecDisplay, createConditionBranchID, isRefValue, normalizeNodeConfig, refField, refNodeId, type WorkflowConditionBranch, type WorkflowVariableRef, } from "./workflow-utils" export type WorkflowBranchSummary = { branchId: string targetNodeId?: string targetName?: string } const CONDITION_OPERATOR_OPTIONS = [ { value: "eq", label: "等于" }, { value: "neq", label: "不等于" }, { value: "contains", label: "包含" }, { value: "not_contains", label: "不包含" }, { value: "gt", label: "大于" }, { value: "gte", label: "大于等于" }, { value: "lt", label: "小于" }, { value: "lte", label: "小于等于" }, { value: "exists", label: "存在" }, { value: "empty", label: "为空" }, ] const inspectorInputClassName = "h-7 rounded-sm border-slate-200 bg-white px-2 text-xs shadow-none" const inspectorComboboxClassName = "h-7 rounded-sm border-slate-200 bg-white text-xs shadow-none" export function NodeConfigPanel({ node, nodeSpec, nodes, availableVariables, showHeader = true, showConditionBranches = true, onChange, onDelete, }: { node: AIWorkflowDefinition["nodes"][number] | null nodeSpec?: AIWorkflowNodeSpec nodes: AIWorkflowDefinition["nodes"] availableVariables?: WorkflowVariableRef[] branchSummaries?: WorkflowBranchSummary[] showHeader?: boolean showConditionBranches?: boolean onChange: (nodeId: string, data: AIWorkflowDefinition["nodes"][number]["data"]) => void onDelete?: (nodeId: string) => void }) { if (!node) { return (
未选择节点
) } const inputsValues = node.data?.inputsValues ?? {} const inputSchema = nodeSpec?.inputSchema ?? [] const outputSchema = nodeSpec?.outputSchema ?? [] const canDelete = node.type !== "start" && node.type !== "end" const config = normalizeNodeConfig(node.data?.config) const branches = config.branches ?? [] const updateData = (data: Partial) => { onChange(node.id, { ...(node.data ?? {}), ...data, }) } const updateConfig = (nextConfig: Record) => updateData({ config: nextConfig }) const inputFields = inputSchema.map((input) => { const value = inputsValues[input.name] return ( { updateData({ inputsValues: { ...inputsValues, [input.name]: next, }, }) }} /> {input.description ? {input.description} : null} ) }) const outputFields = outputSchema.map((output) => { const item = buildVariableSpecDisplay(output) return ( {item.description ? {item.description} : null} ) }) const updateBranch = (branch: WorkflowConditionBranch) => { const nextBranches = branches.some((item) => item.id === branch.id) ? branches.map((item) => (item.id === branch.id ? branch : item)) : [...branches, branch] updateConfig({ ...config, branches: nextBranches }) } const deleteBranch = (branchId: string) => { updateConfig({ ...config, branches: branches.filter((branch) => branch.id !== branchId) }) } const addBranch = () => { const targetNodeId = nodes.find((item) => item.id !== node.id && item.type !== "start")?.id ?? "" updateBranch({ id: createConditionBranchID(branches), name: "新分支", targetNodeId, condition: { operator: "eq", }, }) } return (
{showHeader ? (
{node.data?.title || nodeSpec?.title || node.type}
{node.id}
{canDelete ? ( ) : null}
) : null}
{showConditionBranches && (node.type === "condition" || branches.length > 0) ? ( ) : null}
) } export function ConditionBranchConfigPanel({ node, nodes, branchId, variables, onChange, }: { node: AIWorkflowDefinition["nodes"][number] nodes: AIWorkflowDefinition["nodes"] branchId: string variables: WorkflowVariableRef[] onChange: (nodeId: string, data: AIWorkflowDefinition["nodes"][number]["data"]) => void }) { const config = normalizeNodeConfig(node.data?.config) const branches = config.branches ?? [] const branch = branches.find((item) => item.id === branchId) const targetOptions = buildTargetOptions(nodes, node.id) if (!branch) { return (
条件分支不存在
) } const updateBranch = (nextBranch: WorkflowConditionBranch) => { onChange(node.id, { ...(node.data ?? {}), config: { ...config, branches: branches.map((item) => (item.id === nextBranch.id ? nextBranch : item)), }, }) } return (
updateBranch({ ...branch, targetNodeId })} /> {branch.default ? (
默认分支不需要条件表达式,会在其他条件不匹配时执行。
) : ( )}
) } function ConditionBranchesEditor({ branches, nodes, currentNodeId, variables, onAdd, onChange, onDelete, }: { branches: WorkflowConditionBranch[] nodes: AIWorkflowDefinition["nodes"] currentNodeId: string variables: WorkflowVariableRef[] onAdd: () => void onChange: (branch: WorkflowConditionBranch) => void onDelete: (branchId: string) => void }) { const targetOptions = buildTargetOptions(nodes, currentNodeId) return ( 添加 } > {branches.length === 0 ? (
暂无分支。条件节点需要至少一个默认分支或条件分支。
) : null}
{branches.map((branch) => { return (
{branch.default ? "ELSE" : "IF"} onChange({ ...branch, name: event.target.value })} /> {branch.default ? null : ( )}
目标节点
onChange({ ...branch, targetNodeId })} />
{branch.default ? null : (
)}
) })}
) } function ConditionFields({ branch, variables, onChange, compact = false, }: { branch: WorkflowConditionBranch variables: WorkflowVariableRef[] onChange: (branch: WorkflowConditionBranch) => void compact?: boolean }) { const condition = branch.condition ?? {} const selectedVariable = isRefValue(condition.left) ? variables.find((item) => item.nodeId === refNodeId(condition.left) && item.field === refField(condition.left)) : undefined const valueOptions = selectedVariable?.valueOptions ?? [] const rightDisabled = ["exists", "empty"].includes(condition.operator ?? "") return (
左值
onChange({ ...branch, condition: { ...condition, left }, })} />
判断
onChange({ ...branch, condition: { ...condition, operator }, })} />
{valueOptions.length > 0 && !rightDisabled ? ( ({ value: stringifyConditionRight(option.value), label: option.label || stringifyConditionRight(option.value), description: option.description, }))} placeholder="选择取值" triggerClassName={inspectorComboboxClassName} preserveExternalSelection onChange={(nextValue) => { const selectedOption = valueOptions.find((option) => stringifyConditionRight(option.value) === nextValue) onChange({ ...branch, condition: { ...condition, right: selectedOption?.value ?? nextValue }, }) }} /> ) : ( onChange({ ...branch, condition: { ...condition, right: event.target.value }, })} /> )}
) } function InspectorParameterTabs({ inputCount, outputCount, inputContent, outputContent, }: { inputCount: number outputCount: number inputContent: ReactNode outputContent: ReactNode }) { const tabs = [ inputCount > 0 ? { value: "input", label: "输入", count: inputCount, content: inputContent } : null, outputCount > 0 ? { value: "output", label: "输出", count: outputCount, content: outputContent } : null, ].filter((item): item is { value: string; label: string; count: number; content: ReactNode } => Boolean(item)) if (tabs.length === 0) { return null } if (tabs.length === 1) { return ( {tabs[0].content} ) } return (
{tabs.map((tab) => ( {tab.label} {tab.count} ))}
{tabs.map((tab) => ( {tab.content} ))}
) } function InspectorSection({ title, meta, action, children, }: { title: string meta?: string action?: ReactNode children: ReactNode }) { return (
{title}
{meta ? {meta} : null} {action}
{children}
) } function InspectorRow({ label, detail, required, children, }: { label: string detail?: string required?: boolean children: ReactNode }) { return (
{label} {required ? * : null}
{detail ?
{detail}
: null}
{children}
) } function InspectorField({ label, detail, fieldName, fieldType, required, children, }: { label: string detail?: string fieldName?: string fieldType?: string required?: boolean children: ReactNode }) { const metaItems = [ fieldName ? { label: "字段", value: fieldName } : null, fieldType ? { label: "类型", value: fieldType } : null, ].filter((item): item is { label: string; value: string } => Boolean(item)) return (
{label} {required ? * : null}
{detail ?
{detail}
: null}
{metaItems.length > 0 ? (
{metaItems.map((item) => ( {item.label} {item.value} ))}
) : null}
{children}
) } function InspectorHint({ children }: { children: ReactNode }) { return
{children}
} function buildTargetOptions(nodes: AIWorkflowDefinition["nodes"], currentNodeId: string) { return nodes .filter((node) => node.id !== currentNodeId && node.type !== "start") .map((node) => ({ value: node.id, label: node.data?.title || node.type || node.id, })) } function stringifyConditionRight(value: unknown) { if (value === undefined || value === null) { return "" } if (typeof value === "string") { return value } return JSON.stringify(value) }