From 20bcd9294259d8cdd259ccf76439166f4cc5c6a1 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sun, 27 Sep 2026 16:53:18 +0400 Subject: [PATCH] updated drawer --- frontend/DESIGN.md | 24 +- .../components/AgentPreviewSheet.test.tsx | 101 +++++ .../agents/components/AgentPreviewSheet.tsx | 81 ++++ .../src/agents/workflow/WorkflowBuilder.tsx | 78 ++-- .../agents/workflow/WorkflowPreview.test.tsx | 134 ++++++ .../src/agents/workflow/WorkflowPreview.tsx | 405 +++++++++--------- frontend/src/design/DesignSystem.tsx | 68 +++ 7 files changed, 636 insertions(+), 255 deletions(-) create mode 100644 frontend/src/agents/components/AgentPreviewSheet.test.tsx create mode 100644 frontend/src/agents/components/AgentPreviewSheet.tsx create mode 100644 frontend/src/agents/workflow/WorkflowPreview.test.tsx diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index f45d45b1..0d776303 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -291,12 +291,12 @@ question: is it a **thing** you open, move, share or delete as a whole, or a **place** where you read or edit content? The fill marks objects; the border marks structure. -| Role | Variant | Examples | -| -------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Thing (a tile) | `filled`, on a page or in a modal | agents, folders, sources, tools, custom models, teams, chunks, the shared agent card | -| Place (a panel) | `subtle` on the page, `outline` on a card (modal, drawer) | the agent form's sections, chart panels, stat tiles, log tables, schedule rows, the agent preview | -| Choice (picker tile) | `outline interactive`, or `OptionCard` | the Add tool tiles; selection is drawn by the border, so the border stays | -| Well | `filled padding="sm"` | code, a token to copy, run output, inside a panel (see Code blocks below) | +| Role | Variant | Examples | +| -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Thing (a tile) | `filled`, on a page or in a modal | agents, folders, sources, tools, custom models, teams, chunks, the shared agent card | +| Place (a panel) | `subtle` on the page or a right drawer, `outline` on a card (modal, bottom sheet) | the agent form's sections, chart panels, stat tiles, log tables, schedule rows, the agent preview | +| Choice (picker tile) | `outline interactive`, or `OptionCard` | the Add tool tiles; selection is drawn by the border, so the border stays | +| Well | `filled padding="sm"` | code, a token to copy, run output, inside a panel (see Code blocks below) | - Layers go page → panel (border) → well (fill), or page → tile (fill). `subtle` and `outline` are one role on two surfaces; in light mode they @@ -898,6 +898,17 @@ scroller) and the same blurred overlay. Modal, DialogContent and every Sheet sha `overlayScrim` in `lib/utils.ts` (`bg-black/25 backdrop-blur-xs dark:bg-black/50`). +A right drawer (`SheetContent side="right"`) is on `bg-background`, like the +page: its content paints no surface of its own, and panels in it are +`subtle` (see Card surfaces). An agent is previewed in one shared drawer, +`agents/components/AgentPreviewSheet`, for workflow and classic agents alike: +`w-full sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px]`, a `SheetTitle` +and a one-line `SheetDescription` (the agent's name) in a header padded +`pr-12` to clear the close X, an `info` Running badge while it answers, a +`Separator`, then the preview. The workflow preview's Execution details and +Artifacts rows are the answer's step-row recipe (see Alert), each step a +`subtle sm` panel with its output in a `filled sm` well. + Every phone bottom sheet has one shape: `bg-card`, 16px top corners, no top border, `max-h-sheet` (the visible viewport less the top safe-area inset and a 3rem strip, so the scrim above it can always be tapped to close), and bottom @@ -1184,6 +1195,7 @@ list stays reviewable. | `agents/workflow/WorkflowBuilder.tsx` | `shadcn/no-restyle` | The "Learn more" links in the Set state and Condition nodes' 12px hints keep the sentence's size and weight (`text-xs font-normal` on `link inline`). Two disables. | | `components/MessageInput.tsx` | `shadcn/no-restyle` | The queued-send Cancel is a `link inline` inside the composer's 12px status line, so it takes the line's size (`text-xs`). One disable, beside the send button's. | | `settings/PersonalAccessTokens.tsx` | `shadcn/no-restyle` | Token scope chips are identifiers, so the `neutral` Badge is set in mono (`font-mono`). One disable. | +| `agents/workflow/WorkflowPreview.tsx` | `shadcn/no-restyle` | A step's state changes (keys and values the app serialised) are `neutral` Badges set in mono (`font-mono`), like token scopes. One disable. | | `settings/traces/TraceChips.tsx` | `shadcn/no-restyle` | Trace stat chips (durations, counts) use tabular figures so they don't jitter between rows (`tabular-nums` on Badge). One disable. | | `modals/MCPServerModal.tsx` | `shadcn/no-restyle` | The authorization link inside the test-result Alert keeps the Alert's status colour (`text-current` on `link inline`). One disable (phase 11, 63g). | | `components/MermaidRenderer.tsx` | `shadcn/no-restyle` | The zoom readout between − and + is a `link inline` Button on the `bg-black/70` overlay; it keeps the overlay's white 12px regular text (`text-xs font-normal text-current`). One disable, beside the two zoom-button ones above (phase 11, 63g). | diff --git a/frontend/src/agents/components/AgentPreviewSheet.test.tsx b/frontend/src/agents/components/AgentPreviewSheet.test.tsx new file mode 100644 index 00000000..15e1f914 --- /dev/null +++ b/frontend/src/agents/components/AgentPreviewSheet.test.tsx @@ -0,0 +1,101 @@ +import { act } from 'react'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); +import { createRoot, type Root } from 'react-dom/client'; + +import AgentPreviewSheet from './AgentPreviewSheet'; + +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + +describe('AgentPreviewSheet', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + }); + + const render = async () => { + await act(async () => { + root.render( + undefined} + title="Preview" + description="Carrier onboarding FAQ" + actions={Running} + > +
Chat
+
, + ); + }); + }; + + it('is a right Sheet at the workflow preview widths', async () => { + await render(); + const content = document.querySelector( + '[data-slot="sheet-content"]', + )!; + expect(content.dataset.side).toBe('right'); + expect(content.className).toContain('lg:max-w-[800px]'); + expect(content.className).toContain('p-0'); + }); + + it('titles the drawer with SheetTitle and SheetDescription', async () => { + await render(); + expect( + document.querySelector('[data-slot="sheet-title"]')?.textContent, + ).toBe('Preview'); + expect( + document.querySelector('[data-slot="sheet-description"]')?.textContent, + ).toBe('Carrier onboarding FAQ'); + }); + + it('keeps header actions clear of the close button', async () => { + await render(); + const action = document.querySelector('[data-testid="action"]')!; + const header = action.parentElement!; + // The X sits at top-2 right-2 (32px wide); pr-12 keeps 48px free. + expect(header.className).toContain('pr-12'); + expect( + header.contains(document.querySelector('[data-slot="sheet-title"]')), + ).toBe(true); + }); + + // DESIGN maps "running" to info, as schedule runs do (was primary text). + it('shows a running run as an info badge in the header', async () => { + await act(async () => { + root.render( + undefined} + title="Preview" + running + > +
+ , + ); + }); + const badge = document.querySelector('[data-slot="badge"]')!; + expect(badge.dataset.variant).toBe('info'); + expect(badge.textContent).toBe('agents.schedules.status.running'); + }); + + it('renders the body under a separator on the sheet surface', async () => { + await render(); + const body = document.querySelector('[data-testid="body"]')!; + expect(body.textContent).toBe('Chat'); + expect(document.querySelector('[data-slot="separator"]')).not.toBeNull(); + const content = document.querySelector('[data-slot="sheet-content"]')!; + expect(content.querySelector('.bg-card')).toBeNull(); + }); +}); diff --git a/frontend/src/agents/components/AgentPreviewSheet.tsx b/frontend/src/agents/components/AgentPreviewSheet.tsx new file mode 100644 index 00000000..18dcc8c4 --- /dev/null +++ b/frontend/src/agents/components/AgentPreviewSheet.tsx @@ -0,0 +1,81 @@ +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetTitle, +} from '@/components/ui/sheet'; + +type AgentPreviewSheetProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + /** The drawer's title, e.g. "Preview". */ + title: string; + /** One muted line under the title: the agent's name. */ + description?: string; + /** Shows an info "Running" badge while the preview is answering. */ + running?: boolean; + /** Actions at the header's right end (New chat). */ + actions?: ReactNode; + /** The preview itself; fills the rest of the drawer. */ + children: ReactNode; +}; + +/** + * The drawer an agent is previewed in, for workflow and classic agents alike: + * a right Sheet with a title row, a rule, then the preview filling the rest. + * + * Args: + * open: Whether the drawer is open. + * onOpenChange: Called with the next open state (the X, Escape, the scrim). + * title: The drawer's title. + * description: A muted line under the title. + * running: Whether the preview is answering; shows a Running badge. + * actions: Nodes at the right end of the header, clear of the close button. + * children: The preview body. + */ +export default function AgentPreviewSheet({ + open, + onOpenChange, + title, + description, + running = false, + actions, + children, +}: AgentPreviewSheetProps) { + const { t } = useTranslation(); + return ( + + +
+ {/* pr-12 keeps the header clear of the close X at top-2 right-2. */} +
+
+ {title} + {description && ( + + {description} + + )} +
+ {running && ( + + {t('agents.schedules.status.running')} + + )} + {actions} +
+ +
{children}
+
+
+
+ ); +} diff --git a/frontend/src/agents/workflow/WorkflowBuilder.tsx b/frontend/src/agents/workflow/WorkflowBuilder.tsx index 830170c3..3a155584 100644 --- a/frontend/src/agents/workflow/WorkflowBuilder.tsx +++ b/frontend/src/agents/workflow/WorkflowBuilder.tsx @@ -72,7 +72,6 @@ import { } from '@/components/ui/select'; import { SectionHeader } from '@/components/ui/section-header'; import { SettingRow } from '@/components/ui/setting-row'; -import { Sheet, SheetContent } from '@/components/ui/sheet'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; @@ -90,6 +89,7 @@ import { import { getToolDisplayName } from '../../utils/toolUtils'; import { agentEditPath, agentsListPath } from '../paths'; import AgentPageHeader from '../AgentPageHeader'; +import AgentPreviewSheet from '../components/AgentPreviewSheet'; import { Agent } from '../types'; import { ConditionCase, WorkflowNode } from '../types/workflow'; import { @@ -122,6 +122,7 @@ import { StartNode, } from './nodes'; import WorkflowPreview from './WorkflowPreview'; +import { selectWorkflowPreviewStatus } from './workflowPreviewSlice'; import type { Model } from '../../models/types'; @@ -448,6 +449,7 @@ function WorkflowBuilderInner() { const navigate = useNavigate(); const token = useSelector(selectToken); const sourceDocs = useSelector(selectSourceDocs); + const previewStatus = useSelector(selectWorkflowPreviewStatus); const { agentId } = useParams<{ agentId?: string }>(); const [searchParams] = useSearchParams(); const folderId = searchParams.get('folder_id'); @@ -3299,42 +3301,46 @@ function WorkflowBuilderInner() {
- - - n.type !== 'note') - .map((n) => ({ - id: n.id, - type: n.type as - 'start' | 'end' | 'agent' | 'state' | 'code', - title: n.data.title || n.data.label || n.type, - position: n.position, - data: - n.type === 'code' - ? serializeCodeConfig(n.data.config) - : n.type === 'agent' - ? n.data.config - : n.data, - })), - edges: edges.map((e) => ({ - id: e.id, - source: e.source, - target: e.target, - sourceHandle: e.sourceHandle || undefined, - targetHandle: e.targetHandle || undefined, + + n.type !== 'note') + .map((n) => ({ + id: n.id, + type: n.type as 'start' | 'end' | 'agent' | 'state' | 'code', + title: n.data.title || n.data.label || n.type, + position: n.position, + data: + n.type === 'code' + ? serializeCodeConfig(n.data.config) + : n.type === 'agent' + ? n.data.config + : n.data, })), - }} - /> - - + edges: edges.map((e) => ({ + id: e.id, + source: e.source, + target: e.target, + sourceHandle: e.sourceHandle || undefined, + targetHandle: e.targetHandle || undefined, + })), + }} + /> +
({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +vi.mock('react-redux', () => ({ + useSelector: () => null, + useDispatch: () => vi.fn(), +})); + +vi.mock('./WorkflowRunArtifacts', () => ({ default: () => null })); + +import { ExecutionDetails, RunArtifactsSection } from './WorkflowPreview'; +import type { WorkflowNode } from '../types/workflow'; + +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + +const nodes = [ + { + id: 'n1', + type: 'agent', + title: 'Summarise', + position: { x: 0, y: 0 }, + data: {}, + }, +] as unknown as WorkflowNode[]; + +const steps = [ + { + nodeId: 'n1', + nodeType: 'agent', + nodeTitle: 'Summarise', + status: 'completed', + output: 'Three carriers expire this month.', + stateDelta: { region: 'EU', query: 'ignored' }, + }, +] as never; + +describe('WorkflowPreview execution sections', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + }); + + const renderDetails = async (isOpen = true) => { + await act(async () => { + root.render( + undefined} + />, + ); + }); + }; + + // The answer's step-row recipe (AnswerFlow): ghost sm at ml-3.5, a muted + // icon, muted text, and a chevron that turns while open. + it('draws the Execution details toggle as an answer step row', async () => { + await renderDetails(); + const toggle = container.querySelector('button')!; + expect(toggle.dataset.variant).toBe('ghost'); + expect(toggle.dataset.size).toBe('sm'); + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + expect(toggle.className).toContain('ml-3.5'); + const icons = toggle.querySelectorAll('svg'); + expect(icons[0].getAttribute('class')).toContain('text-muted-foreground'); + expect(icons[icons.length - 1].getAttribute('class')).toContain( + 'rotate-180', + ); + expect(toggle.querySelector('p')).toBeNull(); + }); + + it('draws each step as a subtle panel with its output in a filled well', async () => { + await renderDetails(); + const step = container.querySelector( + '[data-slot="card"][data-variant="subtle"]', + )!; + expect(step.dataset.padding).toBe('sm'); + const well = step.querySelector( + '[data-slot="card"][data-variant="filled"]', + )!; + expect(well.dataset.padding).toBe('sm'); + expect(well.textContent).toContain('Three carriers expire this month.'); + // No hand-rolled muted boxes: a muted box in a muted box disappears. + expect( + container.querySelector( + 'div.bg-muted:not([data-slot="card"]), span.bg-muted', + ), + ).toBeNull(); + }); + + it('shows state changes as mono neutral badges, without query', async () => { + await renderDetails(); + const badges = Array.from( + container.querySelectorAll('[data-slot="badge"]'), + ); + expect(badges).toHaveLength(1); + expect(badges[0].dataset.variant).toBe('neutral'); + expect(badges[0].className).toContain('font-mono'); + expect(badges[0].textContent).toContain('region'); + expect(badges[0].textContent).toContain('EU'); + }); + + it('draws the Artifacts toggle as the same step row', async () => { + await act(async () => { + root.render( + undefined} + />, + ); + }); + const toggle = container.querySelector('button')!; + expect(toggle.dataset.variant).toBe('ghost'); + expect(toggle.dataset.size).toBe('sm'); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + expect(toggle.className).toContain('ml-3.5'); + expect(toggle.querySelector('p')).toBeNull(); + }); +}); diff --git a/frontend/src/agents/workflow/WorkflowPreview.tsx b/frontend/src/agents/workflow/WorkflowPreview.tsx index b8fc8429..d15d4664 100644 --- a/frontend/src/agents/workflow/WorkflowPreview.tsx +++ b/frontend/src/agents/workflow/WorkflowPreview.tsx @@ -11,7 +11,6 @@ import { Flag, GitBranch, type LucideIcon, - MessageSquare, Play, StickyNote, Workflow, @@ -21,9 +20,11 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { EmptyState } from '@/components/ui/empty-state'; import { SectionHeader } from '@/components/ui/section-header'; -import { SheetTitle } from '@/components/ui/sheet'; import { Spinner } from '@/components/ui/spinner'; import { MessageScroller, @@ -94,7 +95,64 @@ const NODE_COLORS: Record = { code: 'text-info', }; -function ExecutionDetails({ +/** + * A step row under an answer, like Reasoning in chat (AnswerFlow): a ghost + * button whose muted icon sits on the answer's ml-6 column, then the panel it + * opens in place on that column. + */ +function StepDisclosure({ + icon: Icon, + label, + count, + isOpen, + onToggle, + children, +}: { + icon: LucideIcon; + label: string; + count?: string; + isOpen: boolean; + onToggle: () => void; + children: React.ReactNode; +}) { + return ( +
+ +
+
{children}
+
+
+ ); +} + +export function ExecutionDetails({ steps, nodes, isOpen, @@ -122,153 +180,121 @@ function ExecutionDetails({ }; return ( -
-
-
- -
- -
-
-
-
- {completedSteps.map((step, stepIndex) => { - const node = nodes.find((n) => n.id === step.nodeId); - const displayName = - node?.title || node?.data?.title || step.nodeTitle; - const StepIcon = NODE_ICONS[step.nodeType] || Circle; - const stateVars = step.stateDelta - ? Object.entries(step.stateDelta).filter( - ([key]) => !['query', 'chat_history'].includes(key), - ) - : []; + +
+ {completedSteps.map((step, stepIndex) => { + const node = nodes.find((n) => n.id === step.nodeId); + const displayName = + node?.title || node?.data?.title || step.nodeTitle; + const StepIcon = NODE_ICONS[step.nodeType] || Circle; + const stateVars = step.stateDelta + ? Object.entries(step.stateDelta).filter( + ([key]) => !['query', 'chat_history'].includes(key), + ) + : []; - const truncateText = (text: string, maxLength: number) => { - if (text.length <= maxLength) return text; - return text.slice(0, maxLength) + '...'; - }; - const hasOutput = - step.output !== undefined && - step.output !== null && - formatValue(step.output) !== ''; - const formattedOutput = hasOutput ? formatValue(step.output) : ''; + const truncateText = (text: string, maxLength: number) => { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength) + '...'; + }; + const hasOutput = + step.output !== undefined && + step.output !== null && + formatValue(step.output) !== ''; + const formattedOutput = hasOutput ? formatValue(step.output) : ''; - return ( -
{ - if (el && stepRefs) stepRefs.current.set(step.nodeId, el); - }} - className="bg-muted rounded-xl p-3" - > -
- - {stepIndex + 1}. - -
- -
- - {displayName} - -
- {step.status === 'completed' && ( - - )} - {step.status === 'failed' && ( - - )} -
-
- {(hasOutput || step.error || stateVars.length > 0) && ( -
- {hasOutput && ( -
- - {t('agents.workflow.preview.outputLabel')}{' '} - - - {truncateText(formattedOutput, 300)} - -
- )} - {step.error && ( - - - - - {t('agents.workflow.preview.errorLabel')}{' '} - - - {step.error} - - - - )} - {stateVars.length > 0 && ( -
- {stateVars.map(([key, value]) => ( - - - {key}: - - - {truncateText(formatValue(value), 50)} - - - ))} -
- )} -
+ // A step is a place you read inside: a subtle panel on the + // drawer's background, with its output in a filled well. + return ( + { + if (el && stepRefs) stepRefs.current.set(step.nodeId, el); + }} + variant="subtle" + padding="sm" + > +
+ + {stepIndex + 1}. + + + + {displayName} + +
+ {step.status === 'completed' && ( + + )} + {step.status === 'failed' && ( + )}
- ); - })} -
-
+
+ {hasOutput && ( + +

+ + {t('agents.workflow.preview.outputLabel')}{' '} + + + {truncateText(formattedOutput, 300)} + +

+
+ )} + {step.error && ( + + + + + {t('agents.workflow.preview.errorLabel')}{' '} + + + {step.error} + + + + )} + {stateVars.length > 0 && ( +
+ {stateVars.map(([key, value]) => ( + // eslint-disable-next-line shadcn/no-restyle -- state keys and values are serialised by the app, so the chip is set in mono + + {key}: + + {truncateText(formatValue(value), 50)} + + + ))} +
+ )} + + ); + })}
-
+ ); } -function RunArtifactsSection({ +export function RunArtifactsSection({ workflowRunId, isOpen, onToggle, @@ -281,47 +307,21 @@ function RunArtifactsSection({ }) { const { t } = useTranslation(); return ( -
-
-
- -
- -
-
-
-
- {isOpen && ( - - )} -
-
-
+ ); } @@ -616,28 +616,7 @@ export default function WorkflowPreview({ queries.length > 0 ? queries[queries.length - 1].executionSteps || [] : []; return ( -
-
-
-
- -
-
- {t('agents.form.sections.preview')} -

- {workflowData.name} - {workflowData.description && ` - ${workflowData.description}`} -

-
-
- {status === 'loading' && ( - - - {t('agents.schedules.status.running')} - - )} -
- +
@@ -662,13 +641,12 @@ export default function WorkflowPreview({
{queries.length === 0 ? ( -
-
- -
-

- {t('agents.workflow.preview.emptyTitle')} -

+
+
) : ( @@ -788,11 +766,12 @@ export default function WorkflowPreview({ )}
-
+
{sendBlockedMessage && ( -

- {t(sendBlockedMessage)} -

+ + + {t(sendBlockedMessage)} + )} handleQuestionSubmission(text)} diff --git a/frontend/src/design/DesignSystem.tsx b/frontend/src/design/DesignSystem.tsx index 7b936af2..44315656 100644 --- a/frontend/src/design/DesignSystem.tsx +++ b/frontend/src/design/DesignSystem.tsx @@ -173,8 +173,46 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { TimePicker } from '@/components/ui/time-picker'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import AgentPreviewSheet from '../agents/components/AgentPreviewSheet'; +import type { WorkflowNode } from '../agents/types/workflow'; +import { + ExecutionDetails, + RunArtifactsSection, +} from '../agents/workflow/WorkflowPreview'; +import type { WorkflowExecutionStep } from '../agents/workflow/workflowPreviewSlice'; import { useDarkTheme } from '../hooks'; +// Sample run for the Agent preview drawer example. +const DEMO_WORKFLOW_NODES = [ + { id: 'd1', type: 'agent', title: 'Find expiring certificates' }, + { id: 'd2', type: 'state', title: 'Set region' }, + { id: 'd3', type: 'agent', title: 'Draft reminders' }, +] as unknown as WorkflowNode[]; + +const DEMO_WORKFLOW_STEPS = [ + { + nodeId: 'd1', + nodeType: 'agent', + nodeTitle: 'Find expiring certificates', + status: 'completed', + output: 'Three carriers have certificates expiring before 31 October.', + }, + { + nodeId: 'd2', + nodeType: 'state', + nodeTitle: 'Set region', + status: 'completed', + stateDelta: { region: 'EU', carriers: ['Halvorsen', 'Nordline'] }, + }, + { + nodeId: 'd3', + nodeType: 'agent', + nodeTitle: 'Draft reminders', + status: 'failed', + error: 'The email tool timed out after 30 s.', + }, +] as unknown as WorkflowExecutionStep[]; + /** * Dev-only style guide, served at /design. It renders every component in * src/components/ui with the variants DESIGN.md documents as the defaults, @@ -422,6 +460,8 @@ export default function DesignSystem() { const [agentType, setAgentType] = useState<'classic' | 'workflow'>('classic'); const [toolOn, setToolOn] = useState(false); const [sectionOpen, setSectionOpen] = useState(false); + const [agentPreviewOpen, setAgentPreviewOpen] = useState(false); + const [demoStepsOpen, setDemoStepsOpen] = useState(true); const [scopes, setScopes] = useState(['agents:read']); const [tokenLimit, setTokenLimit] = useState(true); const [saving, setSaving] = useState(false); @@ -2621,6 +2661,34 @@ export default function DesignSystem() { + + +
+ setDemoStepsOpen(!demoStepsOpen)} + /> + undefined} + /> +
+