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>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import { ArrowRight } from 'lucide-react'
|
|
|
|
import type {
|
|
WorkflowAuthoringSection,
|
|
WorkflowAuthoringSectionConfig,
|
|
} from './workflowAuthoringSections'
|
|
|
|
type WorkflowAuthoringSectionSelectorProps = {
|
|
sections: WorkflowAuthoringSectionConfig[]
|
|
activeSection: WorkflowAuthoringSection
|
|
onSelectSection: (section: WorkflowAuthoringSection) => void
|
|
variant?: 'pill' | 'card'
|
|
}
|
|
|
|
export function WorkflowAuthoringSectionSelector({
|
|
sections,
|
|
activeSection,
|
|
onSelectSection,
|
|
variant = 'pill',
|
|
}: WorkflowAuthoringSectionSelectorProps) {
|
|
if (variant === 'card') {
|
|
return (
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
{sections.map(section => {
|
|
const Icon = section.icon
|
|
const isActive = activeSection === section.key
|
|
const toneLabel = section.tone === 'Escape Hatch' ? 'Direct' : section.tone
|
|
|
|
return (
|
|
<button
|
|
key={section.key}
|
|
type="button"
|
|
onClick={() => onSelectSection(section.key)}
|
|
aria-label={section.label}
|
|
className={`min-h-[76px] rounded-2xl border px-3 py-2.5 text-left transition-colors ${
|
|
isActive
|
|
? 'border-accent/40 bg-accent-light'
|
|
: 'border-border-default bg-surface hover:bg-surface-hover'
|
|
}`}
|
|
title={section.helper}
|
|
>
|
|
<div className="flex h-full items-start justify-between gap-2">
|
|
<div className="min-w-0">
|
|
<span className="inline-flex items-center gap-2 text-sm font-medium text-content">
|
|
<Icon size={14} />
|
|
{section.label}
|
|
</span>
|
|
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-content-muted">
|
|
<span
|
|
className={`rounded-full px-2 py-0.5 font-medium ${
|
|
section.tone === 'Escape Hatch'
|
|
? 'border border-border-default bg-surface text-content-muted'
|
|
: 'bg-slate-100 text-slate-700 dark:bg-slate-900/40 dark:text-slate-300'
|
|
}`}
|
|
>
|
|
{toneLabel}
|
|
</span>
|
|
{section.summaryLabel && (
|
|
<span className="rounded-full border border-border-default bg-surface px-2 py-0.5">
|
|
{section.summaryLabel}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="mt-1 line-clamp-1 text-[11px] text-content-muted">
|
|
{section.helper}
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1.5 self-center">
|
|
{isActive && <ArrowRight size={14} className="text-content-secondary" />}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
{sections.map(section => {
|
|
const Icon = section.icon
|
|
const isActive = activeSection === section.key
|
|
return (
|
|
<button
|
|
key={section.key}
|
|
type="button"
|
|
onClick={() => onSelectSection(section.key)}
|
|
className={`inline-flex items-center gap-1 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-accent text-white'
|
|
: 'border border-border-default bg-surface text-content-secondary hover:bg-surface-hover'
|
|
}`}
|
|
title={section.helper}
|
|
>
|
|
<Icon size={12} />
|
|
{section.label}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|