@geenius/ai-workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +11 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +74 -0
- package/packages/convex/README.md +1 -0
- package/packages/convex/package.json +12 -0
- package/packages/convex/src/convex.config.ts +3 -0
- package/packages/convex/src/index.ts +3 -0
- package/packages/convex/src/mutations.ts +36 -0
- package/packages/convex/src/queries.ts +19 -0
- package/packages/convex/src/schema.ts +24 -0
- package/packages/convex/tsconfig.json +25 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +46 -0
- package/packages/react/src/components/ApprovalModal.tsx +47 -0
- package/packages/react/src/components/StepConfigPanel.tsx +67 -0
- package/packages/react/src/components/StepConnector.tsx +47 -0
- package/packages/react/src/components/StepNode.tsx +38 -0
- package/packages/react/src/components/StepPalette.tsx +48 -0
- package/packages/react/src/components/WorkflowCanvas.tsx +42 -0
- package/packages/react/src/components/WorkflowRunPanel.tsx +64 -0
- package/packages/react/src/components/WorkflowToolbar.tsx +43 -0
- package/packages/react/src/components/index.ts +9 -0
- package/packages/react/src/hooks/index.ts +10 -0
- package/packages/react/src/hooks/useApprovalGate.ts +59 -0
- package/packages/react/src/hooks/useWorkflow.ts +39 -0
- package/packages/react/src/hooks/useWorkflowBuilder.ts +121 -0
- package/packages/react/src/hooks/useWorkflowRun.ts +75 -0
- package/packages/react/src/hooks/useWorkflowStep.ts +52 -0
- package/packages/react/src/hooks/useWorkflowTemplates.ts +54 -0
- package/packages/react/src/index.ts +16 -0
- package/packages/react/src/pages/WorkflowBuilderPage.tsx +81 -0
- package/packages/react/src/pages/WorkflowRunsPage.tsx +59 -0
- package/packages/react/src/pages/index.ts +3 -0
- package/packages/react/tsconfig.json +1 -0
- package/packages/react/tsup.config.ts +7 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +44 -0
- package/packages/react-css/src/components/ApprovalModal.tsx +6 -0
- package/packages/react-css/src/components/StepConfigPanel.tsx +7 -0
- package/packages/react-css/src/components/StepConnector.tsx +6 -0
- package/packages/react-css/src/components/StepNode.tsx +7 -0
- package/packages/react-css/src/components/StepPalette.tsx +6 -0
- package/packages/react-css/src/components/WorkflowCanvas.tsx +6 -0
- package/packages/react-css/src/components/WorkflowRunPanel.tsx +9 -0
- package/packages/react-css/src/components/WorkflowToolbar.tsx +4 -0
- package/packages/react-css/src/components/index.ts +9 -0
- package/packages/react-css/src/hooks/index.ts +3 -0
- package/packages/react-css/src/hooks/useWorkflow.ts +39 -0
- package/packages/react-css/src/hooks/useWorkflowBuilder.ts +121 -0
- package/packages/react-css/src/index.ts +7 -0
- package/packages/react-css/src/pages/WorkflowBuilderPage.tsx +16 -0
- package/packages/react-css/src/pages/WorkflowRunsPage.tsx +6 -0
- package/packages/react-css/src/pages/index.ts +3 -0
- package/packages/react-css/src/styles.css +945 -0
- package/packages/react-css/tsconfig.json +26 -0
- package/packages/react-css/tsup.config.ts +2 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +56 -0
- package/packages/shared/src/__tests__/ai-workflow.test.ts +217 -0
- package/packages/shared/src/config.ts +49 -0
- package/packages/shared/src/convex/index.ts +2 -0
- package/packages/shared/src/convex/schemas.ts +42 -0
- package/packages/shared/src/engine.test.ts +1 -0
- package/packages/shared/src/engine.ts +295 -0
- package/packages/shared/src/index.ts +43 -0
- package/packages/shared/src/steps.ts +68 -0
- package/packages/shared/src/templates.ts +172 -0
- package/packages/shared/src/types.ts +237 -0
- package/packages/shared/src/utils/cost.ts +79 -0
- package/packages/shared/src/utils/dag.ts +133 -0
- package/packages/shared/src/utils/index.ts +5 -0
- package/packages/shared/src/utils/interpolation.ts +53 -0
- package/packages/shared/src/validators.ts +215 -0
- package/packages/shared/tsconfig.json +1 -0
- package/packages/shared/tsup.config.ts +5 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +45 -0
- package/packages/solidjs/src/components/ApprovalModal.tsx +18 -0
- package/packages/solidjs/src/components/StepConfigPanel.tsx +14 -0
- package/packages/solidjs/src/components/StepConnector.tsx +11 -0
- package/packages/solidjs/src/components/StepNode.tsx +12 -0
- package/packages/solidjs/src/components/StepPalette.tsx +22 -0
- package/packages/solidjs/src/components/WorkflowCanvas.tsx +23 -0
- package/packages/solidjs/src/components/WorkflowRunPanel.tsx +18 -0
- package/packages/solidjs/src/components/WorkflowToolbar.tsx +13 -0
- package/packages/solidjs/src/components/index.ts +9 -0
- package/packages/solidjs/src/index.ts +7 -0
- package/packages/solidjs/src/pages/WorkflowBuilderPage.tsx +37 -0
- package/packages/solidjs/src/pages/WorkflowRunsPage.tsx +20 -0
- package/packages/solidjs/src/pages/index.ts +3 -0
- package/packages/solidjs/src/primitives/createApprovalGate.ts +29 -0
- package/packages/solidjs/src/primitives/createWorkflow.ts +28 -0
- package/packages/solidjs/src/primitives/createWorkflowBuilder.ts +56 -0
- package/packages/solidjs/src/primitives/createWorkflowRun.ts +32 -0
- package/packages/solidjs/src/primitives/createWorkflowStep.ts +23 -0
- package/packages/solidjs/src/primitives/createWorkflowTemplates.ts +28 -0
- package/packages/solidjs/src/primitives/index.ts +8 -0
- package/packages/solidjs/tsconfig.json +1 -0
- package/packages/solidjs/tsup.config.ts +7 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +43 -0
- package/packages/solidjs-css/src/components/ApprovalModal.tsx +6 -0
- package/packages/solidjs-css/src/components/StepConfigPanel.tsx +7 -0
- package/packages/solidjs-css/src/components/StepConnector.tsx +6 -0
- package/packages/solidjs-css/src/components/StepNode.tsx +7 -0
- package/packages/solidjs-css/src/components/StepPalette.tsx +7 -0
- package/packages/solidjs-css/src/components/WorkflowCanvas.tsx +7 -0
- package/packages/solidjs-css/src/components/WorkflowRunPanel.tsx +8 -0
- package/packages/solidjs-css/src/components/WorkflowToolbar.tsx +5 -0
- package/packages/solidjs-css/src/components/index.ts +9 -0
- package/packages/solidjs-css/src/index.ts +7 -0
- package/packages/solidjs-css/src/pages/WorkflowBuilderPage.tsx +2 -0
- package/packages/solidjs-css/src/pages/WorkflowRunsPage.tsx +7 -0
- package/packages/solidjs-css/src/pages/index.ts +3 -0
- package/packages/solidjs-css/src/primitives/createWorkflow.ts +28 -0
- package/packages/solidjs-css/src/primitives/createWorkflowBuilder.ts +56 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/styles.css +945 -0
- package/packages/solidjs-css/tsconfig.json +27 -0
- package/packages/solidjs-css/tsup.config.ts +2 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/StepConnector.tsx
|
|
2
|
+
|
|
3
|
+
export interface StepConnectorProps {
|
|
4
|
+
from: { x: number; y: number }
|
|
5
|
+
to: { x: number; y: number }
|
|
6
|
+
label?: string
|
|
7
|
+
isActive?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NODE_WIDTH = 200
|
|
11
|
+
const NODE_HEIGHT = 80
|
|
12
|
+
|
|
13
|
+
export function StepConnector({ from, to, label, isActive }: StepConnectorProps) {
|
|
14
|
+
const x1 = from.x + NODE_WIDTH
|
|
15
|
+
const y1 = from.y + NODE_HEIGHT / 2
|
|
16
|
+
const x2 = to.x
|
|
17
|
+
const y2 = to.y + NODE_HEIGHT / 2
|
|
18
|
+
const midX = (x1 + x2) / 2
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<g data-workflow="connector" data-active={isActive || undefined}>
|
|
22
|
+
<path
|
|
23
|
+
d={`M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`}
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke={isActive ? 'oklch(0.7 0.2 145)' : 'oklch(0.5 0.02 260)'}
|
|
26
|
+
strokeWidth={isActive ? 3 : 2}
|
|
27
|
+
markerEnd="url(#arrowhead)"
|
|
28
|
+
/>
|
|
29
|
+
{label && (
|
|
30
|
+
<text
|
|
31
|
+
x={midX}
|
|
32
|
+
y={(y1 + y2) / 2 - 8}
|
|
33
|
+
textAnchor="middle"
|
|
34
|
+
fill="oklch(0.7 0.02 260)"
|
|
35
|
+
fontSize="12"
|
|
36
|
+
>
|
|
37
|
+
{label}
|
|
38
|
+
</text>
|
|
39
|
+
)}
|
|
40
|
+
<defs>
|
|
41
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
42
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="oklch(0.5 0.02 260)" />
|
|
43
|
+
</marker>
|
|
44
|
+
</defs>
|
|
45
|
+
</g>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/StepNode.tsx
|
|
2
|
+
import type { WorkflowStepDef, StepType } from '@geenius-ai-workflow/shared'
|
|
3
|
+
|
|
4
|
+
const STEP_ICONS: Record<StepType, string> = {
|
|
5
|
+
'llm-call': '🤖', transform: '⚙️', condition: '🔀', 'human-approval': '👤',
|
|
6
|
+
webhook: '🌐', delay: '⏱️', parallel: '⚡', loop: '🔄', 'sub-workflow': '📋', custom: '🔧',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StepNodeProps {
|
|
10
|
+
step: WorkflowStepDef
|
|
11
|
+
isSelected?: boolean
|
|
12
|
+
status?: string
|
|
13
|
+
onClick?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function StepNode({ step, isSelected, status, onClick }: StepNodeProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
data-workflow="step-node"
|
|
20
|
+
data-step-type={step.type}
|
|
21
|
+
data-selected={isSelected || undefined}
|
|
22
|
+
data-status={status}
|
|
23
|
+
role="button"
|
|
24
|
+
tabIndex={0}
|
|
25
|
+
onClick={onClick}
|
|
26
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onClick?.() }}
|
|
27
|
+
style={step.position ? { position: 'absolute', left: step.position.x, top: step.position.y } : undefined}
|
|
28
|
+
>
|
|
29
|
+
<div data-workflow="step-header">
|
|
30
|
+
<span data-workflow="step-icon">{STEP_ICONS[step.type]}</span>
|
|
31
|
+
<span data-workflow="step-name">{step.name}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div data-workflow="step-type-badge">{step.type}</div>
|
|
34
|
+
{step.optional && <span data-workflow="step-optional-badge">Optional</span>}
|
|
35
|
+
{step.retries && <span data-workflow="step-retry-badge">↻ {step.retries.maxAttempts}</span>}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/StepPalette.tsx
|
|
2
|
+
import type { StepType } from '@geenius-ai-workflow/shared'
|
|
3
|
+
|
|
4
|
+
export interface StepPaletteItem {
|
|
5
|
+
type: StepType
|
|
6
|
+
label: string
|
|
7
|
+
icon: string
|
|
8
|
+
description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PALETTE_ITEMS: StepPaletteItem[] = [
|
|
12
|
+
{ type: 'llm-call', label: 'LLM Call', icon: '🤖', description: 'Call an AI model' },
|
|
13
|
+
{ type: 'transform', label: 'Transform', icon: '⚙️', description: 'Transform data' },
|
|
14
|
+
{ type: 'condition', label: 'Condition', icon: '🔀', description: 'If/else branching' },
|
|
15
|
+
{ type: 'human-approval', label: 'Approval', icon: '👤', description: 'Human approval gate' },
|
|
16
|
+
{ type: 'webhook', label: 'Webhook', icon: '🌐', description: 'HTTP request' },
|
|
17
|
+
{ type: 'delay', label: 'Delay', icon: '⏱️', description: 'Wait for duration' },
|
|
18
|
+
{ type: 'parallel', label: 'Parallel', icon: '⚡', description: 'Run steps in parallel' },
|
|
19
|
+
{ type: 'loop', label: 'Loop', icon: '🔄', description: 'Iterate over items' },
|
|
20
|
+
{ type: 'sub-workflow', label: 'Sub-workflow', icon: '📋', description: 'Run another workflow' },
|
|
21
|
+
{ type: 'custom', label: 'Custom', icon: '🔧', description: 'Custom handler' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export interface StepPaletteProps {
|
|
25
|
+
onAddStep: (type: StepType) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function StepPalette({ onAddStep }: StepPaletteProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div data-workflow="step-palette" role="list" aria-label="Available step types">
|
|
31
|
+
<h3 data-workflow="palette-title">Steps</h3>
|
|
32
|
+
{PALETTE_ITEMS.map(item => (
|
|
33
|
+
<button
|
|
34
|
+
key={item.type}
|
|
35
|
+
data-workflow="palette-item"
|
|
36
|
+
role="listitem"
|
|
37
|
+
onClick={() => onAddStep(item.type)}
|
|
38
|
+
>
|
|
39
|
+
<span data-workflow="palette-icon">{item.icon}</span>
|
|
40
|
+
<div data-workflow="palette-info">
|
|
41
|
+
<span data-workflow="palette-label">{item.label}</span>
|
|
42
|
+
<span data-workflow="palette-desc">{item.description}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/WorkflowCanvas.tsx
|
|
2
|
+
import type { WorkflowDefinition, WorkflowStepDef } from '@geenius-ai-workflow/shared'
|
|
3
|
+
import { StepNode } from './StepNode'
|
|
4
|
+
import { StepConnector } from './StepConnector'
|
|
5
|
+
|
|
6
|
+
export interface WorkflowCanvasProps {
|
|
7
|
+
definition: WorkflowDefinition
|
|
8
|
+
selectedStepId: string | null
|
|
9
|
+
onSelectStep: (id: string | null) => void
|
|
10
|
+
onMoveStep?: (stepId: string, pos: { x: number; y: number }) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function WorkflowCanvas({ definition, selectedStepId, onSelectStep, onMoveStep }: WorkflowCanvasProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div data-workflow="canvas" role="application" aria-label="Workflow canvas">
|
|
16
|
+
<svg data-workflow="connections-layer" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
|
17
|
+
{definition.connections.map((conn, i) => {
|
|
18
|
+
const from = definition.steps.find(s => s.id === conn.fromStepId)
|
|
19
|
+
const to = definition.steps.find(s => s.id === conn.toStepId)
|
|
20
|
+
if (!from?.position || !to?.position) return null
|
|
21
|
+
return <StepConnector key={i} from={from.position} to={to.position} label={conn.label} />
|
|
22
|
+
})}
|
|
23
|
+
</svg>
|
|
24
|
+
<div data-workflow="nodes-layer">
|
|
25
|
+
{definition.steps.map(step => (
|
|
26
|
+
<StepNode
|
|
27
|
+
key={step.id}
|
|
28
|
+
step={step}
|
|
29
|
+
isSelected={selectedStepId === step.id}
|
|
30
|
+
onClick={() => onSelectStep(step.id === selectedStepId ? null : step.id)}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
{definition.steps.length === 0 && (
|
|
35
|
+
<div data-workflow="canvas-empty">
|
|
36
|
+
<p data-workflow="empty-title">No steps yet</p>
|
|
37
|
+
<p data-workflow="empty-hint">Drag steps from the palette or use a template</p>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/WorkflowRunPanel.tsx
|
|
2
|
+
import type { WorkflowRun, StepResult } from '@geenius-ai-workflow/shared'
|
|
3
|
+
import { formatCost } from '@geenius-ai-workflow/shared'
|
|
4
|
+
|
|
5
|
+
export interface WorkflowRunPanelProps {
|
|
6
|
+
run: WorkflowRun | null
|
|
7
|
+
isRunning: boolean
|
|
8
|
+
onCancel?: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STATUS_ICONS: Record<string, string> = {
|
|
12
|
+
pending: '⏳', running: '🔄', completed: '✅', failed: '❌', skipped: '⏭️', 'waiting-approval': '👤',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function WorkflowRunPanel({ run, isRunning, onCancel }: WorkflowRunPanelProps) {
|
|
16
|
+
if (!run) {
|
|
17
|
+
return (
|
|
18
|
+
<div data-workflow="run-panel">
|
|
19
|
+
<div data-workflow="run-empty"><p>No active run. Click "Run" to execute the workflow.</p></div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const completedCount = run.stepResults.filter(s => s.status === 'completed').length
|
|
25
|
+
const totalCount = run.stepResults.length
|
|
26
|
+
const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
|
|
27
|
+
const totalCost = run.stepResults.reduce((sum, s) => sum + (s.costUsd ?? 0), 0)
|
|
28
|
+
const totalTokens = run.stepResults.reduce((sum, s) => sum + (s.tokens ?? 0), 0)
|
|
29
|
+
const duration = (run.completedAt ?? Date.now()) - run.startedAt
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div data-workflow="run-panel" data-status={run.status}>
|
|
33
|
+
<div data-workflow="run-header">
|
|
34
|
+
<h3 data-workflow="run-title">
|
|
35
|
+
{STATUS_ICONS[run.status]} Run {run.status}
|
|
36
|
+
</h3>
|
|
37
|
+
{isRunning && onCancel && (
|
|
38
|
+
<button data-workflow="run-cancel" onClick={onCancel}>⏹ Cancel</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
<div data-workflow="run-progress">
|
|
42
|
+
<div data-workflow="run-progress-bar">
|
|
43
|
+
<div data-workflow="run-progress-fill" style={{ width: `${progress}%` }} />
|
|
44
|
+
</div>
|
|
45
|
+
<span data-workflow="run-progress-text">{completedCount}/{totalCount} steps ({progress}%)</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div data-workflow="run-stats">
|
|
48
|
+
<span data-workflow="run-stat">⏱ {(duration / 1000).toFixed(1)}s</span>
|
|
49
|
+
{totalTokens > 0 && <span data-workflow="run-stat">🪙 {totalTokens.toLocaleString()} tokens</span>}
|
|
50
|
+
{totalCost > 0 && <span data-workflow="run-stat">💰 {formatCost(totalCost)}</span>}
|
|
51
|
+
</div>
|
|
52
|
+
{run.error && <div data-workflow="run-error" role="alert">{run.error}</div>}
|
|
53
|
+
<ul data-workflow="run-steps">
|
|
54
|
+
{run.stepResults.map(r => (
|
|
55
|
+
<li key={r.stepId} data-workflow="run-step-item" data-status={r.status}>
|
|
56
|
+
<span data-workflow="run-step-icon">{STATUS_ICONS[r.status]}</span>
|
|
57
|
+
<span data-workflow="run-step-name">{r.stepName}</span>
|
|
58
|
+
<span data-workflow="run-step-duration">{r.durationMs}ms</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/WorkflowToolbar.tsx
|
|
2
|
+
|
|
3
|
+
export interface WorkflowToolbarProps {
|
|
4
|
+
workflowName: string
|
|
5
|
+
isDirty: boolean
|
|
6
|
+
isRunning: boolean
|
|
7
|
+
canUndo: boolean
|
|
8
|
+
canRedo: boolean
|
|
9
|
+
onSave: () => void
|
|
10
|
+
onRun: () => void
|
|
11
|
+
onCancel?: () => void
|
|
12
|
+
onUndo: () => void
|
|
13
|
+
onRedo: () => void
|
|
14
|
+
onExport?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function WorkflowToolbar(props: WorkflowToolbarProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div data-workflow="toolbar" role="toolbar" aria-label="Workflow actions">
|
|
20
|
+
<div data-workflow="toolbar-left">
|
|
21
|
+
<h2 data-workflow="toolbar-title">{props.workflowName}</h2>
|
|
22
|
+
{props.isDirty && <span data-workflow="toolbar-dirty">●</span>}
|
|
23
|
+
</div>
|
|
24
|
+
<div data-workflow="toolbar-center">
|
|
25
|
+
<button data-workflow="toolbar-btn" onClick={props.onUndo} disabled={!props.canUndo} title="Undo">↶</button>
|
|
26
|
+
<button data-workflow="toolbar-btn" onClick={props.onRedo} disabled={!props.canRedo} title="Redo">↷</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div data-workflow="toolbar-right">
|
|
29
|
+
{props.onExport && (
|
|
30
|
+
<button data-workflow="toolbar-btn-secondary" onClick={props.onExport}>📤 Export</button>
|
|
31
|
+
)}
|
|
32
|
+
<button data-workflow="toolbar-btn-secondary" onClick={props.onSave} disabled={!props.isDirty}>
|
|
33
|
+
💾 Save
|
|
34
|
+
</button>
|
|
35
|
+
{props.isRunning ? (
|
|
36
|
+
<button data-workflow="toolbar-btn-danger" onClick={props.onCancel}>⏹ Stop</button>
|
|
37
|
+
) : (
|
|
38
|
+
<button data-workflow="toolbar-btn-primary" onClick={props.onRun}>▶ Run</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — components/index.ts
|
|
2
|
+
export { WorkflowCanvas, type WorkflowCanvasProps } from './WorkflowCanvas'
|
|
3
|
+
export { StepNode, type StepNodeProps } from './StepNode'
|
|
4
|
+
export { StepConnector, type StepConnectorProps } from './StepConnector'
|
|
5
|
+
export { StepConfigPanel, type StepConfigPanelProps } from './StepConfigPanel'
|
|
6
|
+
export { WorkflowRunPanel, type WorkflowRunPanelProps } from './WorkflowRunPanel'
|
|
7
|
+
export { ApprovalModal, type ApprovalModalProps } from './ApprovalModal'
|
|
8
|
+
export { WorkflowToolbar, type WorkflowToolbarProps } from './WorkflowToolbar'
|
|
9
|
+
export { StepPalette, type StepPaletteProps } from './StepPalette'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/index.ts
|
|
2
|
+
export { useWorkflow } from './useWorkflow'
|
|
3
|
+
export type { UseWorkflowOptions } from './useWorkflow'
|
|
4
|
+
export { useWorkflowBuilder } from './useWorkflowBuilder'
|
|
5
|
+
export { useWorkflowRun } from './useWorkflowRun'
|
|
6
|
+
export type { UseWorkflowRunOptions } from './useWorkflowRun'
|
|
7
|
+
export { useWorkflowStep } from './useWorkflowStep'
|
|
8
|
+
export { useApprovalGate } from './useApprovalGate'
|
|
9
|
+
export type { ApprovalRequest } from './useApprovalGate'
|
|
10
|
+
export { useWorkflowTemplates } from './useWorkflowTemplates'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/useApprovalGate.ts
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface ApprovalRequest {
|
|
5
|
+
id: string
|
|
6
|
+
stepId: string
|
|
7
|
+
message: string
|
|
8
|
+
requestedAt: number
|
|
9
|
+
approvers?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useApprovalGate() {
|
|
13
|
+
const [pendingApprovals, setPendingApprovals] = useState<ApprovalRequest[]>([])
|
|
14
|
+
const [history, setHistory] = useState<Array<ApprovalRequest & { decision: 'approved' | 'rejected'; decidedAt: number }>>([])
|
|
15
|
+
|
|
16
|
+
const requestApproval = useCallback((stepId: string, message: string, approvers?: string[]) => {
|
|
17
|
+
const request: ApprovalRequest = {
|
|
18
|
+
id: `approval_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
19
|
+
stepId,
|
|
20
|
+
message,
|
|
21
|
+
requestedAt: Date.now(),
|
|
22
|
+
approvers,
|
|
23
|
+
}
|
|
24
|
+
setPendingApprovals(prev => [...prev, request])
|
|
25
|
+
return request.id
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
const approve = useCallback((approvalId: string) => {
|
|
29
|
+
setPendingApprovals(prev => {
|
|
30
|
+
const request = prev.find(a => a.id === approvalId)
|
|
31
|
+
if (request) {
|
|
32
|
+
setHistory(h => [...h, { ...request, decision: 'approved', decidedAt: Date.now() }])
|
|
33
|
+
}
|
|
34
|
+
return prev.filter(a => a.id !== approvalId)
|
|
35
|
+
})
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const reject = useCallback((approvalId: string) => {
|
|
39
|
+
setPendingApprovals(prev => {
|
|
40
|
+
const request = prev.find(a => a.id === approvalId)
|
|
41
|
+
if (request) {
|
|
42
|
+
setHistory(h => [...h, { ...request, decision: 'rejected', decidedAt: Date.now() }])
|
|
43
|
+
}
|
|
44
|
+
return prev.filter(a => a.id !== approvalId)
|
|
45
|
+
})
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const clearHistory = useCallback(() => setHistory([]), [])
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
pendingApprovals,
|
|
52
|
+
history,
|
|
53
|
+
hasPending: pendingApprovals.length > 0,
|
|
54
|
+
requestApproval,
|
|
55
|
+
approve,
|
|
56
|
+
reject,
|
|
57
|
+
clearHistory,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/useWorkflow.ts
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
3
|
+
import type { WorkflowDefinition, WorkflowRun, StepResult } from '@geenius-ai-workflow/shared'
|
|
4
|
+
import { WorkflowEngine } from '@geenius-ai-workflow/shared'
|
|
5
|
+
import type { WorkflowEngineOptions } from '@geenius-ai-workflow/shared'
|
|
6
|
+
|
|
7
|
+
export interface UseWorkflowOptions extends WorkflowEngineOptions { }
|
|
8
|
+
|
|
9
|
+
export function useWorkflow(options: UseWorkflowOptions) {
|
|
10
|
+
const [run, setRun] = useState<WorkflowRun | null>(null)
|
|
11
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
12
|
+
const [error, setError] = useState<Error | null>(null)
|
|
13
|
+
const [stepResults, setStepResults] = useState<StepResult[]>([])
|
|
14
|
+
|
|
15
|
+
const engine = new WorkflowEngine({
|
|
16
|
+
...options,
|
|
17
|
+
onStepComplete: (result) => {
|
|
18
|
+
setStepResults(prev => [...prev, result])
|
|
19
|
+
options.onStepComplete?.(result)
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const execute = useCallback(async (definition: WorkflowDefinition, input?: Record<string, unknown>) => {
|
|
24
|
+
setIsRunning(true); setError(null); setStepResults([])
|
|
25
|
+
try {
|
|
26
|
+
const result = await engine.execute(definition, input)
|
|
27
|
+
setRun(result)
|
|
28
|
+
return result
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
31
|
+
setError(e); throw e
|
|
32
|
+
} finally { setIsRunning(false) }
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
const cancel = useCallback(() => engine.cancel(), [])
|
|
36
|
+
const reset = useCallback(() => { setRun(null); setError(null); setStepResults([]) }, [])
|
|
37
|
+
|
|
38
|
+
return { execute, cancel, run, isRunning, error, stepResults, reset }
|
|
39
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/useWorkflowBuilder.ts
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
3
|
+
import type { WorkflowDefinition, WorkflowStepDef, StepConnection, WorkflowBuilderState } from '@geenius-ai-workflow/shared'
|
|
4
|
+
|
|
5
|
+
const emptyDef = (): WorkflowDefinition => ({
|
|
6
|
+
id: crypto.randomUUID?.() ?? String(Date.now()),
|
|
7
|
+
name: 'New Workflow',
|
|
8
|
+
version: 1,
|
|
9
|
+
status: 'draft',
|
|
10
|
+
steps: [],
|
|
11
|
+
connections: [],
|
|
12
|
+
createdBy: '',
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
updatedAt: Date.now(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export function useWorkflowBuilder(initial?: Partial<WorkflowDefinition>) {
|
|
18
|
+
const [state, setState] = useState<WorkflowBuilderState>({
|
|
19
|
+
definition: { ...emptyDef(), ...initial },
|
|
20
|
+
selectedStepId: null,
|
|
21
|
+
isDirty: false,
|
|
22
|
+
undoStack: [],
|
|
23
|
+
redoStack: [],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const pushUndo = useCallback(() => {
|
|
27
|
+
setState(prev => ({ ...prev, undoStack: [...prev.undoStack.slice(-20), prev.definition], redoStack: [] }))
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const addStep = useCallback((step: WorkflowStepDef) => {
|
|
31
|
+
pushUndo()
|
|
32
|
+
setState(prev => ({
|
|
33
|
+
...prev,
|
|
34
|
+
isDirty: true,
|
|
35
|
+
definition: { ...prev.definition, steps: [...prev.definition.steps, step], updatedAt: Date.now() },
|
|
36
|
+
}))
|
|
37
|
+
}, [pushUndo])
|
|
38
|
+
|
|
39
|
+
const removeStep = useCallback((stepId: string) => {
|
|
40
|
+
pushUndo()
|
|
41
|
+
setState(prev => ({
|
|
42
|
+
...prev,
|
|
43
|
+
isDirty: true,
|
|
44
|
+
definition: {
|
|
45
|
+
...prev.definition,
|
|
46
|
+
steps: prev.definition.steps.filter(s => s.id !== stepId),
|
|
47
|
+
connections: prev.definition.connections.filter(c => c.fromStepId !== stepId && c.toStepId !== stepId),
|
|
48
|
+
updatedAt: Date.now(),
|
|
49
|
+
},
|
|
50
|
+
selectedStepId: prev.selectedStepId === stepId ? null : prev.selectedStepId,
|
|
51
|
+
}))
|
|
52
|
+
}, [pushUndo])
|
|
53
|
+
|
|
54
|
+
const updateStep = useCallback((stepId: string, updates: Partial<WorkflowStepDef>) => {
|
|
55
|
+
pushUndo()
|
|
56
|
+
setState(prev => ({
|
|
57
|
+
...prev,
|
|
58
|
+
isDirty: true,
|
|
59
|
+
definition: {
|
|
60
|
+
...prev.definition,
|
|
61
|
+
steps: prev.definition.steps.map(s => s.id === stepId ? { ...s, ...updates } : s),
|
|
62
|
+
updatedAt: Date.now(),
|
|
63
|
+
},
|
|
64
|
+
}))
|
|
65
|
+
}, [pushUndo])
|
|
66
|
+
|
|
67
|
+
const addConnection = useCallback((conn: StepConnection) => {
|
|
68
|
+
pushUndo()
|
|
69
|
+
setState(prev => ({
|
|
70
|
+
...prev,
|
|
71
|
+
isDirty: true,
|
|
72
|
+
definition: { ...prev.definition, connections: [...prev.definition.connections, conn], updatedAt: Date.now() },
|
|
73
|
+
}))
|
|
74
|
+
}, [pushUndo])
|
|
75
|
+
|
|
76
|
+
const removeConnection = useCallback((fromStepId: string, toStepId: string) => {
|
|
77
|
+
pushUndo()
|
|
78
|
+
setState(prev => ({
|
|
79
|
+
...prev,
|
|
80
|
+
isDirty: true,
|
|
81
|
+
definition: {
|
|
82
|
+
...prev.definition,
|
|
83
|
+
connections: prev.definition.connections.filter(c => !(c.fromStepId === fromStepId && c.toStepId === toStepId)),
|
|
84
|
+
updatedAt: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
}))
|
|
87
|
+
}, [pushUndo])
|
|
88
|
+
|
|
89
|
+
const selectStep = useCallback((stepId: string | null) => setState(prev => ({ ...prev, selectedStepId: stepId })), [])
|
|
90
|
+
|
|
91
|
+
const undo = useCallback(() => {
|
|
92
|
+
setState(prev => {
|
|
93
|
+
if (prev.undoStack.length === 0) return prev
|
|
94
|
+
const previous = prev.undoStack[prev.undoStack.length - 1]
|
|
95
|
+
return { ...prev, definition: previous, undoStack: prev.undoStack.slice(0, -1), redoStack: [prev.definition, ...prev.redoStack] }
|
|
96
|
+
})
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
const redo = useCallback(() => {
|
|
100
|
+
setState(prev => {
|
|
101
|
+
if (prev.redoStack.length === 0) return prev
|
|
102
|
+
const next = prev.redoStack[0]
|
|
103
|
+
return { ...prev, definition: next, undoStack: [...prev.undoStack, prev.definition], redoStack: prev.redoStack.slice(1) }
|
|
104
|
+
})
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const setDefinition = useCallback((def: WorkflowDefinition) => {
|
|
108
|
+
setState(prev => ({ ...prev, definition: def, isDirty: false, undoStack: [], redoStack: [] }))
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
definition: state.definition,
|
|
113
|
+
selectedStepId: state.selectedStepId,
|
|
114
|
+
isDirty: state.isDirty,
|
|
115
|
+
canUndo: state.undoStack.length > 0,
|
|
116
|
+
canRedo: state.redoStack.length > 0,
|
|
117
|
+
addStep, removeStep, updateStep,
|
|
118
|
+
addConnection, removeConnection,
|
|
119
|
+
selectStep, undo, redo, setDefinition,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/useWorkflowRun.ts
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
3
|
+
import type { WorkflowRun, StepResult, RunStatus } from '@geenius-ai-workflow/shared'
|
|
4
|
+
|
|
5
|
+
export interface UseWorkflowRunOptions {
|
|
6
|
+
/** Poll interval for checking run status (ms) */
|
|
7
|
+
pollIntervalMs?: number
|
|
8
|
+
/** Callback when a step completes */
|
|
9
|
+
onStepComplete?: (result: StepResult) => void
|
|
10
|
+
/** Callback when the run finishes */
|
|
11
|
+
onRunComplete?: (run: WorkflowRun) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useWorkflowRun(options: UseWorkflowRunOptions = {}) {
|
|
15
|
+
const [run, setRun] = useState<WorkflowRun | null>(null)
|
|
16
|
+
const [currentStepIndex, setCurrentStepIndex] = useState(0)
|
|
17
|
+
const [completedSteps, setCompletedSteps] = useState<StepResult[]>([])
|
|
18
|
+
const [status, setStatus] = useState<RunStatus>('pending')
|
|
19
|
+
const [progress, setProgress] = useState(0)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
22
|
+
|
|
23
|
+
const trackRun = useCallback((workflowRun: WorkflowRun) => {
|
|
24
|
+
setRun(workflowRun)
|
|
25
|
+
setStatus(workflowRun.status)
|
|
26
|
+
setCurrentStepIndex(workflowRun.currentStepIndex)
|
|
27
|
+
setCompletedSteps(workflowRun.stepResults)
|
|
28
|
+
setError(workflowRun.error ?? null)
|
|
29
|
+
|
|
30
|
+
const total = workflowRun.stepResults.length
|
|
31
|
+
const done = workflowRun.stepResults.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
32
|
+
setProgress(total > 0 ? Math.round((done / total) * 100) : 0)
|
|
33
|
+
|
|
34
|
+
if (workflowRun.status === 'completed' || workflowRun.status === 'failed') {
|
|
35
|
+
options.onRunComplete?.(workflowRun)
|
|
36
|
+
}
|
|
37
|
+
}, [options.onRunComplete])
|
|
38
|
+
|
|
39
|
+
const updateStep = useCallback((result: StepResult) => {
|
|
40
|
+
setCompletedSteps(prev => {
|
|
41
|
+
const existing = prev.findIndex(s => s.stepId === result.stepId)
|
|
42
|
+
if (existing >= 0) {
|
|
43
|
+
const updated = [...prev]
|
|
44
|
+
updated[existing] = result
|
|
45
|
+
return updated
|
|
46
|
+
}
|
|
47
|
+
return [...prev, result]
|
|
48
|
+
})
|
|
49
|
+
setCurrentStepIndex(prev => prev + 1)
|
|
50
|
+
options.onStepComplete?.(result)
|
|
51
|
+
}, [options.onStepComplete])
|
|
52
|
+
|
|
53
|
+
const reset = useCallback(() => {
|
|
54
|
+
setRun(null)
|
|
55
|
+
setCurrentStepIndex(0)
|
|
56
|
+
setCompletedSteps([])
|
|
57
|
+
setStatus('pending')
|
|
58
|
+
setProgress(0)
|
|
59
|
+
setError(null)
|
|
60
|
+
if (timerRef.current) clearInterval(timerRef.current)
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
|
65
|
+
}, [])
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
run, status, currentStepIndex, completedSteps,
|
|
69
|
+
progress, error,
|
|
70
|
+
trackRun, updateStep, reset,
|
|
71
|
+
isRunning: status === 'running',
|
|
72
|
+
isComplete: status === 'completed',
|
|
73
|
+
isFailed: status === 'failed',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @geenius-ai-workflow/react — src/hooks/useWorkflowStep.ts
|
|
2
|
+
import { useState, useCallback, useMemo } from 'react'
|
|
3
|
+
import type { WorkflowStepDef, StepResult, StepStatus, StepConfig } from '@geenius-ai-workflow/shared'
|
|
4
|
+
|
|
5
|
+
export function useWorkflowStep(stepDef: WorkflowStepDef) {
|
|
6
|
+
const [status, setStatus] = useState<StepStatus>('pending')
|
|
7
|
+
const [output, setOutput] = useState<unknown>(null)
|
|
8
|
+
const [error, setError] = useState<string | null>(null)
|
|
9
|
+
const [durationMs, setDurationMs] = useState(0)
|
|
10
|
+
|
|
11
|
+
const updateFromResult = useCallback((result: StepResult) => {
|
|
12
|
+
setStatus(result.status)
|
|
13
|
+
setOutput(result.output ?? null)
|
|
14
|
+
setError(result.error ?? null)
|
|
15
|
+
setDurationMs(result.durationMs)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
const reset = useCallback(() => {
|
|
19
|
+
setStatus('pending')
|
|
20
|
+
setOutput(null)
|
|
21
|
+
setError(null)
|
|
22
|
+
setDurationMs(0)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const icon = useMemo(() => {
|
|
26
|
+
const map: Record<string, string> = {
|
|
27
|
+
'llm-call': '🤖', 'transform': '⚙️', 'condition': '🔀',
|
|
28
|
+
'human-approval': '👤', 'webhook': '🌐', 'delay': '⏱️',
|
|
29
|
+
'parallel': '⚡', 'loop': '🔄', 'sub-workflow': '📋', 'custom': '🔧',
|
|
30
|
+
}
|
|
31
|
+
return map[stepDef.type] ?? '📦'
|
|
32
|
+
}, [stepDef.type])
|
|
33
|
+
|
|
34
|
+
const statusColor = useMemo(() => {
|
|
35
|
+
const map: Record<StepStatus, string> = {
|
|
36
|
+
pending: 'gray', running: 'blue', completed: 'green',
|
|
37
|
+
failed: 'red', skipped: 'orange', 'waiting-approval': 'purple',
|
|
38
|
+
}
|
|
39
|
+
return map[status]
|
|
40
|
+
}, [status])
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
stepDef, status, output, error, durationMs,
|
|
44
|
+
icon, statusColor,
|
|
45
|
+
updateFromResult, reset,
|
|
46
|
+
isComplete: status === 'completed',
|
|
47
|
+
isRunning: status === 'running',
|
|
48
|
+
isFailed: status === 'failed',
|
|
49
|
+
isWaiting: status === 'waiting-approval',
|
|
50
|
+
config: stepDef.config as StepConfig,
|
|
51
|
+
}
|
|
52
|
+
}
|