ai-design-system 0.1.71 → 0.1.72
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/components/blocks/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx +54 -7
- package/components/composites/StateNode/StateNode.tsx +51 -2
- package/components/composites/TransitionNode/TransitionNode.tsx +51 -1
- package/components/composites/WorkflowToolbar/interfaces.ts +14 -0
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.tsx +28 -3
- package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +6 -2
- package/components/features/NodeEditor/NodeEditor.stories.tsx +60 -0
- package/components/features/NodeEditor/NodeEditor.tsx +47 -10
- package/dist/index.cjs +291 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -0
- package/dist/index.d.ts +63 -38
- package/dist/index.js +292 -134
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,12 +6,43 @@ import { EnhancedDataTable } from "@/components/composites/DataTable"
|
|
|
6
6
|
import type { DashboardRow } from "@/components/composites/DataTable"
|
|
7
7
|
import { Button } from "@/components/primitives/Button"
|
|
8
8
|
import { Icon } from "@/components/primitives/Icon"
|
|
9
|
+
import { IconButton } from "@/components/composites/IconButton"
|
|
10
|
+
|
|
11
|
+
const CopyButton = ({ text, className }: { text: string; className?: string }) => {
|
|
12
|
+
const [copied, setCopied] = React.useState(false)
|
|
13
|
+
|
|
14
|
+
const handleCopy = React.useCallback(
|
|
15
|
+
async (e: React.MouseEvent) => {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
e.stopPropagation()
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(text)
|
|
20
|
+
setCopied(true)
|
|
21
|
+
setTimeout(() => setCopied(false), 2000)
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error("Failed to copy:", err)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
[text]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<IconButton
|
|
31
|
+
icon={copied ? "check" : "copy"}
|
|
32
|
+
size="sm"
|
|
33
|
+
variant="secondary"
|
|
34
|
+
className={className}
|
|
35
|
+
title={copied ? "Copied!" : "Copy"}
|
|
36
|
+
onClick={handleCopy}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
export interface EvalSessionDetails {
|
|
11
42
|
id: string
|
|
12
43
|
date: string
|
|
13
44
|
goldenEvals: DashboardRow[]
|
|
14
|
-
recommendations: Array<{ id: number;
|
|
45
|
+
recommendations: Array<{ id: number; change: string; rationale: string }>
|
|
15
46
|
systemPrompt?: string
|
|
16
47
|
outputTranscript?: string
|
|
17
48
|
}
|
|
@@ -19,7 +50,7 @@ export interface EvalSessionDetails {
|
|
|
19
50
|
export interface EvalSessionDetailsPanelProps {
|
|
20
51
|
sessionDetails: EvalSessionDetails
|
|
21
52
|
activeTab: string
|
|
22
|
-
actionHandlers?: { onTriggerEvaluation?: () => void }
|
|
53
|
+
actionHandlers?: { onTriggerEvaluation?: () => void; onDownloadPrompt?: () => Promise<import('@/components/composites/FileTreeExplorer').FileDownloadResult | undefined> }
|
|
23
54
|
workflowContent?: React.ReactNode
|
|
24
55
|
}
|
|
25
56
|
|
|
@@ -31,12 +62,12 @@ export const EvalTriggerButton = ({ onClick, loading }: { onClick: () => void; l
|
|
|
31
62
|
|
|
32
63
|
const recommendationTableSchema = dynamicTableSchema.parse({
|
|
33
64
|
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
34
|
-
rowKey: "
|
|
65
|
+
rowKey: "change",
|
|
35
66
|
enableFiltering: true,
|
|
36
67
|
enablePagination: false,
|
|
37
68
|
enableRowSelection: false,
|
|
38
69
|
columns: [
|
|
39
|
-
{ key: "
|
|
70
|
+
{ key: "change", label: "Recommendation", renderType: "text" },
|
|
40
71
|
{ key: "rationale", label: "Rationale", renderType: "text" },
|
|
41
72
|
],
|
|
42
73
|
})
|
|
@@ -56,7 +87,7 @@ const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
|
56
87
|
})
|
|
57
88
|
|
|
58
89
|
export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
59
|
-
({ sessionDetails, activeTab,
|
|
90
|
+
({ sessionDetails, activeTab, workflowContent }) => {
|
|
60
91
|
return (
|
|
61
92
|
<div className="flex flex-col h-full bg-background">
|
|
62
93
|
<Tabs value={activeTab} className="flex-1 flex flex-col min-h-0">
|
|
@@ -89,7 +120,15 @@ export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
|
89
120
|
|
|
90
121
|
<TabsContent value="prompts" className="absolute inset-0 m-0 p-0">
|
|
91
122
|
<ScrollArea className="h-full">
|
|
92
|
-
<div className="p-4">
|
|
123
|
+
<div className="p-4 relative group">
|
|
124
|
+
{sessionDetails.systemPrompt && (
|
|
125
|
+
<div className="absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
126
|
+
<CopyButton
|
|
127
|
+
text={sessionDetails.systemPrompt}
|
|
128
|
+
className="h-8 w-8 p-0"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
93
132
|
<pre className="text-xs font-mono bg-muted p-4 rounded whitespace-pre-wrap">
|
|
94
133
|
{sessionDetails.systemPrompt || "No system prompt available."}
|
|
95
134
|
</pre>
|
|
@@ -99,7 +138,15 @@ export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
|
99
138
|
|
|
100
139
|
<TabsContent value="outputs" className="absolute inset-0 m-0 p-0">
|
|
101
140
|
<ScrollArea className="h-full">
|
|
102
|
-
<div className="p-4">
|
|
141
|
+
<div className="p-4 relative group">
|
|
142
|
+
{sessionDetails.outputTranscript && (
|
|
143
|
+
<div className="absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
144
|
+
<CopyButton
|
|
145
|
+
text={sessionDetails.outputTranscript}
|
|
146
|
+
className="h-8 w-8 p-0"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
103
150
|
<pre className="text-xs font-mono bg-muted p-4 rounded whitespace-pre-wrap">
|
|
104
151
|
{sessionDetails.outputTranscript || "No transcript available."}
|
|
105
152
|
</pre>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { NodeToolbar, Position, type NodeProps } from "@xyflow/react";
|
|
4
4
|
import { memo } from "react";
|
|
5
5
|
import { Icon } from "@/components/primitives/Icon";
|
|
6
|
+
import { ButtonGroup } from "@/components/primitives/ButtonGroup";
|
|
7
|
+
import { Button } from "@/components/primitives/Button";
|
|
8
|
+
import { DefaultSwitcher } from "@/components/composites/DefaultSwitcher";
|
|
6
9
|
import {
|
|
7
10
|
Node,
|
|
8
11
|
NodeDescription,
|
|
9
12
|
NodeTitle,
|
|
10
13
|
} from "@/components/ai-elements/node";
|
|
11
14
|
import { cn } from "@/lib/utils";
|
|
15
|
+
import type { ToolbarAction } from "@/components/composites/WorkflowToolbar";
|
|
12
16
|
|
|
13
17
|
export type StateNodeData = {
|
|
14
18
|
label: string;
|
|
@@ -18,6 +22,7 @@ export type StateNodeData = {
|
|
|
18
22
|
isTerminal?: boolean;
|
|
19
23
|
status?: "idle" | "running" | "success" | "error";
|
|
20
24
|
enabled?: boolean;
|
|
25
|
+
actions?: ToolbarAction[];
|
|
21
26
|
};
|
|
22
27
|
|
|
23
28
|
type StateNodeProps = NodeProps & {
|
|
@@ -63,7 +68,6 @@ export const StateNode = memo(({ data, selected, id }: StateNodeProps) => {
|
|
|
63
68
|
const displayDescription = data.description;
|
|
64
69
|
const status = data.status;
|
|
65
70
|
const isDisabled = data.enabled === false;
|
|
66
|
-
const isTerminal = data.isTerminal === true;
|
|
67
71
|
|
|
68
72
|
return (
|
|
69
73
|
<Node
|
|
@@ -99,6 +103,51 @@ export const StateNode = memo(({ data, selected, id }: StateNodeProps) => {
|
|
|
99
103
|
)}
|
|
100
104
|
</div>
|
|
101
105
|
</div>
|
|
106
|
+
{data.actions && data.actions.length > 0 && (
|
|
107
|
+
<NodeToolbar isVisible={true} position={Position.Right} offset={12}>
|
|
108
|
+
{(() => {
|
|
109
|
+
const switcherActions = data.actions.filter((a: ToolbarAction) => a.switcher);
|
|
110
|
+
const buttonActions = data.actions.filter((a: ToolbarAction) => !a.switcher);
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
{buttonActions.length > 0 && (
|
|
114
|
+
<ButtonGroup orientation="horizontal" className="shadow-md bg-secondary/80 backdrop-blur border-border/50">
|
|
115
|
+
{buttonActions.map((action: ToolbarAction) => (
|
|
116
|
+
<Button
|
|
117
|
+
key={action.id}
|
|
118
|
+
size="icon"
|
|
119
|
+
variant="secondary"
|
|
120
|
+
className="h-8 w-8 hover:bg-primary/10"
|
|
121
|
+
onClick={(e) => {
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
action.onClick?.();
|
|
124
|
+
}}
|
|
125
|
+
title={action.title}
|
|
126
|
+
>
|
|
127
|
+
{action.icon ? (
|
|
128
|
+
<Icon name={action.icon as string} size="sm" className={action.icon === 'check' ? 'text-green-500' : action.icon === 'x' ? 'text-red-500' : 'text-primary'} />
|
|
129
|
+
) : (
|
|
130
|
+
<Icon name="play" size="sm" className="text-primary" />
|
|
131
|
+
)}
|
|
132
|
+
</Button>
|
|
133
|
+
))}
|
|
134
|
+
</ButtonGroup>
|
|
135
|
+
)}
|
|
136
|
+
{switcherActions.map((action: ToolbarAction) => (
|
|
137
|
+
<div key={action.id} className="shadow-md bg-secondary/80 backdrop-blur border-border/50 rounded-md">
|
|
138
|
+
<DefaultSwitcher
|
|
139
|
+
themes={action.switcher!.items}
|
|
140
|
+
value={action.switcher!.value}
|
|
141
|
+
onValueChange={action.switcher!.onValueChange}
|
|
142
|
+
placeholder={action.switcher!.placeholder}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
))}
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
})()}
|
|
149
|
+
</NodeToolbar>
|
|
150
|
+
)}
|
|
102
151
|
</Node>
|
|
103
152
|
);
|
|
104
153
|
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { NodeToolbar, Position, type NodeProps } from "@xyflow/react";
|
|
4
4
|
import { memo } from "react";
|
|
5
5
|
import { Icon } from "@/components/primitives/Icon";
|
|
6
|
+
import { ButtonGroup } from "@/components/primitives/ButtonGroup";
|
|
7
|
+
import { Button } from "@/components/primitives/Button";
|
|
8
|
+
import { DefaultSwitcher } from "@/components/composites/DefaultSwitcher";
|
|
6
9
|
import {
|
|
7
10
|
Node,
|
|
8
11
|
NodeDescription,
|
|
9
12
|
NodeTitle,
|
|
10
13
|
} from "@/components/ai-elements/node";
|
|
11
14
|
import { cn } from "@/lib/utils";
|
|
15
|
+
import type { ToolbarAction } from "@/components/composites/WorkflowToolbar";
|
|
12
16
|
|
|
13
17
|
export type TransitionNodeConfig = {
|
|
14
18
|
/** For conditional transitions: form field comparison e.g. "{{record.orderValue}} > {{record.acceptedRange}}" */
|
|
@@ -33,6 +37,7 @@ export type TransitionNodeData = {
|
|
|
33
37
|
config?: TransitionNodeConfig;
|
|
34
38
|
status?: "idle" | "running" | "success" | "error";
|
|
35
39
|
enabled?: boolean;
|
|
40
|
+
actions?: ToolbarAction[];
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
type TransitionNodeProps = NodeProps & {
|
|
@@ -91,6 +96,51 @@ export const TransitionNode = memo(
|
|
|
91
96
|
)}
|
|
92
97
|
</div>
|
|
93
98
|
</div>
|
|
99
|
+
{data.actions && data.actions.length > 0 && (
|
|
100
|
+
<NodeToolbar isVisible={true} position={Position.Right} offset={12}>
|
|
101
|
+
{(() => {
|
|
102
|
+
const switcherActions = data.actions.filter((a: ToolbarAction) => a.switcher);
|
|
103
|
+
const buttonActions = data.actions.filter((a: ToolbarAction) => !a.switcher);
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{buttonActions.length > 0 && (
|
|
107
|
+
<ButtonGroup orientation="horizontal" className="shadow-md bg-secondary/80 backdrop-blur border-border/50">
|
|
108
|
+
{buttonActions.map((action: ToolbarAction) => (
|
|
109
|
+
<Button
|
|
110
|
+
key={action.id}
|
|
111
|
+
size="icon"
|
|
112
|
+
variant="secondary"
|
|
113
|
+
className="h-8 w-8 hover:bg-primary/10"
|
|
114
|
+
onClick={(e) => {
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
action.onClick?.();
|
|
117
|
+
}}
|
|
118
|
+
title={action.title}
|
|
119
|
+
>
|
|
120
|
+
{action.icon ? (
|
|
121
|
+
<Icon name={action.icon as string} size="sm" className={action.icon === 'check' ? 'text-green-500' : action.icon === 'x' ? 'text-red-500' : 'text-primary'} />
|
|
122
|
+
) : (
|
|
123
|
+
<Icon name="play" size="sm" className="text-primary" />
|
|
124
|
+
)}
|
|
125
|
+
</Button>
|
|
126
|
+
))}
|
|
127
|
+
</ButtonGroup>
|
|
128
|
+
)}
|
|
129
|
+
{switcherActions.map((action: ToolbarAction) => (
|
|
130
|
+
<div key={action.id} className="shadow-md bg-secondary/80 backdrop-blur border-border/50 rounded-md">
|
|
131
|
+
<DefaultSwitcher
|
|
132
|
+
themes={action.switcher!.items}
|
|
133
|
+
value={action.switcher!.value}
|
|
134
|
+
onValueChange={action.switcher!.onValueChange}
|
|
135
|
+
placeholder={action.switcher!.placeholder}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
})()}
|
|
142
|
+
</NodeToolbar>
|
|
143
|
+
)}
|
|
94
144
|
</Node>
|
|
95
145
|
);
|
|
96
146
|
}
|
|
@@ -5,6 +5,19 @@ export interface WorkflowVersion {
|
|
|
5
5
|
label: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface ToolbarActionSwitcherItem {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToolbarActionSwitcher {
|
|
14
|
+
items: ToolbarActionSwitcherItem[];
|
|
15
|
+
value?: string;
|
|
16
|
+
onValueChange: (value: string) => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export interface ToolbarAction {
|
|
9
22
|
id: string;
|
|
10
23
|
icon: string | React.ReactNode;
|
|
@@ -13,6 +26,7 @@ export interface ToolbarAction {
|
|
|
13
26
|
disabled?: boolean;
|
|
14
27
|
loading?: boolean;
|
|
15
28
|
indicator?: boolean;
|
|
29
|
+
switcher?: ToolbarActionSwitcher;
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
export interface WorkflowToolbarActionsProps {
|
|
@@ -5,6 +5,7 @@ import { DashboardChart } from "@/components/composites/DashboardChart"
|
|
|
5
5
|
import { EvalSessionDetailsPanel, EvalTriggerButton } from "@/components/blocks/EvalSessionDetailsPanel"
|
|
6
6
|
import type { DashboardRow } from "@/components/composites/DataTable"
|
|
7
7
|
import { IconButton } from "@/components/composites/IconButton"
|
|
8
|
+
import type { FileDownloadResult } from "@/components/composites/FileTreeExplorer"
|
|
8
9
|
import { cn } from "@/lib/utils"
|
|
9
10
|
import type {
|
|
10
11
|
EvalDashboardFeatureInboxState,
|
|
@@ -25,6 +26,27 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
25
26
|
({ inbox, data, actionHandlers, className, workflowContent }) => {
|
|
26
27
|
const [showInbox, setShowInbox] = React.useState(true)
|
|
27
28
|
const [activeTab, setActiveTab] = React.useState("golden-evals")
|
|
29
|
+
const [isDownloading, setIsDownloading] = React.useState(false)
|
|
30
|
+
const downloadRef = React.useRef<HTMLAnchorElement>(null)
|
|
31
|
+
|
|
32
|
+
const handleDownloadClick = React.useCallback(async () => {
|
|
33
|
+
if (!actionHandlers?.onDownloadPrompt || isDownloading) return
|
|
34
|
+
setIsDownloading(true)
|
|
35
|
+
try {
|
|
36
|
+
const result: FileDownloadResult | undefined = await actionHandlers.onDownloadPrompt()
|
|
37
|
+
if (!result) return
|
|
38
|
+
const url = URL.createObjectURL(result.blob)
|
|
39
|
+
const anchor = downloadRef.current
|
|
40
|
+
if (anchor) {
|
|
41
|
+
anchor.href = url
|
|
42
|
+
anchor.download = result.filename
|
|
43
|
+
anchor.click()
|
|
44
|
+
}
|
|
45
|
+
URL.revokeObjectURL(url)
|
|
46
|
+
} finally {
|
|
47
|
+
setIsDownloading(false)
|
|
48
|
+
}
|
|
49
|
+
}, [actionHandlers?.onDownloadPrompt, isDownloading])
|
|
28
50
|
|
|
29
51
|
const selectedSession = React.useMemo(() =>
|
|
30
52
|
inbox.items.find(s => s.id === inbox.selectedItemId) || null
|
|
@@ -72,6 +94,7 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
72
94
|
} : null
|
|
73
95
|
|
|
74
96
|
return (
|
|
97
|
+
<>
|
|
75
98
|
<div className={cn("h-full w-full overflow-hidden bg-background", className)}>
|
|
76
99
|
<SectionLayout
|
|
77
100
|
dragHandleColor="border"
|
|
@@ -169,7 +192,7 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
169
192
|
<div className="flex items-center gap-2">
|
|
170
193
|
<EvalTriggerButton onClick={actionHandlers.onTriggerEvaluation} loading={actionHandlers.isTriggering} />
|
|
171
194
|
{actionHandlers.onDownloadPrompt && (
|
|
172
|
-
<IconButton onClick={
|
|
195
|
+
<IconButton onClick={handleDownloadClick} variant="outline" size="sm" className="h-8 w-8 p-0" title={isDownloading ? 'Preparing...' : 'Download Prompt'} icon={isDownloading ? 'loader-2' : 'download'} iconClassName={`w-4 h-4${isDownloading ? ' animate-spin' : ''}`} />
|
|
173
196
|
)}
|
|
174
197
|
</div>
|
|
175
198
|
) : undefined,
|
|
@@ -194,9 +217,11 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
194
217
|
}
|
|
195
218
|
]}
|
|
196
219
|
/>
|
|
197
|
-
|
|
220
|
+
</div>
|
|
221
|
+
<a ref={downloadRef} style={{ display: 'none' }} />
|
|
222
|
+
</>
|
|
198
223
|
)
|
|
199
224
|
}
|
|
200
225
|
)
|
|
201
|
-
|
|
226
|
+
|
|
202
227
|
EvalDashboardFeature.displayName = "EvalDashboardFeature"
|
|
@@ -25,8 +25,10 @@ export interface EvalDashboardFeatureInboxState {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface EvalRecommendation {
|
|
28
|
-
|
|
28
|
+
change: string;
|
|
29
29
|
rationale: string;
|
|
30
|
+
priority?: number;
|
|
31
|
+
expected_metrics?: number[];
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface EvalDashboardFeatureData {
|
|
@@ -43,9 +45,11 @@ export interface EvalDashboardFeatureData {
|
|
|
43
45
|
}>;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
import type { FileDownloadResult } from '@/components/composites/FileTreeExplorer';
|
|
49
|
+
|
|
46
50
|
export interface EvalDashboardFeatureActionHandlers {
|
|
47
51
|
onTriggerEvaluation?: () => void;
|
|
48
|
-
onDownloadPrompt?: () =>
|
|
52
|
+
onDownloadPrompt?: () => Promise<FileDownloadResult | undefined>;
|
|
49
53
|
isTriggering?: boolean;
|
|
50
54
|
timeRange?: string;
|
|
51
55
|
onTimeRangeChange?: (range: string) => void;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
3
|
import { NodeEditor } from "./NodeEditor";
|
|
3
4
|
import { useNodeEditorMock } from "./useNodeEditor.mock";
|
|
4
5
|
import { mockVersions, mockNodes, mockEdges } from "./NodeEditor.mocks";
|
|
6
|
+
import type { WorkflowNode, WorkflowEdge } from "@/components/blocks/WorkflowCanvas";
|
|
7
|
+
import type { ToolbarAction } from "@/components/composites/WorkflowToolbar";
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* NodeEditor Feature
|
|
@@ -139,3 +142,60 @@ export const Saving: Story = {
|
|
|
139
142
|
canRedo: false,
|
|
140
143
|
},
|
|
141
144
|
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* WithHITLApproval — single HITL transition node with a floating DefaultSwitcher
|
|
148
|
+
* containing workflow-defined approval options beside the node.
|
|
149
|
+
*/
|
|
150
|
+
function NodeEditorWithHITLApprovalStory() {
|
|
151
|
+
const [selectedValue, setSelectedValue] = React.useState("");
|
|
152
|
+
|
|
153
|
+
const hitlNode: WorkflowNode = {
|
|
154
|
+
id: "hitl-1",
|
|
155
|
+
type: "transition",
|
|
156
|
+
position: { x: 300, y: 100 },
|
|
157
|
+
data: {
|
|
158
|
+
label: "Human Approval",
|
|
159
|
+
description: "Manager review required",
|
|
160
|
+
type: "transition",
|
|
161
|
+
status: "idle",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const nodeActions: Record<string, ToolbarAction[]> = {
|
|
166
|
+
"hitl-1": [
|
|
167
|
+
{
|
|
168
|
+
id: "switcher-hitl-1",
|
|
169
|
+
icon: "chevrons-up-down",
|
|
170
|
+
title: "Select approval decision",
|
|
171
|
+
switcher: {
|
|
172
|
+
items: [
|
|
173
|
+
{ label: "Approve", value: "approve" },
|
|
174
|
+
{ label: "Reject", value: "reject" },
|
|
175
|
+
],
|
|
176
|
+
value: selectedValue,
|
|
177
|
+
onValueChange: setSelectedValue,
|
|
178
|
+
placeholder: "Do you approve this evaluation?",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<NodeEditor
|
|
186
|
+
workflowName="HITL Approval"
|
|
187
|
+
versions={[{ id: "v1", label: "v1" }]}
|
|
188
|
+
currentVersionId="v1"
|
|
189
|
+
nodes={[hitlNode]}
|
|
190
|
+
edges={[]}
|
|
191
|
+
nodeActions={nodeActions}
|
|
192
|
+
interactive={false}
|
|
193
|
+
hideDefaultActions={true}
|
|
194
|
+
showMinimap={false}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const WithHITLApproval: Story = {
|
|
200
|
+
render: () => <NodeEditorWithHITLApprovalStory />,
|
|
201
|
+
};
|
|
@@ -15,6 +15,11 @@ const GLOW: Record<string, string> = {
|
|
|
15
15
|
error: "0 0 14px 5px rgba(239, 68, 68, 0.9)",
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
export interface NodeStatusEntry {
|
|
19
|
+
nodeId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export interface NodeEditorProps {
|
|
19
24
|
// Toolbar — left
|
|
20
25
|
workflowName?: string;
|
|
@@ -48,8 +53,22 @@ export interface NodeEditorProps {
|
|
|
48
53
|
hideDefaultActions?: boolean;
|
|
49
54
|
className?: string;
|
|
50
55
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
56
|
+
/** Runtime node statuses from the API. NodeEditor derives highlights internally. */
|
|
57
|
+
nodeStatuses?: NodeStatusEntry[];
|
|
58
|
+
/** Per-node actions */
|
|
59
|
+
nodeActions?: Record<string, ToolbarAction[]>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function statusToGlow(status: string): string | null {
|
|
63
|
+
switch (status) {
|
|
64
|
+
case 'running': return 'active';
|
|
65
|
+
case 'pending_hitl': return 'pending';
|
|
66
|
+
case 'success':
|
|
67
|
+
case 'completed': return 'done';
|
|
68
|
+
case 'error':
|
|
69
|
+
case 'failed': return 'error';
|
|
70
|
+
default: return null;
|
|
71
|
+
}
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export function NodeEditor({
|
|
@@ -76,19 +95,37 @@ export function NodeEditor({
|
|
|
76
95
|
interactive = false,
|
|
77
96
|
hideDefaultActions = false,
|
|
78
97
|
className,
|
|
79
|
-
|
|
98
|
+
nodeStatuses,
|
|
99
|
+
nodeActions,
|
|
80
100
|
}: NodeEditorProps) {
|
|
81
101
|
const highlightedNodes = useMemo(
|
|
82
|
-
() =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
() => {
|
|
103
|
+
const statusGlows: Record<string, string> = {};
|
|
104
|
+
for (const ns of nodeStatuses ?? []) {
|
|
105
|
+
const g = statusToGlow(ns.status);
|
|
106
|
+
if (g) statusGlows[ns.nodeId] = g;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return nodes.map((n: WorkflowNode) => {
|
|
110
|
+
const glow =
|
|
111
|
+
statusGlows[n.id] ??
|
|
112
|
+
(nodeActions?.[n.id]?.some((a) => a.switcher) ? 'pending' : undefined) ??
|
|
113
|
+
statusToGlow(n.data?.status ?? '');
|
|
114
|
+
|
|
115
|
+
const actions = nodeActions?.[n.id];
|
|
116
|
+
|
|
117
|
+
if (!glow && !actions) return n;
|
|
86
118
|
return {
|
|
87
119
|
...n,
|
|
88
|
-
|
|
120
|
+
data: {
|
|
121
|
+
...n.data,
|
|
122
|
+
actions,
|
|
123
|
+
},
|
|
124
|
+
style: { ...(n.style ?? {}), boxShadow: glow ? GLOW[glow] : undefined },
|
|
89
125
|
};
|
|
90
|
-
})
|
|
91
|
-
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
[nodes, nodeStatuses, nodeActions],
|
|
92
129
|
);
|
|
93
130
|
const defaultActionGroups: ToolbarAction[][] = [
|
|
94
131
|
[
|