import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { X, Search, Puzzle, ToggleRight } from "lucide-react"; import type { WorkflowDefinition, WorkflowStepTemplate } from "@fusion/core"; import type { WorkflowEditorNodeKind } from "./nodes/WorkflowNodeTypes"; import { nodeHelpFor } from "./nodes/node-help"; import "./WorkflowAddStepModal.css"; /* FNXC:WorkflowSimpleView 2026-07-10-12:00: The simplified workflow view's add-step surface. Requirements: - One searchable dialog covering EVERYTHING the advanced palette + templates toolbar offers (node kinds, fragments, Fusion step templates, plugin templates) so the simple view loses no add capability. - Node kinds are grouped into human categories (Agent steps / Automation / Flow control) with one-line descriptions sourced from the node help registry, because the flat 16-button advanced palette is the main thing users found hard to use. - When inserting INSIDE a container (foreach/loop/optional-group child edge), container kinds are hidden — containers cannot nest — and FRAGMENTS are hidden too (PR #2006 review): fragments expand to top-level subgraphs, so splicing one into a template-child edge would create cross-boundary edges into the container. Step templates stay available (they materialize a single prompt/script node, valid as a sibling child). */ export interface AddStepPaletteEntry { kind: WorkflowEditorNodeKind; label: string; icon: React.ComponentType<{ size?: number | string; "aria-hidden"?: boolean | "true" | "false" }>; presetConfig?: Record; } const CONTAINER_KINDS: ReadonlySet = new Set(["foreach", "loop", "optional-group"]); const AGENT_KINDS: ReadonlySet = new Set(["prompt", "ask-user", "gate", "step-review"]); const AUTOMATION_KINDS: ReadonlySet = new Set(["script", "code", "notify", "parse-steps"]); export interface WorkflowAddStepModalProps { open: boolean; onClose: () => void; palette: AddStepPaletteEntry[]; /** Hide container kinds (insert target is inside a container). */ disallowContainers?: boolean; fragments: WorkflowDefinition[]; stepTemplates: WorkflowStepTemplate[]; pluginTemplates: Array<{ pluginId: string; template: WorkflowStepTemplate }>; /** Persistent seam-duplication conflict notice (mirrors the toolbar's). */ templateConflict?: string | null; onPickPalette: (entry: AddStepPaletteEntry) => void; onPickFragment: (fragment: WorkflowDefinition) => void; onPickStepTemplate: (template: WorkflowStepTemplate) => void; onPickStepTemplateAsOptionalGroup: (template: WorkflowStepTemplate) => void; } export function WorkflowAddStepModal({ open, onClose, palette, disallowContainers = false, fragments, stepTemplates, pluginTemplates, templateConflict, onPickPalette, onPickFragment, onPickStepTemplate, onPickStepTemplateAsOptionalGroup, }: WorkflowAddStepModalProps) { const { t } = useTranslation("app"); const [query, setQuery] = useState(""); const searchRef = useRef(null); useEffect(() => { if (open) { setQuery(""); // Focus after the dialog paints. const id = window.setTimeout(() => searchRef.current?.focus(), 0); return () => window.clearTimeout(id); } }, [open]); const q = query.trim().toLowerCase(); const categories = useMemo(() => { const eligible = palette.filter((entry) => !(disallowContainers && CONTAINER_KINDS.has(entry.kind))); const withHelp = eligible.map((entry) => ({ entry, description: nodeHelpFor(entry.kind)?.summary ?? "", })); const matches = (item: { entry: AddStepPaletteEntry; description: string }) => !q || item.entry.label.toLowerCase().includes(q) || item.entry.kind.includes(q) || item.description.toLowerCase().includes(q); return [ { id: "agent", label: t("workflowNodes.addStepAgentSteps", "Agent steps"), items: withHelp.filter((item) => AGENT_KINDS.has(item.entry.kind)).filter(matches), }, { id: "automation", label: t("workflowNodes.addStepAutomation", "Automation"), items: withHelp.filter((item) => AUTOMATION_KINDS.has(item.entry.kind)).filter(matches), }, { id: "flow", label: t("workflowNodes.addStepFlowControl", "Flow control"), items: withHelp .filter((item) => !AGENT_KINDS.has(item.entry.kind) && !AUTOMATION_KINDS.has(item.entry.kind)) .filter(matches), }, ].filter((category) => category.items.length > 0); }, [palette, disallowContainers, q, t]); const filteredFragments = useMemo( () => (disallowContainers ? [] : fragments.filter((f) => !q || f.name.toLowerCase().includes(q))), [fragments, q, disallowContainers], ); const filteredStepTemplates = useMemo( () => stepTemplates.filter((s) => !q || s.name.toLowerCase().includes(q)), [stepTemplates, q], ); const filteredPluginTemplates = useMemo( () => pluginTemplates.filter((p) => !q || p.template.name.toLowerCase().includes(q)), [pluginTemplates, q], ); const hasTemplates = filteredFragments.length > 0 || filteredStepTemplates.length > 0 || filteredPluginTemplates.length > 0; const hasAnyResult = categories.length > 0 || hasTemplates; if (!open) return null; return (
{ if (e.key === "Escape") { e.stopPropagation(); onClose(); } }} >
e.stopPropagation()} >

{t("workflowNodes.addStepTitle", "Add a step")}

setQuery(e.target.value)} />
{templateConflict && (
{t( "workflowNodes.templateSeamConflict", 'This fragment duplicates the "{{seam}}" seam already on the canvas, so it can\'t be inserted.', { seam: templateConflict }, )}
)}
{categories.map((category) => (

{category.label}

{category.items.map(({ entry, description }) => { const Icon = entry.icon; return ( ); })}
))} {hasTemplates && (

{t("workflowNodes.templatesSection", "Templates")}

{filteredFragments.map((fragment) => ( ))} {filteredStepTemplates.map((template) => (
{!disallowContainers && ( )}
))} {filteredPluginTemplates.map(({ pluginId, template }) => ( ))}
)} {!hasAnyResult && (

{t("workflowNodes.addStepNoMatches", "No steps match “{{query}}”.", { query })}

)}
); }