"use client" import { useEffect, useMemo, useState, type ReactNode } from "react" import { CheckIcon, ChevronsUpDownIcon, Trash2Icon } from "lucide-react" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { OptionCombobox } from "@/components/option-combobox" import { fetchKnowledgeBasesAll, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type KnowledgeBase } from "@/lib/api/admin" import { Status } from "@/lib/generated/enums" 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-8 rounded-sm border-slate-200 bg-white px-2 text-sm shadow-none" const inspectorComboboxClassName = "h-8 rounded-sm border-slate-200 bg-white text-sm 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}
{node.type === "knowledge_retrieve" ? ( updateConfig(nextConfig)} /> ) : 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 KnowledgeRetrieveConfigPanel({ config, onChange, }: { config: Record onChange: (config: Record) => void }) { const [knowledgeBases, setKnowledgeBases] = useState([]) const [open, setOpen] = useState(false) const selectedKnowledgeIds = normalizeKnowledgeBaseIds(config.knowledgeBaseIds) const knowledgeOptions = useMemo( () => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })), [knowledgeBases] ) const selectedKnowledgeOptions = selectedKnowledgeIds .map((id) => knowledgeOptions.find((option) => Number(option.value) === id)) .filter((option): option is { value: string; label: string } => Boolean(option)) useEffect(() => { let cancelled = false fetchKnowledgeBasesAll({ status: Status.Ok }) .then((items) => { if (!cancelled) { setKnowledgeBases(items ?? []) } }) .catch(() => { if (!cancelled) { setKnowledgeBases([]) } }) return () => { cancelled = true } }, []) const updateKnowledgeBaseIds = (ids: number[]) => { onChange({ ...config, knowledgeBaseIds: uniquePositiveNumbers(ids) }) } const toggleKnowledgeBase = (value: string) => { const id = Number(value) if (!Number.isFinite(id) || id <= 0) return if (selectedKnowledgeIds.includes(id)) { updateKnowledgeBaseIds(selectedKnowledgeIds.filter((item) => item !== id)) return } updateKnowledgeBaseIds([...selectedKnowledgeIds, id]) } return (
} > {selectedKnowledgeOptions.length === 0 ? "选择知识库" : selectedKnowledgeOptions.length === 1 ? selectedKnowledgeOptions[0].label : `已选择 ${selectedKnowledgeOptions.length} 个知识库`} 没有可用知识库 {knowledgeOptions.map((option) => { const selected = selectedKnowledgeIds.includes(Number(option.value)) return ( toggleKnowledgeBase(option.value)} > {option.label} ) })} {selectedKnowledgeOptions.length === 0 ? (
未选择知识库,流程发布校验不会通过。
) : null}
) } 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) } function normalizeKnowledgeBaseIds(value: unknown) { if (!Array.isArray(value)) { return [] } return uniquePositiveNumbers( value .map((item) => Number(item)) .filter((item) => Number.isFinite(item)) ) } function uniquePositiveNumbers(input: number[]) { return Array.from(new Set(input.filter((item) => item > 0))) }