import { useEffect, useMemo, useState, type ReactNode } from 'react' import { ArrowRight, Plus } from 'lucide-react' import type { WorkflowNodeDefinition } from '../../api/workflows' import { AUTHORING_STAGE_DESCRIPTIONS, AUTHORING_STAGE_LABELS, AUTHORING_STAGE_STYLES, CATEGORY_COLORS, CATEGORY_LABELS, FAMILY_FILTER_DESCRIPTIONS, FAMILY_FILTER_LABELS, FAMILY_FILTER_STYLES, NODE_KIND_FILTER_LABELS, NODE_LIBRARY_GROUP_LABELS, NODE_LIBRARY_GROUP_STYLES, getDefinitionBadges, getDefinitionFamily, getDefinitionModuleLabel, getDefinitionModuleNamespace, type WorkflowAuthoringStage, type WorkflowGraphFamily, type WorkflowNodeFamilyFilter, type WorkflowNodeKindFilter, type WorkflowNodeLibraryGroup, } from './workflowNodeLibrary' import { buildWorkflowNodeCatalogModel, filterWorkflowNodeDefinitions, getAvailableFamilyFilters, } from './workflowNodeCatalog' import { STARTER_NODE_STEP_ORDER, STARTER_PATH_TITLES } from './workflowAuthoringGuidance' import { CATEGORY_FILTER_LABELS, CATEGORY_FILTERS, type FilterPillOption, type WorkflowNodeCategoryFilter, WorkflowNodeCatalogFilterControls, } from './WorkflowNodeCatalogFilterControls' import { WorkflowNodeCatalogEmptyState } from './WorkflowNodeCatalogEmptyState' import { WorkflowNodeCatalogQuickInsert } from './WorkflowNodeCatalogQuickInsert' import { getWorkflowNodeContractPresentation, getWorkflowNodeContractSignals, getWorkflowNodeContractSummary, } from './workflowNodeContracts' import { WorkflowNodeContractSignalList } from './WorkflowNodeContractSignalList' function ContractSummaryMetric({ label, value, title, }: { label: string value: string title?: string }) { return (

{label}

{value}

) } function RuntimeCoverageBadges({ runtimeCounts, size = 'sm', }: { runtimeCounts: Partial> size?: 'sm' | 'xs' }) { return ( <> {(['legacy', 'bridge', 'graph'] as WorkflowNodeLibraryGroup[]).map(group => { const count = runtimeCounts[group] ?? 0 if (count === 0) return null const label = size === 'sm' ? NODE_LIBRARY_GROUP_LABELS[group] : NODE_KIND_FILTER_LABELS[group] return ( {label} {count} ) })} ) } function StageCoverageBadges({ stages, }: { stages: WorkflowAuthoringStage[] }) { return ( <> {stages.map(stage => ( {AUTHORING_STAGE_LABELS[stage]} ))} ) } type WorkflowNodeCatalogBrowserProps = { definitions: WorkflowNodeDefinition[] graphFamily: WorkflowGraphFamily variant?: 'menu' | 'panel' onSelectStep?: (step: string) => void onEmptyAction?: () => void emptyActionLabel?: string renderIcon?: (iconName?: string, size?: number) => ReactNode searchPlaceholder?: string autoFocusSearch?: boolean } export function WorkflowNodeCatalogBrowser({ definitions, graphFamily, variant = 'panel', onSelectStep, onEmptyAction, emptyActionLabel = 'Clear Filters', renderIcon, searchPlaceholder = 'Search node label, step, or capability', autoFocusSearch = false, }: WorkflowNodeCatalogBrowserProps) { const [query, setQuery] = useState('') const [familyFilter, setFamilyFilter] = useState( graphFamily === 'mixed' ? 'all' : graphFamily, ) const [kindFilter, setKindFilter] = useState('all') const [categoryFilter, setCategoryFilter] = useState('all') const [moduleFilter, setModuleFilter] = useState('all') const [moduleQuery, setModuleQuery] = useState('') useEffect(() => { setFamilyFilter(graphFamily === 'mixed' ? 'all' : graphFamily) setCategoryFilter('all') setModuleFilter('all') setModuleQuery('') }, [graphFamily]) const availableFamilyFilters = useMemo( () => getAvailableFamilyFilters(graphFamily), [graphFamily], ) const filteredDefinitions = useMemo(() => { return filterWorkflowNodeDefinitions(definitions, { graphFamily, familyFilter, kindFilter, query, }) }, [definitions, familyFilter, graphFamily, kindFilter, query]) const normalizedModuleQuery = moduleQuery.trim().toLowerCase() const visibleDefinitions = useMemo(() => { const scopedByModule = moduleFilter === 'all' ? filteredDefinitions : filteredDefinitions.filter(definition => getDefinitionModuleNamespace(definition) === moduleFilter) const scopedByModuleQuery = !normalizedModuleQuery ? scopedByModule : scopedByModule.filter(definition => { const namespace = getDefinitionModuleNamespace(definition).toLowerCase() return ( namespace.includes(normalizedModuleQuery) || getDefinitionModuleLabel(definition).toLowerCase().includes(normalizedModuleQuery) || definition.module_key.toLowerCase().includes(normalizedModuleQuery) || definition.label.toLowerCase().includes(normalizedModuleQuery) ) }) if (categoryFilter === 'all') return scopedByModuleQuery return scopedByModuleQuery.filter(definition => definition.category === categoryFilter) }, [categoryFilter, filteredDefinitions, moduleFilter, normalizedModuleQuery]) const catalogModel = useMemo(() => buildWorkflowNodeCatalogModel(visibleDefinitions), [visibleDefinitions]) const moduleFilters = catalogModel.moduleFilters const stageSections = catalogModel.stageSections useEffect(() => { if (moduleFilter !== 'all' && !moduleFilters.some(module => module.namespace === moduleFilter)) { setModuleFilter('all') } }, [moduleFilter, moduleFilters]) const familySections = catalogModel.familySections const firstVisibleDefinition = visibleDefinitions[0] const totalModuleCount = moduleFilters.length const shouldExpandStageCoverage = variant === 'panel' && query.trim().length === 0 && moduleFilter === 'all' const quickInsertDefinitions = useMemo(() => { const prioritizedSteps = STARTER_NODE_STEP_ORDER[graphFamily] if (prioritizedSteps.length === 0) return visibleDefinitions.slice(0, 4) const byStep = new Map(visibleDefinitions.map(definition => [definition.step, definition])) return prioritizedSteps .map(step => byStep.get(step)) .filter((definition): definition is WorkflowNodeDefinition => Boolean(definition)) .slice(0, variant === 'menu' ? 5 : 6) }, [graphFamily, variant, visibleDefinitions]) const quickInsertTitle = graphFamily === 'mixed' ? 'Suggested nodes' : STARTER_PATH_TITLES[graphFamily] const runtimeFilterOptions: FilterPillOption[] = [ { value: 'all', label: NODE_KIND_FILTER_LABELS.all }, { value: 'legacy', label: NODE_KIND_FILTER_LABELS.legacy }, { value: 'bridge', label: NODE_KIND_FILTER_LABELS.bridge }, { value: 'graph', label: NODE_KIND_FILTER_LABELS.graph }, ] const familyFilterOptions = availableFamilyFilters.map(filter => ({ value: filter, label: FAMILY_FILTER_LABELS[filter], })) const categoryFilterOptions: FilterPillOption[] = CATEGORY_FILTERS.map( filter => ({ value: filter, label: CATEGORY_FILTER_LABELS[filter], }), ) return (
{ if (firstVisibleDefinition && onSelectStep) { onSelectStep(firstVisibleDefinition.step) } }} searchPlaceholder={searchPlaceholder} autoFocusSearch={autoFocusSearch} moduleFilters={moduleFilters} moduleFilter={moduleFilter} onModuleFilterChange={setModuleFilter} moduleQuery={moduleQuery} onModuleQueryChange={setModuleQuery} onClearModuleScope={() => { setModuleFilter('all') setModuleQuery('') }} />
{stageSections.length > 0 && (

Stage Coverage

Use Quick Insert for the first canonical steps, then use stage coverage to inspect which modules can fill the rest of the production path.

{stageSections.length} active stages
{stageSections.map(section => (
{AUTHORING_STAGE_LABELS[section.stage]} {section.modules.length} modules {section.definitions.length} nodes

{AUTHORING_STAGE_DESCRIPTIONS[section.stage]}

))}
)} {visibleDefinitions.length === 0 && ( )}
{familySections.map(familySection => { const familyLabel = FAMILY_FILTER_LABELS[familySection.family] const familyDescription = FAMILY_FILTER_DESCRIPTIONS[familySection.family] const familyStyle = FAMILY_FILTER_STYLES[familySection.family] return (
{familyLabel} {familySection.modules.length} modules {familySection.definitions.length} nodes

{familyDescription}

{familySection.modules.map(moduleGroup => (
{moduleGroup.label} {moduleGroup.namespace}
{moduleGroup.definitions.length}
{moduleGroup.stageSections.map(stageSection => { const stageLabel = AUTHORING_STAGE_LABELS[stageSection.stage] const stageDescription = AUTHORING_STAGE_DESCRIPTIONS[stageSection.stage] const stageStyle = AUTHORING_STAGE_STYLES[stageSection.stage] return (
{stageLabel}

{stageDescription}

{stageSection.definitions.length}
{stageSection.categories.map(categorySection => { const { category, definitions: categoryDefinitions } = categorySection return (
{CATEGORY_LABELS[category]} {categoryDefinitions.length}
{categoryDefinitions.map(definition => { const family = getDefinitionFamily(definition) const contractSummary = getWorkflowNodeContractSummary(definition) const contractPresentation = getWorkflowNodeContractPresentation(contractSummary) const { inputMetric, variableMetric, outputMetric, socketRequirementDescription, variableSummaryText, } = contractPresentation const contractSignals = getWorkflowNodeContractSignals( definition, contractSummary, ) const isActionable = Boolean(onSelectStep) return (
{renderIcon && ( {renderIcon(definition.icon, variant === 'menu' ? 14 : 13)} )}

{definition.label}

{FAMILY_FILTER_LABELS[family]} {getDefinitionBadges(definition).map(badge => ( {badge.label} ))}

{definition.step}

{isActionable && ( )}

{definition.description}

Wiring

{socketRequirementDescription}

Variables

{variableSummaryText}

) })}
) })}
) })}
))}
) })}
) }