"use client";

/**
 * 工作流可视化编辑器。基于 @xyflow/react,样式套用 ai-video 设计 token。
 *
 * 工具栏:添加节点、工作流设置、动态变量。全局设置、节点、条件边编辑与调试预览
 * 复用画布右半覆盖层。
 * 节点目录来自后端 /api/node-types。数据通过 value/onChange 与外部 graph 同步。
 */

import {
  addEdge,
  Background,
  BackgroundVariant,
  type Connection,
  Controls,
  type Edge,
  type Node,
  type NodeChange,
  type OnConnectEnd,
  Panel,
  ReactFlow,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import {
  AudioLines,
  Braces,
  Bot,
  Brain,
  Database,
  Flag,
  GitBranch,
  Loader2,
  MessageSquareText,
  PhoneForwarded,
  Play,
  Plus,
  Settings2,
  Sparkles,
  Tag,
  Trash2,
  Wrench,
  X,
  Zap,
} from "lucide-react";
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { SectionCard } from "@/components/editor/section-card";
import { KnowledgeRetrievalConfigDialog } from "@/components/editor/knowledge-retrieval-config-dialog";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import type { KnowledgeRetrievalConfig, TurnConfig } from "@/lib/api";
import { TurnConfigEditor } from "@/components/turn-config-editor";
import { Textarea } from "@/components/ui/textarea";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

import { edgeTypes } from "./ConditionEdge";
import {
  ActiveNodeContext,
  EdgeActionContext,
  NodeActionContext,
  NodeSpecsContext,
} from "./context";
import { nodeTypes } from "./GenericNode";
import {
  accentVar,
  defaultGraph,
  type NodeSpecMap,
  type RuntimeNodeSpec,
  type ExpressionRule,
  type WorkflowGraph,
  type WorkflowEdgeData,
  type WorkflowNodeData,
  type WorkflowNodeType,
} from "./specs";
import { useNodeSpecs } from "./useNodeSpecs";

export type WorkflowSettings = {
  llm?: string;
  asr?: string;
  tts?: string;
  toolIds: string[];
  knowledgeBaseId: string;
  knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
  globalPrompt: string;
  allowInterrupt: boolean;
  turnConfig: TurnConfig;
};

export type ModelOption = { value: string; label: string };

export type WorkflowEditorProps = {
  value?: WorkflowGraph;
  onChange?: (graph: WorkflowGraph) => void;
  settings: WorkflowSettings;
  onSettingsChange: (settings: WorkflowSettings) => void;
  modelOptions: { llm: ModelOption[]; asr: ModelOption[]; tts: ModelOption[] };
  toolOptions?: ModelOption[];
  knowledgeOptions?: ModelOption[];
  onOpenDynamicVariables: () => void;
  editingNodeId: string | null;
  onEditingNodeIdChange: (nodeId: string | null) => void;
  editingEdgeId: string | null;
  onEditingEdgeIdChange: (edgeId: string | null) => void;
  settingsOpen: boolean;
  onSettingsOpenChange: (open: boolean) => void;
  debugOpen: boolean;
  onDebugOpenChange: (open: boolean) => void;
  debugPanel: ReactNode;
  /** 调试通话中当前激活的节点 id(用于高亮)。 */
  activeNodeId?: string | null;
};

let nodeSeq = 0;
const NONE = "__none__";

function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
  const data: WorkflowNodeData = {
    name: spec.displayName,
    ...(spec.type === "agent"
      ? {
          contextPolicy: "inherit",
          inheritGlobalConfig: true,
          entryMode: "wait_user",
          entrySpeech: "",
        }
      : {}),
  };
  for (const field of spec.fields) {
    if (field.default !== undefined) data[field.key] = field.default;
  }
  return data;
}

function toFlow(graph: WorkflowGraph): { nodes: Node[]; edges: Edge[] } {
  return {
    nodes: graph.nodes.map((n) => ({
      id: n.id,
      type: n.type,
      position: n.position,
      data: n.data,
    })),
    edges: graph.edges.map((e) => ({
      id: e.id,
      type: "condition",
      source: e.source,
      target: e.target,
      data: e.data ?? {},
    })),
  };
}

function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
  return {
    specVersion: 3,
    settings: {
      globalPrompt: "",
      defaultLlmResourceId: "",
      defaultAsrResourceId: "",
      defaultTtsResourceId: "",
      toolIds: [],
      knowledgeBaseId: "",
      knowledgeMode: "automatic",
      knowledgeTopN: 5,
      knowledgeScoreThreshold: 0,
    },
    nodes: nodes.map((n) => ({
      id: n.id,
      type: n.type as WorkflowNodeType,
      position: n.position,
      data: n.data as WorkflowNodeData,
    })),
    edges: edges.map((e) => ({
      id: e.id,
      source: e.source,
      target: e.target,
      data: (e.data ?? {
        mode: "always",
        priority: 10,
      }) as WorkflowGraph["edges"][number]["data"],
    })),
  };
}

function Canvas({
  value,
  onChange,
  settings,
  onSettingsChange,
  modelOptions,
  activeNodeId,
  onOpenDynamicVariables,
  editingNodeId,
  onEditingNodeIdChange,
  editingEdgeId,
  onEditingEdgeIdChange,
  settingsOpen,
  onSettingsOpenChange: setSettingsOpen,
  debugOpen,
  onDebugOpenChange,
  debugPanel,
  specsByType,
  toolOptions = [],
  knowledgeOptions = [],
}: WorkflowEditorProps & { specsByType: NodeSpecMap }) {
  const initial = useMemo(() => toFlow(value ?? defaultGraph()), [value]);
  const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
  const [addOpen, setAddOpen] = useState(false);
  const [addSourceId, setAddSourceId] = useState<string | null>(null);
  const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null);
  const { screenToFlowPosition } = useReactFlow();

  // 回传画布状态给外部(助手 graph)。用 ref 避免把 onChange 放进依赖导致循环。
  const onChangeRef = useRef(onChange);
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);
  useEffect(() => {
    const graph = fromFlow(nodes, edges);
    graph.settings = {
      globalPrompt: settings.globalPrompt,
      defaultLlmResourceId: settings.llm ?? "",
      defaultAsrResourceId: settings.asr ?? "",
      defaultTtsResourceId: settings.tts ?? "",
      toolIds: settings.toolIds,
      knowledgeBaseId: settings.knowledgeBaseId,
      knowledgeMode: settings.knowledgeRetrievalConfig.mode,
      knowledgeTopN: settings.knowledgeRetrievalConfig.topN,
      knowledgeScoreThreshold:
        settings.knowledgeRetrievalConfig.scoreThreshold,
    };
    onChangeRef.current?.(graph);
  }, [
    nodes,
    edges,
    settings.globalPrompt,
    settings.llm,
    settings.asr,
    settings.tts,
    settings.toolIds,
    settings.knowledgeBaseId,
    settings.knowledgeRetrievalConfig,
  ]);

  const onConnect = useCallback(
    (connection: Connection) => {
      const sourceType = nodes.find((node) => node.id === connection.source)?.type;
      const priority =
        edges.filter((edge) => edge.source === connection.source).length * 10 + 10;
      setEdges((eds) =>
        addEdge(
          {
            ...connection,
            id: `e-${connection.source}-${connection.target}-${Date.now()}`,
            type: "condition",
            animated: true,
            data:
              sourceType === "agent"
                ? {
                    mode: "llm",
                    priority,
                    condition: "当前阶段任务已经完成",
                  }
                : { mode: "always", priority },
          },
          eds,
        ),
      );
    },
    [nodes, edges, setEdges],
  );

  // 连线约束:不能连入开始节点(无入边句柄),不能自连。
  const isValidConnection = useCallback(
    (c: Connection | Edge) => {
      if (c.source === c.target) return false;
      const source = nodes.find((n) => n.id === c.source);
      const target = nodes.find((n) => n.id === c.target);
      if (!source || !target) return false;

      const sourceSpec = specsByType[source.type as string];
      const targetSpec = specsByType[target.type as string];
      if (!sourceSpec?.hasSource || !targetSpec?.hasTarget) return false;
      if (edges.some((e) => e.source === c.source && e.target === c.target)) {
        return false;
      }

      const sourceLimit = sourceSpec.constraints.maxOutgoing;
      if (
        sourceLimit !== undefined &&
        edges.filter((e) => e.source === c.source).length >= sourceLimit
      ) {
        return false;
      }
      const targetLimit = targetSpec.constraints.maxIncoming;
      if (
        targetLimit !== undefined &&
        edges.filter((e) => e.target === c.target).length >= targetLimit
      ) {
        return false;
      }
      return true;
    },
    [edges, nodes, specsByType],
  );

  const addNode = useCallback(
    (spec: RuntimeNodeSpec) => {
      nodeSeq += 1;
      const id = `${spec.type}-${Date.now()}-${nodeSeq}`;
      const source = addSourceId
        ? nodes.find((node) => node.id === addSourceId)
        : undefined;
      const position = addPosition
        ? { x: addPosition.x - 125, y: addPosition.y }
        : screenToFlowPosition({
            x: window.innerWidth / 2,
            y: window.innerHeight / 2,
          });
      const data = defaultNodeData(spec);
      setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
      if (source) {
        setEdges((currentEdges) => {
          const priority =
            currentEdges.filter((edge) => edge.source === source.id).length * 10 + 10;
          return addEdge(
            {
              id: `e-${source.id}-${id}-${Date.now()}`,
              source: source.id,
              target: id,
              type: "condition",
              animated: true,
              data:
                source.type === "agent"
                  ? {
                      mode: "llm",
                      priority,
                      condition: "当前阶段任务已经完成",
                    }
                  : { mode: "always", priority },
            },
            currentEdges,
          );
        });
      }
      setAddOpen(false);
      setAddSourceId(null);
      setAddPosition(null);
      setSettingsOpen(false);
      onDebugOpenChange(false);
      onEditingEdgeIdChange(null);
      onEditingNodeIdChange(id);
    },
    [
      addPosition,
      addSourceId,
      nodes,
      onDebugOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      screenToFlowPosition,
      setEdges,
      setNodes,
      setSettingsOpen,
    ],
  );

  const updateNodeData = useCallback(
    (id: string, patch: Partial<WorkflowNodeData>) => {
      setNodes((ns) =>
        ns.map((n) =>
          n.id === id ? { ...n, data: { ...n.data, ...patch } } : n,
        ),
      );
    },
    [setNodes],
  );

  const deleteNode = useCallback(
    (id: string) => {
      if (nodes.find((node) => node.id === id)?.type === "start") return;
      setNodes((ns) => ns.filter((n) => n.id !== id));
      setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
      if (editingNodeId === id) onEditingNodeIdChange(null);
    },
    [editingNodeId, nodes, onEditingNodeIdChange, setNodes, setEdges],
  );

  const handleNodesChange = useCallback(
    (changes: NodeChange[]) => {
      const startIds = new Set(
        nodes.filter((node) => node.type === "start").map((node) => node.id),
      );
      onNodesChange(
        changes.filter(
          (change) => change.type !== "remove" || !startIds.has(change.id),
        ),
      );
    },
    [nodes, onNodesChange],
  );

  const updateEdgeData = useCallback(
    (
      id: string,
      patch: WorkflowGraph["edges"][number]["data"],
    ) => {
      setEdges((es) =>
        es.map((e) =>
          e.id === id ? { ...e, data: { ...(e.data ?? {}), ...patch } } : e,
        ),
      );
    },
    [setEdges],
  );

  const deleteEdge = useCallback(
    (id: string) => {
      setEdges((es) => es.filter((e) => e.id !== id));
      if (editingEdgeId === id) onEditingEdgeIdChange(null);
    },
    [editingEdgeId, onEditingEdgeIdChange, setEdges],
  );

  const canCreateFromSource = useCallback(
    (id: string) => {
      const source = nodes.find((node) => node.id === id);
      if (!source) return false;
      const spec = specsByType[source.type as string];
      if (!spec?.hasSource) return false;
      const outgoingCount = edges.filter((edge) => edge.source === id).length;
      if (
        spec.constraints.maxOutgoing !== undefined &&
        outgoingCount >= spec.constraints.maxOutgoing
      ) {
        return false;
      }
      return source.type === "agent" || outgoingCount === 0;
    },
    [edges, nodes, specsByType],
  );
  const onConnectEnd = useCallback<OnConnectEnd>(
    (event, connectionState) => {
      if (
        connectionState.isValid ||
        connectionState.toNode ||
        connectionState.fromHandle?.type !== "source" ||
        !connectionState.fromNode ||
        !canCreateFromSource(connectionState.fromNode.id)
      ) {
        return;
      }
      const pointer = "changedTouches" in event
        ? event.changedTouches[0]
        : event;
      if (!pointer) return;
      setAddSourceId(connectionState.fromNode.id);
      setAddPosition(
        screenToFlowPosition({ x: pointer.clientX, y: pointer.clientY }),
      );
      setAddOpen(true);
    },
    [canCreateFromSource, screenToFlowPosition],
  );
  const openSettings = useCallback(() => {
    onDebugOpenChange(false);
    onEditingNodeIdChange(null);
    onEditingEdgeIdChange(null);
    setSettingsOpen(true);
  }, [
    onDebugOpenChange,
    onEditingEdgeIdChange,
    onEditingNodeIdChange,
    setSettingsOpen,
  ]);

  const nodeActions = useMemo(
    () => ({
      edit: (id: string) => {
        setSettingsOpen(false);
        onDebugOpenChange(false);
        onEditingEdgeIdChange(null);
        onEditingNodeIdChange(id);
      },
      remove: deleteNode,
    }),
    [
      deleteNode,
      onDebugOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      setSettingsOpen,
    ],
  );
  const edgeActions = useMemo(
    () => ({
      edit: (id: string) => {
        setSettingsOpen(false);
        onDebugOpenChange(false);
        onEditingNodeIdChange(null);
        onEditingEdgeIdChange(id);
      },
      remove: deleteEdge,
    }),
    [
      deleteEdge,
      onDebugOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      setSettingsOpen,
    ],
  );

  const editingNode = nodes.find((n) => n.id === editingNodeId);
  const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
  const editingEdge = edges.find((e) => e.id === editingEdgeId);
  const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
  const canAddSpec = useCallback(
    (spec: RuntimeNodeSpec) => {
      const limit = spec.constraints.maxInstances;
      if (limit === undefined) return true;
      return nodes.filter((node) => node.type === spec.type).length < limit;
    },
    [nodes],
  );

  return (
    <NodeSpecsContext.Provider value={specsByType}>
      <ActiveNodeContext.Provider value={activeNodeId ?? null}>
      <NodeActionContext.Provider value={nodeActions}>
        <EdgeActionContext.Provider value={edgeActions}>
          <div className="relative h-full w-full min-h-[560px]">
            <section className="relative h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft shadow-sm">
              <div
                aria-hidden
                className="pointer-events-none absolute -right-24 -top-24 z-0 h-80 w-80 rounded-full opacity-30 blur-3xl"
                style={{
                  background:
                    "radial-gradient(circle, var(--gradient-sky), transparent 68%)",
                }}
              />
              <div
                aria-hidden
                className="pointer-events-none absolute -bottom-28 left-1/4 z-0 h-72 w-72 rounded-full opacity-25 blur-3xl"
                style={{
                  background:
                    "radial-gradient(circle, var(--gradient-lavender), transparent 68%)",
                }}
              />

              <ReactFlow
                nodes={nodes}
                edges={edges}
                nodeTypes={nodeTypes}
                edgeTypes={edgeTypes}
                onNodesChange={handleNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                onConnectEnd={onConnectEnd}
                isValidConnection={isValidConnection}
                onPaneClick={() => {
                  onEditingEdgeIdChange(null);
                }}
                fitView
                proOptions={{ hideAttribution: true }}
                defaultEdgeOptions={{ type: "condition", animated: true }}
              >
                <Background
                  variant={BackgroundVariant.Dots}
                  gap={22}
                  size={1}
                  color="var(--hairline-strong)"
                />
                <Controls
                  className="!rounded-xl !border !border-hairline !bg-card !shadow-sm [&_button]:!border-hairline [&_button]:!bg-card [&_button]:!text-foreground"
                />
                <Panel position="top-left">
                  <TooltipProvider>
                    <div className="flex flex-col gap-2">
                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            className="h-10 w-10 rounded-full shadow-sm"
                            aria-label="添加节点"
                            onClick={() => {
                              setAddSourceId(null);
                              setAddPosition(null);
                              setAddOpen(true);
                            }}
                          >
                            <Plus size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">添加节点</TooltipContent>
                      </Tooltip>

                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            variant="outline"
                            className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
                            aria-label="工作流设置"
                            onClick={openSettings}
                          >
                            <Settings2 size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">工作流设置</TooltipContent>
                      </Tooltip>

                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            variant="outline"
                            className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
                            aria-label="动态变量"
                            onClick={onOpenDynamicVariables}
                          >
                            <Braces size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">动态变量</TooltipContent>
                      </Tooltip>
                    </div>
                  </TooltipProvider>
                </Panel>
              </ReactFlow>
            </section>

            {/* 添加节点弹窗 */}
            <Dialog
              open={addOpen}
              onOpenChange={(open) => {
                setAddOpen(open);
                if (!open) {
                  setAddSourceId(null);
                  setAddPosition(null);
                }
              }}
            >
              <DialogContent className="gap-0 overflow-hidden border border-hairline bg-card p-0 shadow-2xl sm:max-w-[500px]">
                <DialogHeader className="relative overflow-hidden border-b border-hairline px-6 py-6 pr-16">
                  <div
                    aria-hidden
                    className="pointer-events-none absolute -right-14 -top-16 h-40 w-40 rounded-full opacity-40 blur-3xl"
                    style={{
                      background:
                        "radial-gradient(circle, var(--gradient-sky), transparent 68%)",
                    }}
                  />
                  <div className="relative flex items-start gap-4">
                    <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
                      <Plus size={18} />
                    </div>
                    <div>
                      <div className="caption-label text-muted-soft">
                        节点目录
                      </div>
                      <DialogTitle className="font-display display-sm mt-1 text-ink">
                        添加节点
                      </DialogTitle>
                      <DialogDescription className="mt-2 leading-6">
                        {addSourceId
                          ? "选择节点类型，将在当前节点下方创建并自动连接。"
                          : "选择节点类型并添加到画布中央，随后可编辑内容并建立连线。"}
                      </DialogDescription>
                    </div>
                  </div>
                </DialogHeader>
                <div className="flex max-h-[440px] flex-col gap-3 overflow-y-auto bg-canvas-soft/70 p-4">
                  {addableSpecs.length === 0 ? (
                    <p className="rounded-2xl border border-dashed border-hairline-strong bg-card px-4 py-8 text-center text-sm text-muted-soft">
                      暂无可添加的节点类型。
                    </p>
                  ) : null}
                  {addableSpecs.map((spec) => {
                    const Icon = spec.icon;
                    const canAdd = canAddSpec(spec);
                    return (
                      <button
                        key={spec.type}
                        type="button"
                        disabled={!canAdd}
                        className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md disabled:cursor-not-allowed disabled:opacity-55 disabled:hover:translate-y-0 disabled:hover:border-hairline disabled:hover:shadow-sm"
                        onClick={() => addNode(spec)}
                      >
                        <span
                          aria-hidden
                          className="absolute left-5 right-5 top-0 h-px"
                          style={{
                            background: `linear-gradient(90deg, transparent, var(${accentVar(spec.accent)}), transparent)`,
                          }}
                        />
                        <div
                          className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-foreground transition-transform group-hover:scale-105"
                          style={{
                            background: `color-mix(in srgb, var(${accentVar(spec.accent)}) 28%, var(--surface-strong))`,
                          }}
                        >
                          <Icon size={17} />
                        </div>
                        <div className="min-w-0 flex-1">
                          <div className="flex items-center gap-2 text-sm font-medium text-foreground">
                            <span>{spec.displayName}</span>
                            {!canAdd && (
                              <span className="rounded-full bg-surface-strong px-2 py-0.5 text-[10px] font-normal text-muted-foreground">
                                已添加
                              </span>
                            )}
                          </div>
                          <div className="mt-1 text-xs leading-5 text-muted-foreground">
                            {spec.description}
                          </div>
                        </div>
                        <Plus
                          size={15}
                          className="mt-1 shrink-0 text-muted-soft transition-colors group-hover:text-foreground"
                        />
                      </button>
                    );
                  })}
                </div>
              </DialogContent>
            </Dialog>

            {(debugOpen ||
              settingsOpen ||
              (editingNode && editingSpec) ||
              editingEdge) && (
              <aside className="absolute inset-y-0 right-0 z-40 flex w-1/2 flex-col overflow-hidden rounded-r-2xl border-l border-hairline bg-card shadow-2xl">
                {debugOpen ? (
                  debugPanel
                ) : settingsOpen ||
                  (editingNode && editingSpec) ||
                  editingEdge ? (
                  <>
                    <div className="flex min-h-14 shrink-0 items-center gap-3 border-b border-hairline px-4 py-3">
                      <button
                        type="button"
                        aria-label={
                          settingsOpen
                            ? "关闭工作流设置"
                            : editingEdge
                              ? "关闭边编辑"
                              : "关闭节点编辑"
                        }
                        title="关闭"
                        className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground"
                        onClick={() => {
                          if (settingsOpen) setSettingsOpen(false);
                          else if (editingEdge) onEditingEdgeIdChange(null);
                          else onEditingNodeIdChange(null);
                        }}
                      >
                        <X size={16} />
                      </button>
                      <h2 className="min-w-0 truncate text-sm font-medium text-foreground">
                        {settingsOpen
                          ? "工作流设置"
                          : editingEdge
                            ? "编辑连接条件"
                            : `编辑${editingSpec?.displayName ?? "节点"}`}
                      </h2>
                    </div>
                    <div className="scrollbar-subtle min-h-0 flex-1 overflow-y-auto bg-canvas-soft px-4 pb-5 pt-4">
                      {settingsOpen ? (
                        <SettingsForm
                          settings={settings}
                          onSettingsChange={onSettingsChange}
                          modelOptions={modelOptions}
                          toolOptions={toolOptions}
                          knowledgeOptions={knowledgeOptions}
                        />
                      ) : editingNode && editingSpec ? (
                        <NodeForm
                          key={editingNode.id}
                          panel
                          spec={editingSpec}
                          data={editingNode.data as WorkflowNodeData}
                          toolOptions={toolOptions}
                          knowledgeOptions={knowledgeOptions}
                          llmOptions={modelOptions.llm}
                          asrOptions={modelOptions.asr}
                          ttsOptions={modelOptions.tts}
                          workflowSettings={settings}
                          onChange={(patch) =>
                            updateNodeData(editingNode.id, patch)
                          }
                        />
                      ) : editingEdge ? (
                        <EdgeForm
                          key={editingEdge.id}
                          edge={editingEdge}
                          sourceType={nodes.find((node) => node.id === editingEdge.source)?.type}
                          onChange={(patch) =>
                            updateEdgeData(editingEdge.id, patch)
                          }
                        />
                      ) : null}
                    </div>
                  </>
                ) : null}
              </aside>
            )}
          </div>
        </EdgeActionContext.Provider>
      </NodeActionContext.Provider>
      </ActiveNodeContext.Provider>
    </NodeSpecsContext.Provider>
  );
}

function SettingsForm({
  settings,
  onSettingsChange,
  modelOptions,
  toolOptions,
  knowledgeOptions,
}: {
  settings: WorkflowSettings;
  onSettingsChange: (settings: WorkflowSettings) => void;
  modelOptions: WorkflowEditorProps["modelOptions"];
  toolOptions: ModelOption[];
  knowledgeOptions: ModelOption[];
}) {
  return (
    <div className="space-y-3">
      <SectionCard
        icon={<MessageSquareText size={15} />}
        title="全局提示词"
        description="与所有选择继承全局配置的 Agent 节点提示词合并"
      >
        <Textarea
          rows={4}
          value={settings.globalPrompt}
          onChange={(event) =>
            onSettingsChange({
              ...settings,
              globalPrompt: event.target.value,
            })
          }
          className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
        />
      </SectionCard>

      <SectionCard
        icon={<Brain size={15} />}
        title="模型配置"
        description="工作流中所有 Agent 共用的大语言模型"
      >
        <ModelSelect
          label="大语言模型"
          value={settings.llm}
          options={modelOptions.llm}
          onChange={(v) => onSettingsChange({ ...settings, llm: v })}
        />
      </SectionCard>

      <SectionCard
        icon={<AudioLines size={15} />}
        title="语音配置"
        description="Agent 节点未单独选择资源时继承这里的默认值"
      >
        <ModelSelect
          label="语音识别"
          value={settings.asr}
          options={modelOptions.asr}
          onChange={(v) => onSettingsChange({ ...settings, asr: v })}
        />
        <ModelSelect
          label="语音合成"
          value={settings.tts}
          options={modelOptions.tts}
          onChange={(v) => onSettingsChange({ ...settings, tts: v })}
        />
      </SectionCard>

      <SectionCard
        icon={<Database size={15} />}
        title="知识库配置"
        description="所有继承全局配置的 Agent 都可以使用该知识库"
      >
        <div className="flex items-center gap-1.5">
          <span className="text-sm font-medium text-foreground">知识库选择</span>
          <KnowledgeRetrievalConfigDialog
            disabled={!settings.knowledgeBaseId}
            value={settings.knowledgeRetrievalConfig}
            onChange={(knowledgeRetrievalConfig) =>
              onSettingsChange({ ...settings, knowledgeRetrievalConfig })
            }
          />
        </div>
        <NodeSelect
          label=""
          value={settings.knowledgeBaseId}
          options={knowledgeOptions}
          onChange={(knowledgeBaseId) =>
            onSettingsChange({
              ...settings,
              knowledgeBaseId: knowledgeBaseId || "",
            })
          }
          noneLabel="无"
        />
      </SectionCard>

      <SectionCard
        icon={<Wrench size={15} />}
        title="工具"
        description="所有继承全局配置的 Agent 都可以调用这些工具"
      >
        <ToolOptionPicker
          options={toolOptions}
          selectedIds={settings.toolIds}
          onChange={(toolIds) => onSettingsChange({ ...settings, toolIds })}
        />
      </SectionCard>

      <SectionCard
        icon={<Sparkles size={15} />}
        title="交互策略"
        description="设置实时视频对话时的交互体验"
      >
        <TurnConfigEditor
          enabled={settings.allowInterrupt}
          config={settings.turnConfig}
          onEnabledChange={(allowInterrupt) =>
            onSettingsChange({ ...settings, allowInterrupt })
          }
          onConfigChange={(turnConfig) =>
            onSettingsChange({ ...settings, turnConfig })
          }
        />
      </SectionCard>
    </div>
  );
}

function ModelSelect({
  label,
  value,
  options,
  onChange,
}: {
  label: string;
  value?: string;
  options: ModelOption[];
  onChange: (value: string | undefined) => void;
}) {
  return (
    <div className="block">
      {label && (
        <div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
      )}
      <Select
        value={value ?? NONE}
        onValueChange={(v) => onChange(v === NONE ? undefined : v)}
      >
        <SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
          <SelectValue placeholder="选择模型资源" />
        </SelectTrigger>
        <SelectContent className="border-hairline bg-popover text-popover-foreground">
          <SelectItem value={NONE}>未选择</SelectItem>
          {options.map((o) => (
            <SelectItem key={o.value} value={o.value}>
              {o.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

function NodeForm({
  panel = false,
  spec,
  data,
  toolOptions,
  knowledgeOptions,
  llmOptions,
  asrOptions,
  ttsOptions,
  workflowSettings,
  onChange,
}: {
  panel?: boolean;
  spec: RuntimeNodeSpec;
  data: WorkflowNodeData;
  toolOptions: ModelOption[];
  knowledgeOptions: ModelOption[];
  llmOptions: ModelOption[];
  asrOptions: ModelOption[];
  ttsOptions: ModelOption[];
  workflowSettings: WorkflowSettings;
  onChange: (patch: WorkflowNodeData) => void;
}) {
  const [draft, setDraft] = useState<WorkflowNodeData>({ ...data });
  const [argumentsJson, setArgumentsJson] = useState(
    JSON.stringify(data.arguments ?? {}, null, 2),
  );
  const [assignmentsJson, setAssignmentsJson] = useState(
    JSON.stringify(data.resultAssignments ?? {}, null, 2),
  );
  const [jsonError, setJsonError] = useState("");
  const commit = (next: WorkflowNodeData) => {
    setDraft(next);
    onChange(next);
  };
  const set = (key: string, val: unknown) =>
    commit({ ...draft, [key]: val });
  const setPatch = (patch: Partial<WorkflowNodeData>) =>
    commit({ ...draft, ...patch });
  const commitActionJson = (
    nextArgumentsJson: string,
    nextAssignmentsJson: string,
  ) => {
    try {
      const args = JSON.parse(nextArgumentsJson) as Record<string, unknown>;
      const assignments = JSON.parse(nextAssignmentsJson) as Record<string, string>;
      if (Array.isArray(args) || Array.isArray(assignments)) throw new Error();
      setJsonError("");
      commit({ ...draft, arguments: args, resultAssignments: assignments });
    } catch {
      setJsonError("参数和结果映射必须是 JSON 对象");
    }
  };

  if (panel) {
    if (spec.type !== "agent") {
      return (
        <WorkflowNodePanelForm
          spec={spec}
          draft={draft}
          set={set}
          toolOptions={toolOptions}
          argumentsJson={argumentsJson}
          assignmentsJson={assignmentsJson}
          jsonError={jsonError}
          setArgumentsJson={setArgumentsJson}
          setAssignmentsJson={setAssignmentsJson}
          commitActionJson={commitActionJson}
        />
      );
    }
    return (
      <AgentPanelForm
        draft={draft}
        set={set}
        setPatch={setPatch}
        workflowSettings={workflowSettings}
        toolOptions={toolOptions}
        knowledgeOptions={knowledgeOptions}
        llmOptions={llmOptions}
        asrOptions={asrOptions}
        ttsOptions={ttsOptions}
      />
    );
  }

  return (
    <>
      {!panel && (
        <DialogHeader>
          <DialogTitle className="font-display text-ink">
            编辑{spec.displayName}
          </DialogTitle>
        </DialogHeader>
      )}

      <div className="flex flex-col gap-5 py-2">
        {spec.fields.map((field) => {
          const raw = draft[field.key];
          if (field.type === "switch") {
            return (
              <label
                key={field.key}
                className="flex items-center justify-between gap-3"
              >
                <span className="text-sm font-medium text-foreground">
                  {field.label}
                </span>
                <Switch
                  checked={Boolean(raw)}
                  onCheckedChange={(checked) => set(field.key, checked)}
                />
              </label>
            );
          }
          return (
            <div key={field.key} className="flex flex-col gap-2">
              <label className="text-sm font-medium text-foreground">
                {field.label}
                {field.required && <span className="text-destructive"> *</span>}
              </label>
              {field.type === "textarea" ? (
                <Textarea
                  rows={4}
                  value={(raw as string) ?? ""}
                  onChange={(e) => set(field.key, e.target.value)}
                  className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
              ) : (
                <Input
                  value={(raw as string) ?? ""}
                  onChange={(e) => set(field.key, e.target.value)}
                  className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
              )}
            </div>
          );
        })}

        {spec.type === "agent" && (
          <>
            <NodeSelect
              label="进入节点时"
              value={(draft.entryMode as string) || "wait_user"}
              options={[
                { value: "wait_user", label: "等待用户说话（默认）" },
                { value: "generate", label: "立即让 LLM 回复" },
                { value: "fixed_speech", label: "播放固定进入语" },
              ]}
              onChange={(value) => set("entryMode", value || "wait_user")}
              allowNone={false}
            />
            {draft.entryMode === "fixed_speech" && (
              <div className="block">
                <div className="mb-2 text-sm font-medium text-foreground">
                  固定进入语 <span className="text-destructive">*</span>
                </div>
                <Textarea
                  rows={3}
                  value={draft.entrySpeech ?? ""}
                  onChange={(event) => set("entrySpeech", event.target.value)}
                  placeholder="例如：您好，请告诉我需要处理的问题。"
                  className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
                <span className="mt-2 block text-xs text-muted-soft">
                  支持使用 {"{{variable}}"} 动态变量；只播放语音，不调用 LLM。
                </span>
              </div>
            )}
            <div className="flex flex-col gap-2">
              <label className="text-sm font-medium text-foreground">可用工具</label>
              <ToolOptionPicker
                options={toolOptions}
                selectedIds={draft.toolIds ?? []}
                onChange={(toolIds) => set("toolIds", toolIds)}
              />
            </div>
            <NodeSelect
              label="知识库"
              value={(draft.knowledgeBaseId as string) || ""}
              options={knowledgeOptions}
              onChange={(value) => set("knowledgeBaseId", value || "")}
            />
            <NodeSelect
              label="知识库模式"
              value={(draft.knowledgeMode as string) || "disabled"}
              options={[
                { value: "disabled", label: "关闭" },
                { value: "automatic", label: "每轮自动检索" },
                { value: "on_demand", label: "Agent 按需调用" },
              ]}
              onChange={(value) => set("knowledgeMode", value || "disabled")}
              allowNone={false}
            />
            <NodeSelect
              label="ASR 资源"
              value={(draft.asrResourceId as string) || ""}
              options={asrOptions}
              onChange={(value) => set("asrResourceId", value || "")}
              noneLabel="继承 Workflow 默认值"
            />
            <NodeSelect
              label="TTS 资源"
              value={(draft.ttsResourceId as string) || ""}
              options={ttsOptions}
              onChange={(value) => set("ttsResourceId", value || "")}
              noneLabel="继承 Workflow 默认值"
            />
          </>
        )}

        {spec.type === "action" && (
          <>
            <NodeSelect
              label="确定性执行工具"
              value={(draft.toolId as string) || ""}
              options={toolOptions}
              onChange={(value) => set("toolId", value || "")}
              noneLabel="请选择工具"
            />
            <div className="flex flex-col gap-2">
              <label className="text-sm font-medium text-foreground">工具参数 JSON</label>
              <Textarea
                rows={5}
                value={argumentsJson}
                onChange={(event) => {
                  const value = event.target.value;
                  setArgumentsJson(value);
                  commitActionJson(value, assignmentsJson);
                }}
                className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
              />
              <span className="text-xs text-muted-soft">字符串值可使用 {"{{variable}}"} 动态变量。</span>
            </div>
            <div className="flex flex-col gap-2">
              <label className="text-sm font-medium text-foreground">结果变量映射 JSON</label>
              <Textarea
                rows={4}
                value={assignmentsJson}
                onChange={(event) => {
                  const value = event.target.value;
                  setAssignmentsJson(value);
                  commitActionJson(argumentsJson, value);
                }}
                className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
              />
              <span className="text-xs text-muted-soft">格式：变量名 → 响应 JSON Path。</span>
            </div>
            {jsonError && <span className="text-xs text-destructive">{jsonError}</span>}
          </>
        )}

        {spec.type === "handoff" && (
          <NodeSelect
            label="转交类型"
            value={(draft.targetType as string) || "human"}
            options={[
              { value: "ai", label: "其他 AI" },
              { value: "human", label: "人工" },
              { value: "queue", label: "队列" },
              { value: "phone", label: "电话" },
            ]}
            onChange={(value) => set("targetType", value || "human")}
            allowNone={false}
          />
        )}

        {spec.type === "end" && (
          <NodeSelect
            label="结束范围"
            value={(draft.scope as string) || "session"}
            options={[
              { value: "flow", label: "仅结束 AI 流程" },
              { value: "session", label: "结束音视频会话" },
            ]}
            onChange={(value) => set("scope", value || "session")}
            allowNone={false}
          />
        )}
      </div>
    </>
  );
}

function WorkflowNodePanelForm({
  spec,
  draft,
  set,
  toolOptions,
  argumentsJson,
  assignmentsJson,
  jsonError,
  setArgumentsJson,
  setAssignmentsJson,
  commitActionJson,
}: {
  spec: RuntimeNodeSpec;
  draft: WorkflowNodeData;
  set: (key: string, val: unknown) => void;
  toolOptions: ModelOption[];
  argumentsJson: string;
  assignmentsJson: string;
  jsonError: string;
  setArgumentsJson: (value: string) => void;
  setAssignmentsJson: (value: string) => void;
  commitActionJson: (argumentsValue: string, assignmentsValue: string) => void;
}) {
  return (
    <div className="space-y-3">
      <SectionCard
        icon={<Tag size={15} />}
        title="节点信息"
        description="节点在画布上显示的名称"
      >
        <label className="block">
          <div className="mb-1.5 text-sm font-medium text-foreground">
            节点名称
          </div>
          <Input
            value={draft.name ?? ""}
            onChange={(event) => set("name", event.target.value)}
            className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
          />
        </label>
      </SectionCard>

      {spec.type === "start" && (
        <SectionCard
          icon={<Play size={15} />}
          title="开场白"
          description="会话建立后首先向用户播放的固定内容"
        >
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              固定开场白
            </div>
            <Textarea
              rows={5}
              value={draft.greeting ?? ""}
              onChange={(event) => set("greeting", event.target.value)}
              placeholder="例如：你好，我是 AI 视频助手，有什么可以帮你？"
              className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
            />
          </label>
        </SectionCard>
      )}

      {spec.type === "action" && (
        <SectionCard
          icon={<Zap size={15} />}
          title="工具执行"
          description="确定性调用工具，并将响应字段写入会话动态变量"
        >
          <NodeSelect
            label="执行工具"
            value={(draft.toolId as string) || ""}
            options={toolOptions}
            onChange={(value) => set("toolId", value || "")}
            noneLabel="请选择工具"
          />
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              工具参数 JSON
            </div>
            <Textarea
              rows={5}
              value={argumentsJson}
              onChange={(event) => {
                const value = event.target.value;
                setArgumentsJson(value);
                commitActionJson(value, assignmentsJson);
              }}
              className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background font-mono text-xs text-foreground placeholder:text-muted-soft"
            />
            <span className="mt-1.5 block text-xs text-muted-foreground">
              字符串值可使用 {"{{variable}}"} 动态变量。
            </span>
          </label>
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              结果变量映射 JSON
            </div>
            <Textarea
              rows={4}
              value={assignmentsJson}
              onChange={(event) => {
                const value = event.target.value;
                setAssignmentsJson(value);
                commitActionJson(argumentsJson, value);
              }}
              className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background font-mono text-xs text-foreground placeholder:text-muted-soft"
            />
            <span className="mt-1.5 block text-xs text-muted-foreground">
              格式：变量名 → 响应 JSON Path。
            </span>
          </label>
          {jsonError && (
            <p role="alert" className="text-xs text-destructive">
              {jsonError}
            </p>
          )}
        </SectionCard>
      )}

      {spec.type === "handoff" && (
        <SectionCard
          icon={<PhoneForwarded size={15} />}
          title="转交配置"
          description="发送转交事件并继续执行工作流"
        >
          <NodeSelect
            label="转交类型"
            value={(draft.targetType as string) || "human"}
            options={[
              { value: "ai", label: "其他 AI" },
              { value: "human", label: "人工" },
              { value: "queue", label: "队列" },
              { value: "phone", label: "电话" },
            ]}
            onChange={(value) => set("targetType", value || "human")}
            allowNone={false}
          />
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              转交目标
            </div>
            <Input
              value={draft.target ?? ""}
              onChange={(event) => set("target", event.target.value)}
              placeholder="人工、队列、AI 或电话号码"
              className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
            />
          </label>
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              转交提示
            </div>
            <Textarea
              rows={4}
              value={draft.message ?? ""}
              onChange={(event) => set("message", event.target.value)}
              placeholder="例如：好的，正在为你转接人工客服。"
              className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
            />
          </label>
        </SectionCard>
      )}

      {spec.type === "end" && (
        <SectionCard
          icon={<Flag size={15} />}
          title="结束配置"
          description="结束工作流，并可在结束前播放一段固定回复"
        >
          <NodeSelect
            label="结束范围"
            value={(draft.scope as string) || "session"}
            options={[
              { value: "flow", label: "仅结束 AI 流程" },
              { value: "session", label: "结束音视频会话" },
            ]}
            onChange={(value) => set("scope", value || "session")}
            allowNone={false}
          />
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              固定结束语
            </div>
            <Textarea
              rows={4}
              value={draft.message ?? ""}
              onChange={(event) => set("message", event.target.value)}
              placeholder="例如：感谢你的来电，再见。"
              className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
            />
          </label>
        </SectionCard>
      )}
    </div>
  );
}

/** Agent 右栏：与 AssistantPage 共用紧凑 SectionCard。 */
function AgentPanelForm({
  draft,
  set,
  setPatch,
  workflowSettings,
  toolOptions,
  knowledgeOptions,
  llmOptions,
  asrOptions,
  ttsOptions,
}: {
  draft: WorkflowNodeData;
  set: (key: string, val: unknown) => void;
  setPatch: (patch: Partial<WorkflowNodeData>) => void;
  workflowSettings: WorkflowSettings;
  toolOptions: ModelOption[];
  knowledgeOptions: ModelOption[];
  llmOptions: ModelOption[];
  asrOptions: ModelOption[];
  ttsOptions: ModelOption[];
}) {
  const inheritsGlobal = draft.inheritGlobalConfig !== false;
  const knowledgeConfig: KnowledgeRetrievalConfig = {
    mode:
      draft.knowledgeMode === "on_demand" ? "on_demand" : "automatic",
    topN: Number(draft.knowledgeTopN ?? 5),
    scoreThreshold: Number(draft.knowledgeScoreThreshold ?? 0),
  };

  const setInheritance = (inheritGlobalConfig: boolean) => {
    if (inheritGlobalConfig) {
      setPatch({ inheritGlobalConfig: true });
      return;
    }
    setPatch({
      inheritGlobalConfig: false,
      llmResourceId:
        (draft.llmResourceId as string) || workflowSettings.llm || "",
      asrResourceId:
        (draft.asrResourceId as string) || workflowSettings.asr || "",
      ttsResourceId:
        (draft.ttsResourceId as string) || workflowSettings.tts || "",
      toolIds: draft.toolIds?.length
        ? draft.toolIds
        : workflowSettings.toolIds,
      knowledgeBaseId:
        (draft.knowledgeBaseId as string) ||
        workflowSettings.knowledgeBaseId,
      knowledgeMode:
        draft.knowledgeMode === "on_demand" ||
        draft.knowledgeMode === "automatic"
          ? draft.knowledgeMode
          : workflowSettings.knowledgeRetrievalConfig.mode,
      knowledgeTopN:
        draft.knowledgeTopN ??
        workflowSettings.knowledgeRetrievalConfig.topN,
      knowledgeScoreThreshold:
        draft.knowledgeScoreThreshold ??
        workflowSettings.knowledgeRetrievalConfig.scoreThreshold,
    });
  };

  return (
    <div className="space-y-3">
      <SectionCard
        icon={<Tag size={15} />}
        title="节点信息"
        description="节点在画布上显示的名称"
      >
        <label className="block">
          <div className="mb-1.5 text-sm font-medium text-foreground">节点名称</div>
          <Input
            value={draft.name ?? ""}
            onChange={(event) => set("name", event.target.value)}
            className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
          />
        </label>
      </SectionCard>

      <SectionCard
        icon={<Settings2 size={15} />}
        title="配置范围"
        description="默认复用工作流全局的模型、语音、知识库和工具"
      >
        <div className="flex items-center justify-between gap-4 rounded-xl border border-hairline bg-canvas-soft px-3.5 py-3">
          <div>
            <div className="text-sm font-medium text-foreground">
              继承全局配置
            </div>
            <p className="mt-1 text-xs leading-5 text-muted-foreground">
              {inheritsGlobal
                ? "全局提示词会与当前节点提示词合并。"
                : "当前节点使用独立的完整助手配置。"}
            </p>
          </div>
          <Switch checked={inheritsGlobal} onCheckedChange={setInheritance} />
        </div>
      </SectionCard>

      <SectionCard
        icon={<MessageSquareText size={15} />}
        title="提示词"
        description={
          inheritsGlobal
            ? "描述当前阶段任务，并与工作流全局提示词合并"
            : "描述当前独立助手的角色、能力和回答要求"
        }
      >
        <Textarea
          rows={8}
          value={draft.prompt ?? ""}
          onChange={(event) => set("prompt", event.target.value)}
          placeholder="请输入提示词，描述助手的角色、能力和回答要求"
          className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
        />
      </SectionCard>

      <SectionCard
        icon={<Bot size={15} />}
        title="进入行为"
        description="进入该节点时的首轮交互方式"
      >
        <NodeSelect
          label="进入节点时"
          value={(draft.entryMode as string) || "wait_user"}
          options={[
            { value: "wait_user", label: "等待用户说话（默认）" },
            { value: "generate", label: "立即让 LLM 回复" },
            { value: "fixed_speech", label: "播放固定进入语" },
          ]}
          onChange={(value) => set("entryMode", value || "wait_user")}
          allowNone={false}
        />
        {draft.entryMode === "fixed_speech" && (
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              固定进入语 <span className="text-destructive">*</span>
            </div>
            <Textarea
              rows={3}
              value={draft.entrySpeech ?? ""}
              onChange={(event) => set("entrySpeech", event.target.value)}
              placeholder="例如：您好，请告诉我需要处理的问题。"
              className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
            />
            <span className="mt-1.5 block text-xs text-muted-foreground">
              支持使用 {"{{variable}}"} 动态变量；只播放语音，不调用 LLM。
            </span>
          </label>
        )}
      </SectionCard>

      {!inheritsGlobal && (
        <>
          <SectionCard
            icon={<Brain size={15} />}
            title="模型与语音"
            description="当前 Agent 独立使用的推理、语音识别和语音合成资源"
          >
            <NodeSelect
              label="大语言模型"
              value={(draft.llmResourceId as string) || ""}
              options={llmOptions}
              onChange={(value) => set("llmResourceId", value || "")}
              noneLabel="请选择模型"
            />
            <NodeSelect
              label="语音识别"
              value={(draft.asrResourceId as string) || ""}
              options={asrOptions}
              onChange={(value) => set("asrResourceId", value || "")}
              noneLabel="请选择语音识别"
            />
            <NodeSelect
              label="语音合成"
              value={(draft.ttsResourceId as string) || ""}
              options={ttsOptions}
              onChange={(value) => set("ttsResourceId", value || "")}
              noneLabel="请选择语音合成"
            />
          </SectionCard>

          <SectionCard
            icon={<Database size={15} />}
            title="知识库配置"
            description="选择该阶段回答时可检索的业务知识来源"
          >
            <div className="flex items-center gap-1.5">
              <span className="text-sm font-medium text-foreground">
                知识库选择
              </span>
              <KnowledgeRetrievalConfigDialog
                disabled={!draft.knowledgeBaseId}
                value={knowledgeConfig}
                onChange={(config) =>
                  setPatch({
                    knowledgeMode: config.mode,
                    knowledgeTopN: config.topN,
                    knowledgeScoreThreshold: config.scoreThreshold,
                  })
                }
              />
            </div>
            <NodeSelect
              label=""
              value={(draft.knowledgeBaseId as string) || ""}
              options={knowledgeOptions}
              onChange={(value) => set("knowledgeBaseId", value || "")}
              noneLabel="无"
            />
          </SectionCard>

          <SectionCard
            icon={<Wrench size={15} />}
            title="工具"
            description="配置该阶段可以调用的工具"
          >
            <ToolOptionPicker
              options={toolOptions}
              selectedIds={draft.toolIds ?? []}
              onChange={(toolIds) => set("toolIds", toolIds)}
            />
          </SectionCard>
        </>
      )}
    </div>
  );
}

function ToolOptionPicker({
  options,
  selectedIds,
  onChange,
}: {
  options: ModelOption[];
  selectedIds: string[];
  onChange: (ids: string[]) => void;
}) {
  const [open, setOpen] = useState(false);
  const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
  const selected = selectedIds
    .map((id) => options.find((option) => option.value === id))
    .filter((option): option is ModelOption => Boolean(option));

  return (
    <>
      <div className="flex min-h-9 flex-wrap items-center gap-2">
        {selected.map((option) => (
          <div
            key={option.value}
            className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
          >
            <Wrench size={14} />
            <span className="max-w-48 truncate">{option.label}</span>
            <button
              type="button"
              onClick={() =>
                onChange(selectedIds.filter((id) => id !== option.value))
              }
              className="text-muted-soft transition-colors hover:text-foreground"
              aria-label={`移除工具 ${option.label}`}
            >
              <X size={13} />
            </button>
          </div>
        ))}
        <Button
          type="button"
          variant="outline"
          size="icon-sm"
          className="border-hairline-strong text-muted-foreground hover:text-foreground"
          onClick={() => {
            setDraftIds(selectedIds);
            setOpen(true);
          }}
          aria-label="添加工具"
          title="添加工具"
        >
          <Plus size={15} />
        </Button>
      </div>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-xl">
          <DialogHeader>
            <DialogTitle>选择工具</DialogTitle>
            <DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
          </DialogHeader>
          {options.length === 0 ? (
            <div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
              暂无可用工具
            </div>
          ) : (
            <div className="max-h-80 divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
              {options.map((option) => {
                const checked = draftIds.includes(option.value);
                return (
                  <label
                    key={option.value}
                    className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-surface-strong/40"
                  >
                    <input
                      type="checkbox"
                      checked={checked}
                      onChange={() =>
                        setDraftIds((current) =>
                          checked
                            ? current.filter((id) => id !== option.value)
                            : [...current, option.value],
                        )
                      }
                      className="size-4 accent-primary"
                    />
                    <span className="truncate font-medium text-foreground">
                      {option.label}
                    </span>
                  </label>
                );
              })}
            </div>
          )}
          <DialogFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              取消
            </Button>
            <Button
              onClick={() => {
                onChange(draftIds);
                setOpen(false);
              }}
            >
              确认
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

function NodeSelect({
  label,
  value,
  options,
  onChange,
  allowNone = true,
  noneLabel = "未选择",
}: {
  label: string;
  value: string;
  options: ModelOption[];
  onChange: (value: string | undefined) => void;
  allowNone?: boolean;
  noneLabel?: string;
}) {
  return (
    <div className="block">
      {label && (
        <div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
      )}
      <Select
        value={value || (allowNone ? NONE : options[0]?.value)}
        onValueChange={(next) => onChange(next === NONE ? undefined : next)}
      >
        <SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
          <SelectValue placeholder={label ? `请选择${label}` : "请选择"} />
        </SelectTrigger>
        <SelectContent className="border-hairline bg-popover text-popover-foreground">
          {allowNone && <SelectItem value={NONE}>{noneLabel}</SelectItem>}
          {options.map((option) => (
            <SelectItem key={option.value} value={option.value}>
              {option.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

function EdgeForm({
  edge,
  sourceType,
  onChange,
}: {
  edge: Edge;
  sourceType?: string;
  onChange: (patch: WorkflowEdgeData) => void;
}) {
  const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
  const [mode, setMode] = useState(data.mode ?? "always");
  const [priority, setPriority] = useState(data.priority ?? 10);
  const [label, setLabel] = useState(data.label ?? "");
  const [condition, setCondition] = useState(data.condition ?? "");
  const [transitionSpeech, setTransitionSpeech] = useState(data.transitionSpeech ?? "");
  const [combinator, setCombinator] = useState<"and" | "or">(
    data.expression?.combinator ?? "and",
  );
  const [rules, setRules] = useState<ExpressionRule[]>(
    data.expression?.rules?.length
      ? data.expression.rules
      : [{ variable: "", operator: "eq", value: "" }],
  );

  const publish = ({
    nextMode = mode,
    nextPriority = priority,
    nextLabel = label,
    nextCondition = condition,
    nextTransitionSpeech = transitionSpeech,
    nextCombinator = combinator,
    nextRules = rules,
  }: {
    nextMode?: WorkflowEdgeData["mode"];
    nextPriority?: number;
    nextLabel?: string;
    nextCondition?: string;
    nextTransitionSpeech?: string;
    nextCombinator?: "and" | "or";
    nextRules?: ExpressionRule[];
  }) =>
    onChange({
      mode: nextMode,
      priority: nextPriority,
      label: nextLabel.trim() ? nextLabel : undefined,
      condition: nextMode === "llm" ? nextCondition : undefined,
      expression:
        nextMode === "expression"
          ? { combinator: nextCombinator, rules: nextRules }
          : undefined,
      transitionSpeech: nextTransitionSpeech.trim()
        ? nextTransitionSpeech
        : undefined,
    });

  const setRule = (index: number, patch: Partial<ExpressionRule>) => {
    const nextRules = rules.map((rule, ruleIndex) =>
      ruleIndex === index ? { ...rule, ...patch } : rule,
    );
    setRules(nextRules);
    publish({ nextRules });
  };

  const parseValue = (value: string): unknown => {
    if (value === "true") return true;
    if (value === "false") return false;
    if (value !== "" && Number.isFinite(Number(value))) return Number(value);
    return value;
  };

  return (
    <div className="space-y-3">
      <SectionCard
        icon={<GitBranch size={15} />}
        title="路由方式"
        description="选择由 Agent 判断、动态变量表达式判断，或作为确定性默认路径"
      >
        <NodeSelect
          label="判断方式"
          value={mode}
          options={[
            ...(sourceType === "agent" ? [{ value: "llm", label: "LLM 判断" }] : []),
            { value: "expression", label: "动态变量表达式" },
            { value: "always", label: "默认路径" },
          ]}
          onChange={(value) => {
            const nextMode =
              (value as WorkflowEdgeData["mode"]) || "always";
            setMode(nextMode);
            publish({ nextMode });
          }}
          allowNone={false}
        />
        <label className="block">
          <div className="mb-1.5 text-sm font-medium text-foreground">
            优先级
          </div>
          <Input
            type="number"
            value={priority}
            onChange={(event) => {
              const nextPriority = Number(event.target.value) || 0;
              setPriority(nextPriority);
              publish({ nextPriority });
            }}
            className="border-hairline-strong bg-background text-foreground"
          />
          <span className="mt-1.5 block text-xs text-muted-foreground">
            同一节点的边按数字从小到大依次判断。
          </span>
        </label>
      </SectionCard>

      <SectionCard
        icon={<Braces size={15} />}
        title="触发条件"
        description="配置画布标签以及这条连接被命中的条件"
      >
        <label className="block">
          <div className="mb-1.5 text-sm font-medium text-foreground">
            条件标签
          </div>
          <Input
            value={label}
            maxLength={64}
            placeholder="例如：用户想转人工"
            onChange={(event) => {
              const nextLabel = event.target.value;
              setLabel(nextLabel);
              publish({ nextLabel });
            }}
            className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
          />
          <span className="mt-1.5 block text-xs text-muted-foreground">
            用于画布和日志中识别该路径，{label.length}/64
          </span>
        </label>

        {mode === "llm" && (
          <label className="block">
            <div className="mb-1.5 text-sm font-medium text-foreground">
              自然语言条件 <span className="text-destructive">*</span>
            </div>
            <Textarea
              rows={4}
              value={condition}
              placeholder="例如：用户已经明确表示需要人工客服。"
              onChange={(event) => {
                const nextCondition = event.target.value;
                setCondition(nextCondition);
                publish({ nextCondition });
              }}
              className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
            />
            {!condition.trim() && (
              <span className="mt-1.5 block text-xs text-destructive">
                LLM 判断需要填写触发条件。
              </span>
            )}
          </label>
        )}

        {mode === "expression" && (
          <div className="space-y-3">
            <NodeSelect
              label="规则组合"
              value={combinator}
              options={[
                { value: "and", label: "全部满足（AND）" },
                { value: "or", label: "任一满足（OR）" },
              ]}
              onChange={(value) => {
                const nextCombinator = value === "or" ? "or" : "and";
                setCombinator(nextCombinator);
                publish({ nextCombinator });
              }}
              allowNone={false}
            />
            {rules.map((rule, index) => (
              <div
                key={index}
                className="space-y-2 rounded-xl border border-hairline bg-canvas-soft p-3"
              >
                <Input
                  value={rule.variable}
                  placeholder="动态变量名"
                  onChange={(event) => setRule(index, { variable: event.target.value })}
                  className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
                <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-2">
                  <Select
                    value={rule.operator}
                    onValueChange={(value) =>
                      setRule(index, {
                        operator: value as ExpressionRule["operator"],
                      })
                    }
                  >
                    <SelectTrigger className="border-hairline-strong bg-background">
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      {[
                        "eq",
                        "neq",
                        "gt",
                        "gte",
                        "lt",
                        "lte",
                        "contains",
                        "in",
                        "exists",
                      ].map((operator) => (
                        <SelectItem key={operator} value={operator}>
                          {operator}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                  <Input
                    disabled={rule.operator === "exists"}
                    value={rule.value == null ? "" : String(rule.value)}
                    placeholder="比较值"
                    onChange={(event) =>
                      setRule(index, {
                        value: parseValue(event.target.value),
                      })
                    }
                    className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                  />
                  <Button
                    type="button"
                    size="icon"
                    variant="outline"
                    disabled={rules.length === 1}
                    aria-label={`删除第 ${index + 1} 条规则`}
                    onClick={() => {
                      const nextRules = rules.filter(
                        (_, ruleIndex) => ruleIndex !== index,
                      );
                      setRules(nextRules);
                      publish({ nextRules });
                    }}
                  >
                    <Trash2 size={14} />
                  </Button>
                </div>
                {!rule.variable.trim() && (
                  <span className="text-xs text-destructive">
                    请输入动态变量名。
                  </span>
                )}
              </div>
            ))}
            <Button
              type="button"
              variant="outline"
              className="w-full gap-2 border-hairline-strong"
              onClick={() => {
                const nextRules = [
                  ...rules,
                  { variable: "", operator: "eq", value: "" } as ExpressionRule,
                ];
                setRules(nextRules);
                publish({ nextRules });
              }}
            >
              <Plus size={14} />
              添加规则
            </Button>
          </div>
        )}

        {mode === "always" && (
          <p className="rounded-xl border border-hairline bg-canvas-soft px-3.5 py-3 text-sm leading-6 text-muted-foreground">
            当前节点没有其它条件命中时，将沿这条默认路径继续执行。
          </p>
        )}
      </SectionCard>

      <SectionCard
        icon={<MessageSquareText size={15} />}
        title="过渡语"
        description="命中连接后、进入下一节点前播放的固定内容"
      >
        <label className="block">
          <div className="mb-1.5 text-sm font-medium text-foreground">
            固定过渡语（可选）
          </div>
          <Textarea
            rows={3}
            value={transitionSpeech}
            placeholder="例如：好的，正在为你转接。"
            onChange={(event) => {
              const nextTransitionSpeech = event.target.value;
              setTransitionSpeech(nextTransitionSpeech);
              publish({ nextTransitionSpeech });
            }}
            className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
          />
          <span className="mt-1.5 block text-xs text-muted-foreground">
            会显示在调试与完整聊天记录中，同时使用当前 TTS 播放。
          </span>
        </label>
      </SectionCard>
    </div>
  );
}

export function WorkflowEditor(props: WorkflowEditorProps) {
  const { byType, loading, error } = useNodeSpecs();

  if (loading) {
    return (
      <div className="flex h-full w-full items-center justify-center rounded-2xl border border-hairline bg-canvas-soft text-muted-foreground">
        <Loader2 className="mr-2 animate-spin" size={18} />
        正在加载节点目录…
      </div>
    );
  }
  if (error) {
    return (
      <div className="flex h-full w-full items-center justify-center rounded-2xl border border-hairline bg-canvas-soft px-6 text-center text-sm text-destructive">
        加载节点目录失败:{error}
      </div>
    );
  }

  return (
    <ReactFlowProvider>
      <Canvas {...props} specsByType={byType} />
    </ReactFlowProvider>
  );
}
