ai-design-system 0.1.68 → 0.1.70
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/{composites → blocks}/EvalSessionDetailsPanel/EvalSessionDetailsPanel.stories.tsx +1 -1
- package/components/{composites → blocks}/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx +15 -5
- package/components/blocks/WorkflowCanvas/interfaces.ts +2 -1
- package/components/blocks/index.ts +4 -0
- package/components/composites/DashboardChart/DashboardChart.tsx +107 -31
- package/components/composites/DataTable/EnhancedDataTable.tsx +6 -4
- package/components/composites/ProjectSwitcher/ProjectSwitcher.tsx +2 -2
- package/components/composites/index.ts +1 -3
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.behaviors.stories.tsx +13 -6
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.mocks.ts +30 -0
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.stories.tsx +9 -2
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.tsx +25 -19
- package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +8 -0
- package/components/features/NodeEditor/NodeEditor.tsx +25 -1
- package/components/features/PageLayout/PageLayout.stories.tsx +2 -2
- package/components/features/index.ts +4 -3
- package/dist/index.cjs +1285 -1190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +25 -30
- package/dist/index.js +1286 -1191
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- /package/components/{composites → blocks}/EvalSessionDetailsPanel/index.ts +0 -0
package/components/{composites → blocks}/EvalSessionDetailsPanel/EvalSessionDetailsPanel.stories.tsx
RENAMED
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"
|
|
|
2
2
|
import { EvalSessionDetailsPanel } from "./EvalSessionDetailsPanel"
|
|
3
3
|
|
|
4
4
|
const meta = {
|
|
5
|
-
title: "
|
|
5
|
+
title: "Blocks/EvalSessionDetailsPanel",
|
|
6
6
|
component: EvalSessionDetailsPanel,
|
|
7
7
|
parameters: {
|
|
8
8
|
layout: "padded",
|
package/components/{composites → blocks}/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx
RENAMED
|
@@ -18,12 +18,14 @@ export interface EvalSessionDetails {
|
|
|
18
18
|
export interface EvalSessionDetailsPanelProps {
|
|
19
19
|
sessionDetails: EvalSessionDetails
|
|
20
20
|
activeTab: string
|
|
21
|
+
actionHandlers?: { onTriggerEvaluation?: () => void }
|
|
22
|
+
workflowContent?: React.ReactNode
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export const EvalTriggerButton = ({ onClick }: { onClick: () => void }) => (
|
|
24
|
-
<Button onClick={onClick} size="sm">
|
|
25
|
-
<Icon name=
|
|
26
|
-
Trigger
|
|
25
|
+
export const EvalTriggerButton = ({ onClick, loading }: { onClick: () => void; loading?: boolean }) => (
|
|
26
|
+
<Button onClick={onClick} variant="default" size="sm" className="h-8" disabled={loading}>
|
|
27
|
+
<Icon name={loading ? 'loader-2' : 'play'} className={`w-4 h-4 mr-2${loading ? ' animate-spin' : ''}`} />
|
|
28
|
+
{loading ? 'Evaluating...' : 'Trigger'}
|
|
27
29
|
</Button>
|
|
28
30
|
)
|
|
29
31
|
|
|
@@ -42,7 +44,7 @@ const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
|
42
44
|
})
|
|
43
45
|
|
|
44
46
|
export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
45
|
-
({ sessionDetails, activeTab }) => {
|
|
47
|
+
({ sessionDetails, activeTab, actionHandlers, workflowContent }) => {
|
|
46
48
|
return (
|
|
47
49
|
<div className="flex flex-col h-full bg-background">
|
|
48
50
|
<Tabs value={activeTab} className="flex-1 flex flex-col min-h-0">
|
|
@@ -79,6 +81,14 @@ export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
|
79
81
|
</div>
|
|
80
82
|
</ScrollArea>
|
|
81
83
|
</TabsContent>
|
|
84
|
+
|
|
85
|
+
<TabsContent value="workflow" className="absolute inset-0 m-0 p-0">
|
|
86
|
+
{workflowContent || (
|
|
87
|
+
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
88
|
+
No workflow data available.
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</TabsContent>
|
|
82
92
|
</div>
|
|
83
93
|
</Tabs>
|
|
84
94
|
</div>
|
|
@@ -2,8 +2,9 @@ import type { Connection, EdgeChange, Node, NodeChange, OnConnectStartParams } f
|
|
|
2
2
|
import type React from "react";
|
|
3
3
|
import type { StateNodeData } from "@/components/composites/StateNode";
|
|
4
4
|
import type { TransitionNodeData } from "@/components/composites/TransitionNode";
|
|
5
|
+
import type { TriggerNodeData } from "@/components/composites/TriggerNode";
|
|
5
6
|
|
|
6
|
-
export type WorkflowNodeData = StateNodeData | TransitionNodeData;
|
|
7
|
+
export type WorkflowNodeData = StateNodeData | TransitionNodeData | TriggerNodeData;
|
|
7
8
|
export type WorkflowNode = Node<WorkflowNodeData>;
|
|
8
9
|
|
|
9
10
|
export interface WorkflowEdge {
|
|
@@ -27,3 +27,7 @@ export type { FormReportsSectionProps } from './FormReportsSection'
|
|
|
27
27
|
|
|
28
28
|
export { InboxPanel } from './InboxPanel'
|
|
29
29
|
export type { InboxPanelProps } from './InboxPanel'
|
|
30
|
+
|
|
31
|
+
// EvalSessionDetailsPanel Block
|
|
32
|
+
export { EvalSessionDetailsPanel, EvalTriggerButton } from './EvalSessionDetailsPanel'
|
|
33
|
+
export type { EvalSessionDetailsPanelProps, EvalSessionDetails } from './EvalSessionDetailsPanel'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Area,
|
|
2
|
+
import { Area, Line, ComposedChart, CartesianGrid, XAxis } from "recharts"
|
|
3
|
+
import type { TooltipContentProps } from "recharts"
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
ChartContainer,
|
|
@@ -70,21 +71,29 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
const filteredSeries = React.useMemo(() => {
|
|
73
|
-
|
|
74
|
+
// 1. Sort ascending chronologically (oldest to newest)
|
|
75
|
+
const sortedSeries = [...series].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
|
76
|
+
|
|
77
|
+
const referenceDate = sortedSeries.length > 0 ? sortedSeries[sortedSeries.length - 1].date : new Date().toISOString()
|
|
74
78
|
let daysToSubtract = 90
|
|
75
79
|
if (timeRange === "30d") {
|
|
76
80
|
daysToSubtract = 30
|
|
77
81
|
} else if (timeRange === "7d") {
|
|
78
82
|
daysToSubtract = 7
|
|
79
|
-
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let filtered = sortedSeries;
|
|
86
|
+
if (timeRange === "10" || timeRange === "20" || timeRange === "30") {
|
|
80
87
|
// Special logic for count-based limits if passed as timeRange
|
|
81
88
|
const limit = parseInt(timeRange, 10)
|
|
82
|
-
|
|
89
|
+
filtered = sortedSeries.slice(-limit)
|
|
90
|
+
} else {
|
|
91
|
+
const startDate = new Date(referenceDate)
|
|
92
|
+
startDate.setDate(startDate.getDate() - daysToSubtract)
|
|
93
|
+
filtered = sortedSeries.filter(item => new Date(item.date) >= startDate)
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
startDate.setDate(startDate.getDate() - daysToSubtract)
|
|
87
|
-
return series.filter(item => new Date(item.date) >= startDate)
|
|
96
|
+
return filtered.map(item => ({ ...item, timestamp: new Date(item.date).getTime() }))
|
|
88
97
|
}, [series, timeRange])
|
|
89
98
|
|
|
90
99
|
return (
|
|
@@ -137,7 +146,7 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
137
146
|
}}
|
|
138
147
|
className={cn("aspect-auto w-full", chartClassName || "h-[250px]")}
|
|
139
148
|
>
|
|
140
|
-
<
|
|
149
|
+
<ComposedChart data={filteredSeries}>
|
|
141
150
|
<defs>
|
|
142
151
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
|
143
152
|
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={1} />
|
|
@@ -152,50 +161,117 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
152
161
|
</defs>
|
|
153
162
|
<CartesianGrid vertical={false} />
|
|
154
163
|
<XAxis
|
|
155
|
-
dataKey="
|
|
164
|
+
dataKey="timestamp"
|
|
165
|
+
type="number"
|
|
166
|
+
scale="time"
|
|
167
|
+
domain={['dataMin', 'dataMax']}
|
|
156
168
|
tickLine={false}
|
|
157
169
|
axisLine={false}
|
|
158
170
|
tickMargin={8}
|
|
159
171
|
minTickGap={32}
|
|
160
172
|
tickFormatter={(value) => {
|
|
161
173
|
const date = new Date(value)
|
|
162
|
-
return date.
|
|
174
|
+
return date.toLocaleString("en-US", {
|
|
163
175
|
month: "short",
|
|
164
176
|
day: "numeric",
|
|
177
|
+
hour: "numeric",
|
|
178
|
+
minute: "2-digit"
|
|
165
179
|
})
|
|
166
180
|
}}
|
|
167
181
|
/>
|
|
168
182
|
<ChartTooltip
|
|
169
183
|
cursor={false}
|
|
170
|
-
content={
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
content={(props: TooltipContentProps) => {
|
|
185
|
+
const { active, payload, content: _content, label: _label, ...rest } = props;
|
|
186
|
+
if (!active || !payload?.length) return null;
|
|
187
|
+
|
|
188
|
+
const uniquePayload = payload.filter(
|
|
189
|
+
(item, index, self) =>
|
|
190
|
+
index === self.findIndex((t) => String(t.dataKey) === String(item.dataKey)),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<ChartTooltipContent
|
|
195
|
+
{...rest}
|
|
196
|
+
payload={uniquePayload}
|
|
197
|
+
labelFormatter={(value, tooltipPayload) => {
|
|
198
|
+
const typedPayload = tooltipPayload as Array<{ payload: { timestamp?: number } }>;
|
|
199
|
+
const timestamp = typedPayload?.[0]?.payload?.timestamp;
|
|
200
|
+
if (!timestamp) return null;
|
|
201
|
+
const date = new Date(timestamp);
|
|
202
|
+
if (isNaN(date.getTime())) return ""
|
|
203
|
+
return date.toLocaleDateString("en-US", {
|
|
204
|
+
month: "short",
|
|
205
|
+
year: "numeric",
|
|
206
|
+
})
|
|
207
|
+
}}
|
|
208
|
+
formatter={(value, name, item, index, tooltipPayload) => {
|
|
209
|
+
const typedPayload = tooltipPayload as { timestamp?: number };
|
|
210
|
+
const typedItem = item as { color?: string };
|
|
211
|
+
const labelText = name === "desktop" ? desktopLabel : (name === "mobile" ? mobileLabel : name);
|
|
212
|
+
const timeStr = typedPayload?.timestamp ? new Date(typedPayload.timestamp).toLocaleTimeString("en-US", {
|
|
213
|
+
hour: "numeric",
|
|
214
|
+
minute: "2-digit"
|
|
215
|
+
}) : null;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex flex-col w-full">
|
|
219
|
+
<div className="flex w-full items-center gap-2">
|
|
220
|
+
<div
|
|
221
|
+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
|
|
222
|
+
style={{ backgroundColor: typedItem.color }}
|
|
223
|
+
/>
|
|
224
|
+
<div className="flex flex-1 justify-between items-center gap-4">
|
|
225
|
+
<span className="text-muted-foreground">{labelText as React.ReactNode}</span>
|
|
226
|
+
<span className="font-mono font-medium tabular-nums text-foreground">{value as React.ReactNode}</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
{timeStr && (
|
|
230
|
+
<div className="text-[10px] text-muted-foreground self-end mt-0.5">
|
|
231
|
+
{timeStr}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}}
|
|
237
|
+
indicator="dot"
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}}
|
|
181
241
|
/>
|
|
182
242
|
{showMobile && (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
243
|
+
<>
|
|
244
|
+
<Area
|
|
245
|
+
dataKey="mobile"
|
|
246
|
+
type="monotone"
|
|
247
|
+
fill="url(#fillMobile)"
|
|
248
|
+
stroke="none"
|
|
249
|
+
/>
|
|
250
|
+
<Line
|
|
251
|
+
dataKey="mobile"
|
|
252
|
+
type="monotone"
|
|
253
|
+
stroke="var(--color-mobile)"
|
|
254
|
+
strokeWidth={2}
|
|
255
|
+
dot={false}
|
|
256
|
+
activeDot={{ r: 4 }}
|
|
257
|
+
/>
|
|
258
|
+
</>
|
|
190
259
|
)}
|
|
191
260
|
<Area
|
|
192
261
|
dataKey="desktop"
|
|
193
|
-
type="
|
|
262
|
+
type="monotone"
|
|
194
263
|
fill="url(#fillDesktop)"
|
|
264
|
+
stroke="none"
|
|
265
|
+
/>
|
|
266
|
+
<Line
|
|
267
|
+
dataKey="desktop"
|
|
268
|
+
type="monotone"
|
|
195
269
|
stroke="var(--color-desktop)"
|
|
196
|
-
|
|
270
|
+
strokeWidth={2}
|
|
271
|
+
dot={false}
|
|
272
|
+
activeDot={{ r: 4 }}
|
|
197
273
|
/>
|
|
198
|
-
</
|
|
274
|
+
</ComposedChart>
|
|
199
275
|
</ChartContainer>
|
|
200
276
|
</CardContent>
|
|
201
277
|
</Card>
|
|
@@ -506,10 +506,12 @@ export function EnhancedDataTable({
|
|
|
506
506
|
))}
|
|
507
507
|
</DropdownMenuContent>
|
|
508
508
|
</DropdownMenu>
|
|
509
|
-
|
|
510
|
-
<
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
{onCreateClick && (
|
|
510
|
+
<Button variant="outline" size="sm" className="h-8" onClick={onCreateClick}>
|
|
511
|
+
<Icon name="plus" size="sm" />
|
|
512
|
+
<span>{createButtonLabel}</span>
|
|
513
|
+
</Button>
|
|
514
|
+
)}
|
|
513
515
|
{rightActions}
|
|
514
516
|
</div>
|
|
515
517
|
</div>
|
|
@@ -34,7 +34,7 @@ export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
|
|
|
34
34
|
({ projects, selectedProjectId, onSelectProject, onCreateProject, className }) => {
|
|
35
35
|
const [open, setOpen] = React.useState(false)
|
|
36
36
|
|
|
37
|
-
const selectedProject = projects.find((p) => p.id === selectedProjectId)
|
|
37
|
+
const selectedProject = projects.find((p) => String(p.id) === String(selectedProjectId))
|
|
38
38
|
const displayLabel = selectedProject ? selectedProject.name : "Select Project..."
|
|
39
39
|
|
|
40
40
|
const defaultValue = selectedProject ? `${selectedProject.name}-${selectedProject.id}` : undefined
|
|
@@ -86,7 +86,7 @@ export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
|
|
|
86
86
|
</div>
|
|
87
87
|
<Icon name="check"
|
|
88
88
|
className={`h-4 w-4 ${
|
|
89
|
-
selectedProjectId === project.id ? "opacity-100" : "opacity-0"
|
|
89
|
+
String(selectedProjectId) === String(project.id) ? "opacity-100" : "opacity-0"
|
|
90
90
|
}`}
|
|
91
91
|
/>
|
|
92
92
|
</CommandItem>
|
|
@@ -206,6 +206,4 @@ export type { SessionHeaderProps, ChatSessionInfo } from './SessionHeader'
|
|
|
206
206
|
export { AppBreadcrumb } from './AppBreadcrumb'
|
|
207
207
|
export type { AppBreadcrumbProps, BreadcrumbItemData } from './AppBreadcrumb'
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
export { EvalSessionDetailsPanel, EvalTriggerButton } from './EvalSessionDetailsPanel'
|
|
211
|
-
export type { EvalSessionDetailsPanelProps, EvalSessionDetails } from './EvalSessionDetailsPanel'
|
|
209
|
+
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import * as React from "react"
|
|
2
3
|
import { expect, within, userEvent, fn } from "@storybook/test"
|
|
3
4
|
import { EvalDashboardFeature } from "./EvalDashboardFeature"
|
|
4
|
-
import { evalDashboardFeatureStateMock } from "./EvalDashboardFeature.mocks"
|
|
5
|
+
import { evalDashboardFeatureStateMock, workflowMockNodes, workflowMockEdges } from "./EvalDashboardFeature.mocks"
|
|
6
|
+
import { NodeEditor } from "@/components/features/NodeEditor"
|
|
7
|
+
|
|
8
|
+
const workflowMock = (
|
|
9
|
+
<NodeEditor nodes={workflowMockNodes} edges={workflowMockEdges} showMinimap={false} interactive={false} />
|
|
10
|
+
)
|
|
5
11
|
|
|
6
12
|
const meta = {
|
|
7
13
|
title: "Features/EvalDashboardFeature/Behaviors",
|
|
@@ -25,16 +31,17 @@ export const Interactive: Story = {
|
|
|
25
31
|
actionHandlers: {
|
|
26
32
|
onTriggerEvaluation: fn(),
|
|
27
33
|
},
|
|
34
|
+
workflowContent: workflowMock,
|
|
28
35
|
},
|
|
29
36
|
play: async ({ canvasElement }) => {
|
|
30
37
|
const canvas = within(canvasElement)
|
|
31
38
|
|
|
32
|
-
//
|
|
39
|
+
// 4 tabs: Golden Evals, System Prompts, Outputs, Workflow
|
|
33
40
|
const tabs = await canvas.findAllByRole("tab")
|
|
34
|
-
expect(tabs.length).
|
|
41
|
+
expect(tabs.length).toBe(4)
|
|
35
42
|
|
|
36
|
-
// Switch to
|
|
37
|
-
const
|
|
38
|
-
await userEvent.click(
|
|
43
|
+
// Switch to Workflow tab
|
|
44
|
+
const workflowTab = canvas.getByRole("tab", { name: /Workflow/i })
|
|
45
|
+
await userEvent.click(workflowTab)
|
|
39
46
|
},
|
|
40
47
|
}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import type { UseEvalDashboardFeatureReturn } from "./useEvalDashboardFeature.d"
|
|
2
|
+
import type { WorkflowNode, WorkflowEdge } from "@/components/blocks/WorkflowCanvas"
|
|
3
|
+
|
|
4
|
+
export const workflowMockNodes: WorkflowNode[] = [
|
|
5
|
+
{ id: 'trigger-1', type: 'trigger', position: { x: 100, y: 20 }, data: { label: 'Agentic Eval', type: 'trigger', status: 'idle' } },
|
|
6
|
+
{ id: 'state-1', type: 'state', position: { x: 250, y: 20 }, data: { label: 'Initialize Eval', type: 'state', description: 'Parse session messages and config', status: 'idle' } },
|
|
7
|
+
{ id: 'transition-1', type: 'transition', position: { x: 400, y: 20 }, data: { label: 'Parse Session', type: 'transition', status: 'idle' } },
|
|
8
|
+
{ id: 'state-2', type: 'state', position: { x: 250, y: 140 }, data: { label: 'Fetch System Prompt', type: 'state', description: 'Load system prompt from session', status: 'idle' } },
|
|
9
|
+
{ id: 'transition-2', type: 'transition', position: { x: 400, y: 140 }, data: { label: 'Check Config', type: 'transition', description: 'Check if multi-agent config exists', status: 'idle' } },
|
|
10
|
+
{ id: 'state-2b', type: 'state', position: { x: 250, y: 260 }, data: { label: 'Fetch Config Prompt', type: 'state', description: 'Load agent config system prompt', status: 'idle' } },
|
|
11
|
+
{ id: 'transition-2b', type: 'transition', position: { x: 400, y: 260 }, data: { label: 'Format Prompt', type: 'transition', description: 'Build multi-agent system prompt', status: 'idle' } },
|
|
12
|
+
{ id: 'state-3', type: 'state', position: { x: 250, y: 380 }, data: { label: 'Build Messages', type: 'state', description: 'Construct LLM message payload', status: 'idle' } },
|
|
13
|
+
{ id: 'transition-3', type: 'transition', position: { x: 400, y: 380 }, data: { label: 'LLM Call', type: 'transition', status: 'idle' } },
|
|
14
|
+
{ id: 'state-4', type: 'state', position: { x: 250, y: 500 }, data: { label: 'Parse Eval', type: 'state', description: 'Parse LLM eval response', status: 'idle' } },
|
|
15
|
+
{ id: 'transition-4', type: 'transition', position: { x: 400, y: 500 }, data: { label: 'Persist', type: 'transition', status: 'idle' } },
|
|
16
|
+
{ id: 'state-5', type: 'state', position: { x: 250, y: 620 }, data: { label: 'Complete', type: 'state', description: 'Eval finished', status: 'idle', isTerminal: true } },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const workflowMockEdges: WorkflowEdge[] = [
|
|
20
|
+
{ id: 'e-trigger-1', source: 'trigger-1', target: 'state-1' },
|
|
21
|
+
{ id: 'e-1-transition-1', source: 'state-1', target: 'transition-1' },
|
|
22
|
+
{ id: 'e-transition-1-2', source: 'transition-1', target: 'state-2' },
|
|
23
|
+
{ id: 'e-2-transition-2', source: 'state-2', target: 'transition-2' },
|
|
24
|
+
{ id: 'e-transition-2-2b', source: 'transition-2', target: 'state-2b' },
|
|
25
|
+
{ id: 'e-2b-transition-2b', source: 'state-2b', target: 'transition-2b' },
|
|
26
|
+
{ id: 'e-transition-2b-3', source: 'transition-2b', target: 'state-3' },
|
|
27
|
+
{ id: 'e-3-transition-3', source: 'state-3', target: 'transition-3' },
|
|
28
|
+
{ id: 'e-transition-3-4', source: 'transition-3', target: 'state-4' },
|
|
29
|
+
{ id: 'e-4-transition-4', source: 'state-4', target: 'transition-4' },
|
|
30
|
+
{ id: 'e-transition-4-5', source: 'transition-4', target: 'state-5' },
|
|
31
|
+
]
|
|
2
32
|
|
|
3
33
|
export const evalDashboardFeatureStateMock: UseEvalDashboardFeatureReturn = {
|
|
4
34
|
inbox: {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import * as React from "react"
|
|
2
3
|
import { EvalDashboardFeature } from "./EvalDashboardFeature"
|
|
3
|
-
import { evalDashboardFeatureStateMock } from "./EvalDashboardFeature.mocks"
|
|
4
|
+
import { evalDashboardFeatureStateMock, workflowMockNodes, workflowMockEdges } from "./EvalDashboardFeature.mocks"
|
|
4
5
|
import { useEvalDashboardFeatureMock } from "./useEvalDashboardFeature.mock"
|
|
6
|
+
import { NodeEditor } from "@/components/features/NodeEditor"
|
|
7
|
+
|
|
8
|
+
const workflowMock = (
|
|
9
|
+
<NodeEditor nodes={workflowMockNodes} edges={workflowMockEdges} showMinimap={false} interactive={false} />
|
|
10
|
+
)
|
|
5
11
|
|
|
6
12
|
const meta = {
|
|
7
13
|
title: "Features/EvalDashboardFeature",
|
|
@@ -23,12 +29,13 @@ type Story = StoryObj<typeof meta>
|
|
|
23
29
|
export const Default: Story = {
|
|
24
30
|
args: {
|
|
25
31
|
...evalDashboardFeatureStateMock,
|
|
32
|
+
workflowContent: workflowMock,
|
|
26
33
|
},
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export const WithStateManagement: Story = {
|
|
30
37
|
render: () => {
|
|
31
38
|
const state = useEvalDashboardFeatureMock()
|
|
32
|
-
return <EvalDashboardFeature {...state} className="h-[100dvh]" />
|
|
39
|
+
return <EvalDashboardFeature {...state} workflowContent={workflowMock} className="h-[100dvh]" />
|
|
33
40
|
},
|
|
34
41
|
}
|
|
@@ -2,7 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import { InboxPanel } from "@/components/blocks/InboxPanel"
|
|
3
3
|
import { SectionLayout } from "@/components/blocks/SectionLayout"
|
|
4
4
|
import { DashboardChart } from "@/components/composites/DashboardChart"
|
|
5
|
-
import { EvalSessionDetailsPanel, EvalTriggerButton } from "@/components/
|
|
5
|
+
import { EvalSessionDetailsPanel, EvalTriggerButton } from "@/components/blocks/EvalSessionDetailsPanel"
|
|
6
6
|
import type { DashboardRow } from "@/components/composites/DataTable"
|
|
7
7
|
import { cn } from "@/lib/utils"
|
|
8
8
|
import type {
|
|
@@ -17,10 +17,11 @@ export interface EvalDashboardFeatureProps {
|
|
|
17
17
|
data: EvalDashboardFeatureData | null
|
|
18
18
|
actionHandlers?: EvalDashboardFeatureActionHandlers
|
|
19
19
|
className?: string
|
|
20
|
+
workflowContent?: React.ReactNode
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
23
|
-
({ inbox, data, actionHandlers, className }) => {
|
|
24
|
+
({ inbox, data, actionHandlers, className, workflowContent }) => {
|
|
24
25
|
const [showInbox, setShowInbox] = React.useState(true)
|
|
25
26
|
const [activeTab, setActiveTab] = React.useState("golden-evals")
|
|
26
27
|
|
|
@@ -28,12 +29,13 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
28
29
|
inbox.items.find(s => s.id === inbox.selectedItemId) || null
|
|
29
30
|
, [inbox.items, inbox.selectedItemId])
|
|
30
31
|
|
|
31
|
-
// Calculate average score for the trend section
|
|
32
|
+
// Calculate average score for the trend section (project-wide from runsHistory)
|
|
32
33
|
const avgScore = React.useMemo(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const runs = data?.runsHistory || [];
|
|
35
|
+
if (runs.length === 0) return "0.0";
|
|
36
|
+
const sum = runs.reduce((acc, curr) => acc + curr.score, 0);
|
|
37
|
+
return (sum / runs.length).toFixed(1);
|
|
38
|
+
}, [data?.runsHistory]);
|
|
37
39
|
|
|
38
40
|
const inboxItems = React.useMemo(() => {
|
|
39
41
|
return inbox.items.map(session => ({
|
|
@@ -45,18 +47,19 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
45
47
|
}, [inbox.items]);
|
|
46
48
|
|
|
47
49
|
const mockChartData = React.useMemo(() => {
|
|
50
|
+
const runs = data?.runsHistory || [];
|
|
48
51
|
// Sort items chronologically for the chart
|
|
49
|
-
const sortedItems = [...
|
|
52
|
+
const sortedItems = [...runs].sort(
|
|
50
53
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
51
54
|
)
|
|
52
55
|
|
|
53
|
-
return sortedItems.map(
|
|
54
|
-
date:
|
|
56
|
+
return sortedItems.map(run => ({
|
|
57
|
+
date: run.date,
|
|
55
58
|
// We map score to 'desktop' since DashboardChart is hardcoded for now
|
|
56
|
-
desktop:
|
|
59
|
+
desktop: run.score,
|
|
57
60
|
mobile: 0
|
|
58
61
|
}))
|
|
59
|
-
}, [
|
|
62
|
+
}, [data?.runsHistory]);
|
|
60
63
|
|
|
61
64
|
const sessionDetails = selectedSession ? {
|
|
62
65
|
id: selectedSession.id,
|
|
@@ -86,12 +89,12 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
86
89
|
<DashboardChart
|
|
87
90
|
series={mockChartData}
|
|
88
91
|
title="Experiments Analysis"
|
|
89
|
-
description={`Average Score: ${avgScore}
|
|
92
|
+
description={`Average Score: ${avgScore} (per run)`}
|
|
90
93
|
shortDescription={`Avg: ${avgScore}`}
|
|
91
94
|
timeRanges={[
|
|
92
|
-
{ value: "10", label: "Last 10
|
|
93
|
-
{ value: "20", label: "Last 20
|
|
94
|
-
{ value: "30", label: "Last 30
|
|
95
|
+
{ value: "10", label: "Last 10 Runs", shortLabel: "Last 10" },
|
|
96
|
+
{ value: "20", label: "Last 20 Runs", shortLabel: "Last 20" },
|
|
97
|
+
{ value: "30", label: "Last 30 Runs", shortLabel: "Last 30" }
|
|
95
98
|
]}
|
|
96
99
|
desktopLabel="Score"
|
|
97
100
|
mobileLabel=""
|
|
@@ -144,7 +147,8 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
144
147
|
tabs: [
|
|
145
148
|
{ label: "Golden Evals", value: "golden-evals" },
|
|
146
149
|
{ label: "System Prompts", value: "prompts" },
|
|
147
|
-
{ label: "Outputs", value: "outputs" }
|
|
150
|
+
{ label: "Outputs", value: "outputs" },
|
|
151
|
+
...(workflowContent ? [{ label: "Workflow", value: "workflow" }] : []),
|
|
148
152
|
],
|
|
149
153
|
defaultTab: activeTab,
|
|
150
154
|
onTabChange: setActiveTab,
|
|
@@ -154,13 +158,15 @@ export const EvalDashboardFeature = React.memo<EvalDashboardFeatureProps>(
|
|
|
154
158
|
onClick: () => setShowInbox(!showInbox)
|
|
155
159
|
},
|
|
156
160
|
actions: actionHandlers?.onTriggerEvaluation ? (
|
|
157
|
-
<EvalTriggerButton onClick={actionHandlers.onTriggerEvaluation} />
|
|
158
|
-
) : undefined
|
|
161
|
+
<EvalTriggerButton onClick={actionHandlers.onTriggerEvaluation} loading={actionHandlers.isTriggering} />
|
|
162
|
+
) : undefined,
|
|
159
163
|
} : undefined,
|
|
160
164
|
content: sessionDetails ? (
|
|
161
165
|
<EvalSessionDetailsPanel
|
|
162
166
|
sessionDetails={sessionDetails}
|
|
163
167
|
activeTab={activeTab}
|
|
168
|
+
actionHandlers={actionHandlers}
|
|
169
|
+
workflowContent={workflowContent}
|
|
164
170
|
/>
|
|
165
171
|
) : (
|
|
166
172
|
<div className="flex-1 flex items-center justify-center text-muted-foreground bg-background h-full">
|
|
@@ -28,10 +28,18 @@ export interface EvalDashboardFeatureData {
|
|
|
28
28
|
goldenEvals: GoldenEvalResult[];
|
|
29
29
|
systemPrompt: string;
|
|
30
30
|
outputTranscript: string;
|
|
31
|
+
runsHistory?: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
session_id: string;
|
|
34
|
+
date: string;
|
|
35
|
+
score: number;
|
|
36
|
+
total: number;
|
|
37
|
+
}>;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export interface EvalDashboardFeatureActionHandlers {
|
|
34
41
|
onTriggerEvaluation?: () => void;
|
|
42
|
+
isTriggering?: boolean;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface UseEvalDashboardFeatureReturn {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useMemo } from "react";
|
|
3
4
|
import { WorkflowToolbar, WorkflowToolbarActions } from "@/components/composites/WorkflowToolbar";
|
|
4
5
|
import { WorkflowCanvas } from "@/components/blocks/WorkflowCanvas";
|
|
5
6
|
import type { WorkflowNode, WorkflowEdge } from "@/components/blocks/WorkflowCanvas";
|
|
@@ -7,6 +8,13 @@ import type { ToolbarAction, WorkflowVersion } from "@/components/composites/Wor
|
|
|
7
8
|
import type { Connection, NodeChange, EdgeChange } from "@xyflow/react";
|
|
8
9
|
import { cn } from "@/lib/utils";
|
|
9
10
|
|
|
11
|
+
const GLOW: Record<string, string> = {
|
|
12
|
+
active: "0 0 14px 5px rgba(99, 102, 241, 0.9)",
|
|
13
|
+
pending: "0 0 14px 5px rgba(251, 146, 60, 1)",
|
|
14
|
+
done: "0 0 10px 3px rgba(34, 197, 94, 0.75)",
|
|
15
|
+
error: "0 0 14px 5px rgba(239, 68, 68, 0.9)",
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
export interface NodeEditorProps {
|
|
11
19
|
// Toolbar — left
|
|
12
20
|
workflowName?: string;
|
|
@@ -39,6 +47,9 @@ export interface NodeEditorProps {
|
|
|
39
47
|
interactive?: boolean;
|
|
40
48
|
hideDefaultActions?: boolean;
|
|
41
49
|
className?: string;
|
|
50
|
+
|
|
51
|
+
/** Per-node highlight states: { [nodeId]: 'active' | 'pending' | 'done' | 'error' } */
|
|
52
|
+
nodeHighlights?: Record<string, string>;
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
export function NodeEditor({
|
|
@@ -65,7 +76,20 @@ export function NodeEditor({
|
|
|
65
76
|
interactive = false,
|
|
66
77
|
hideDefaultActions = false,
|
|
67
78
|
className,
|
|
79
|
+
nodeHighlights,
|
|
68
80
|
}: NodeEditorProps) {
|
|
81
|
+
const highlightedNodes = useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
nodes.map((n: WorkflowNode) => {
|
|
84
|
+
const glow = nodeHighlights?.[n.id];
|
|
85
|
+
if (!glow) return n;
|
|
86
|
+
return {
|
|
87
|
+
...n,
|
|
88
|
+
style: { ...(n.style ?? {}), boxShadow: GLOW[glow] },
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
[nodes, nodeHighlights],
|
|
92
|
+
);
|
|
69
93
|
const defaultActionGroups: ToolbarAction[][] = [
|
|
70
94
|
[
|
|
71
95
|
{ id: "undo", icon: "undo-2", title: "Undo", onClick: onUndo, disabled: !canUndo },
|
|
@@ -87,7 +111,7 @@ export function NodeEditor({
|
|
|
87
111
|
className="h-full w-full"
|
|
88
112
|
edges={edges}
|
|
89
113
|
interactive={interactive}
|
|
90
|
-
nodes={
|
|
114
|
+
nodes={highlightedNodes}
|
|
91
115
|
showMinimap={showMinimap}
|
|
92
116
|
topLeft={
|
|
93
117
|
<WorkflowToolbar
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
mockPageLayoutRefinementMessages,
|
|
10
10
|
mockSidebarConfig,
|
|
11
11
|
} from './PageLayout.mocks'
|
|
12
|
-
import { NodeEditor } from '
|
|
12
|
+
import { NodeEditor } from '@/components/features/NodeEditor'
|
|
13
13
|
import { RefinementPanel } from '../RefinementPanel/RefinementPanel'
|
|
14
|
-
import { mockEdges, mockNodes, mockVersions } from '
|
|
14
|
+
import { mockEdges, mockNodes, mockVersions } from '@/components/features/NodeEditor/NodeEditor.mocks'
|
|
15
15
|
import { DashboardFeature } from '../DashboardFeature/DashboardFeature'
|
|
16
16
|
import { useDashboardIntegrationMock } from './useDashboardIntegration.mock'
|
|
17
17
|
|
|
@@ -17,13 +17,14 @@ export type { RefinementPanelProps, RefinementMessage } from "./RefinementPanel"
|
|
|
17
17
|
export { SpecNavigator } from "./SpecNavigator";
|
|
18
18
|
export type { SpecNavigatorProps } from "./SpecNavigator";
|
|
19
19
|
|
|
20
|
+
// NodeEditor Feature
|
|
21
|
+
export { NodeEditor } from "./NodeEditor";
|
|
22
|
+
export type { NodeEditorProps } from "./NodeEditor";
|
|
23
|
+
|
|
20
24
|
// TextEditor Feature
|
|
21
25
|
export { TextEditor } from "./TextEditor";
|
|
22
26
|
export type { TextEditorProps } from "./TextEditor";
|
|
23
27
|
|
|
24
|
-
// NodeEditor Feature
|
|
25
|
-
export { NodeEditor } from "./NodeEditor";
|
|
26
|
-
export type { NodeEditorProps } from "./NodeEditor";
|
|
27
28
|
|
|
28
29
|
// DashboardFeature Feature
|
|
29
30
|
export { DashboardFeature } from "./DashboardFeature";
|