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 (
)
}
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}
)
})}
)
})}
)
})}
))}
)
})}
)
}