Full graph-based workflow execution engine with: - WorkflowGraphRuntime with two-phase dispatch (collect → fire Celery tasks) - Node registry with contract validation, socket types, execution kinds - WorkflowRuntimeServices: preflight, context resolution, shadow-mode A/B - WorkflowRouter: CRUD + dispatch/preflight/run-history API endpoints - OutputTypeContracts: workflow binding, rollout-mode resolution - Admin router: output-type workflow binding endpoints - Frontend: complete workflow editor with drag-drop canvas, node inspector, module bundles, reference bundles, preflight panel, validation banner, authoring guidance, blueprint templates, shadow/rollout gate UI - Tests: comprehensive coverage for all workflow modules - Docs: NODE_CONTRACT_AUDIT and VALIDATION_ERROR_INVENTORY All 20 blocks from NEXT_20_BLOCK_BATCH_PLAN completed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
import { useEffect, useRef, type ReactNode } from 'react'
|
|
import { Boxes, Milestone, X } from 'lucide-react'
|
|
|
|
import type { WorkflowNodeDefinition } from '../../api/workflows'
|
|
import type { WorkflowGraphFamily } from './workflowNodeLibrary'
|
|
import { WorkflowAuthoringSectionContent } from './WorkflowAuthoringSectionContent'
|
|
import { WorkflowAuthoringSectionSelector } from './WorkflowAuthoringSectionSelector'
|
|
import {
|
|
type WorkflowAuthoringActions,
|
|
type WorkflowAuthoringPosition,
|
|
} from './workflowAuthoringActions'
|
|
import { useWorkflowAuthoringSurface } from './workflowAuthoringSurface'
|
|
|
|
export const NODE_COMMAND_MENU_WIDTH = 380
|
|
|
|
interface NodeCommandMenuProps {
|
|
definitions: WorkflowNodeDefinition[]
|
|
graphFamily: WorkflowGraphFamily
|
|
activeSteps?: string[]
|
|
actions: WorkflowAuthoringActions
|
|
preferredPosition?: WorkflowAuthoringPosition
|
|
onClose: () => void
|
|
renderIcon: (iconName?: string, size?: number) => ReactNode
|
|
}
|
|
|
|
export function NodeCommandMenu({
|
|
definitions,
|
|
graphFamily,
|
|
activeSteps = [],
|
|
actions,
|
|
preferredPosition,
|
|
onClose,
|
|
renderIcon,
|
|
}: NodeCommandMenuProps) {
|
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
const { activeSection, activeSectionDetail, chrome, insertBindings, plan, sections, setActiveSection } =
|
|
useWorkflowAuthoringSurface({
|
|
definitions,
|
|
graphFamily,
|
|
activeSteps,
|
|
actions,
|
|
preferredPosition,
|
|
onAfterInsert: onClose,
|
|
})
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [onClose])
|
|
|
|
useEffect(() => {
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
if (!containerRef.current) return
|
|
if (containerRef.current.contains(event.target as Node)) return
|
|
onClose()
|
|
}
|
|
|
|
window.addEventListener('pointerdown', handlePointerDown)
|
|
return () => window.removeEventListener('pointerdown', handlePointerDown)
|
|
}, [onClose])
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="flex max-h-[calc(100vh-1.5rem)] flex-col overflow-hidden rounded-2xl border border-border-default bg-surface shadow-2xl"
|
|
style={{ width: NODE_COMMAND_MENU_WIDTH }}
|
|
>
|
|
<div className="border-b border-border-default px-3 py-2.5">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<p className="text-sm font-semibold text-content">{chrome.menuTitle}</p>
|
|
<span className="rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
|
|
{activeSteps.length} on canvas
|
|
</span>
|
|
{plan.referenceBundles.length > 0 && (
|
|
<span className="inline-flex items-center gap-1 rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
|
|
<Milestone size={11} />
|
|
{plan.referenceBundles.length} paths
|
|
</span>
|
|
)}
|
|
{plan.moduleBundles.length > 0 && (
|
|
<span className="inline-flex items-center gap-1 rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
|
|
<Boxes size={11} />
|
|
{plan.moduleBundles.length} modules
|
|
</span>
|
|
)}
|
|
<span className="rounded-full border border-border-default bg-surface-hover px-2 py-0.5">
|
|
Esc closes
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-xs text-content-muted">
|
|
{chrome.menuHelper}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded-lg p-1 text-content-muted hover:bg-surface-hover hover:text-content"
|
|
title="Close node picker"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-3 py-2.5">
|
|
<div className="space-y-2.5">
|
|
<div className="sticky top-0 z-10 -mx-1 rounded-xl bg-surface/95 px-1 pb-2 pt-1 backdrop-blur">
|
|
<div className="mb-2 flex items-center justify-between gap-2 rounded-xl border border-border-default bg-surface-hover/30 px-3 py-2">
|
|
<div className="min-w-0">
|
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-content-secondary">
|
|
{chrome.guideEyebrow}
|
|
</p>
|
|
<p className="text-xs text-content-muted">
|
|
{activeSectionDetail?.helper ?? chrome.guideHelper}
|
|
</p>
|
|
</div>
|
|
<span className="shrink-0 rounded-full border border-border-default bg-surface px-2 py-0.5 text-[11px] text-content-muted">
|
|
{activeSectionDetail?.label ?? activeSectionDetail?.tone ?? 'Guided'}
|
|
</span>
|
|
</div>
|
|
<WorkflowAuthoringSectionSelector
|
|
sections={sections}
|
|
activeSection={activeSection}
|
|
onSelectSection={setActiveSection}
|
|
variant="pill"
|
|
/>
|
|
</div>
|
|
|
|
<WorkflowAuthoringSectionContent
|
|
activeSection={activeSection}
|
|
definitions={definitions}
|
|
graphFamily={graphFamily}
|
|
activeSteps={activeSteps}
|
|
actions={insertBindings}
|
|
renderIcon={renderIcon}
|
|
nodesVariant="menu"
|
|
searchPlaceholder="Search raw nodes"
|
|
autoFocusSearch
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|