ai-design-system 0.1.75 → 0.1.78

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.
Files changed (56) hide show
  1. package/components/ai-elements/ExpoAppPreview/ExpoAppPreview.tsx +112 -0
  2. package/components/ai-elements/ExpoAppPreview/index.ts +2 -0
  3. package/components/ai-elements/ExpoAppPreview/interfaces.ts +58 -0
  4. package/components/ai-elements/checkpoint.tsx +86 -0
  5. package/components/ai-elements/edge.tsx +41 -5
  6. package/components/blocks/AIConversation/AIConversation.stories.tsx +129 -0
  7. package/components/blocks/AIConversation/AIConversation.tsx +135 -77
  8. package/components/blocks/PreviewCanvas/index.ts +2 -0
  9. package/components/blocks/PreviewCanvas/interfaces.ts +21 -0
  10. package/components/blocks/PreviewCanvas/preview-layout.ts +112 -0
  11. package/components/blocks/WorkflowCanvas/WorkflowCanvas.stories.tsx +60 -0
  12. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +12 -1
  13. package/components/blocks/WorkflowCanvas/index.ts +2 -2
  14. package/components/blocks/index.ts +5 -0
  15. package/components/composites/AdjustableLayout/AdjustableLayout.tsx +8 -10
  16. package/components/composites/AgentIndicator/AgentIndicator.tsx +1 -0
  17. package/components/composites/DevicePreviewNode/DevicePreviewNode.stories.tsx +214 -0
  18. package/components/composites/DevicePreviewNode/DevicePreviewNode.tsx +250 -0
  19. package/components/composites/DevicePreviewNode/ExpoAppPreview.stories.tsx +228 -0
  20. package/components/composites/DevicePreviewNode/index.ts +12 -0
  21. package/components/composites/DevicePreviewNode/interfaces.ts +98 -0
  22. package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.stories.tsx +81 -0
  23. package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.tsx +127 -0
  24. package/components/composites/DevicePreviewToolbar/index.ts +6 -0
  25. package/components/composites/DevicePreviewToolbar/interfaces.ts +38 -0
  26. package/components/composites/MediaPreview/MediaPreview.stories.tsx +60 -0
  27. package/components/composites/MediaPreview/MediaPreview.tsx +186 -0
  28. package/components/composites/MediaPreview/index.ts +9 -0
  29. package/components/composites/PromptInput/PromptInput.tsx +17 -0
  30. package/components/composites/SpecialistMessage/SpecialistMessage.tsx +37 -1
  31. package/components/composites/StreamingMarkdown.tsx +11 -5
  32. package/components/composites/SystemMessage/SystemMessage.tsx +49 -1
  33. package/components/composites/index.ts +11 -1
  34. package/components/features/ChatPanel/ChatPanel.stories.tsx +2 -0
  35. package/components/features/ChatPanel/ChatPanel.tsx +21 -0
  36. package/components/features/ChatPanel/useChatPanel.d.ts +3 -0
  37. package/components/features/ChatPanel/useChatPanel.mock.ts +12 -0
  38. package/components/features/NodeEditor/NodeEditor.stories.tsx +409 -0
  39. package/components/features/NodeEditor/NodeEditor.tsx +5 -0
  40. package/components/features/NodeEditor/PREVIEW_MANIFEST_MENTAL_MODEL.md +197 -0
  41. package/components/features/NodeEditor/bookgeek.preview.json +311 -0
  42. package/components/features/PageLayout/PageLayout.stories.tsx +2 -2
  43. package/components/features/TextEditor/TextEditor.tsx +25 -23
  44. package/components/features/TextEditor/index.ts +3 -0
  45. package/components/features/TextEditor/useTextEditor.mock.ts +1 -14
  46. package/components/index.ts +15 -3
  47. package/dist/index.cjs +963 -132
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.css +135 -0
  50. package/dist/index.d.ts +295 -78
  51. package/dist/index.js +960 -136
  52. package/dist/index.js.map +1 -1
  53. package/{components/blocks/WorkflowCanvas → lib/workflow}/interfaces.ts +4 -1
  54. package/lib/workflow/layout-engine.ts +105 -0
  55. package/package.json +1 -1
  56. package/components/blocks/WorkflowCanvas/layout-engine.ts +0 -77
@@ -6,6 +6,11 @@ import {
6
6
  ConversationScrollButton,
7
7
  type ConversationProps,
8
8
  } from "@/components/ai-elements/conversation"
9
+ import {
10
+ Checkpoint,
11
+ CheckpointIcon,
12
+ CheckpointTrigger,
13
+ } from "@/components/ai-elements/checkpoint"
9
14
  import type { ToolCall, SubAgent } from "@/components/composites"
10
15
  import { UserMessage } from "@/components/composites/UserMessage"
11
16
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
@@ -29,6 +34,21 @@ type AIMessageBlock =
29
34
  | { type: 'toolCall'; id: string; toolCall: ToolCall }
30
35
  | { type: 'subAgent'; id: string; subAgent: SubAgent }
31
36
 
37
+ // SpecialistMessage's `subAgents` prop expects string-only `output`, while
38
+ // SubAgent allows structured output — normalize recursively when nesting.
39
+ function toNestedSubAgents(
40
+ agents?: SubAgent[]
41
+ ): { id: string; subAgentName: string; status: SubAgent['status']; input?: string | Record<string, unknown>; output?: string; subAgents?: ReturnType<typeof toNestedSubAgents> }[] | undefined {
42
+ return agents?.map((a) => ({
43
+ id: a.id,
44
+ subAgentName: a.subAgentName,
45
+ status: a.status,
46
+ input: a.input,
47
+ output: typeof a.output === 'string' ? a.output : a.output ? JSON.stringify(a.output) : undefined,
48
+ subAgents: toNestedSubAgents(a.subAgents),
49
+ }));
50
+ }
51
+
32
52
  interface AIMessage {
33
53
  id: string;
34
54
  type: 'human' | 'ai';
@@ -40,6 +60,7 @@ interface AIMessage {
40
60
  subAgents?: SubAgent[];
41
61
  isLoading?: boolean;
42
62
  blocks?: AIMessageBlock[];
63
+ checkpointId?: string;
43
64
  }
44
65
 
45
66
  export interface AIConversationProps
@@ -77,6 +98,10 @@ export interface AIConversationProps
77
98
  * Receives the raw content string and returns a ReactNode.
78
99
  */
79
100
  renderSystemMessage?: (content: string) => React.ReactNode
101
+ /**
102
+ * Callback fired when user restores a conversation checkpoint
103
+ */
104
+ onRestoreCheckpoint?: (messageId: string, checkpointId: string) => void
80
105
  }
81
106
 
82
107
  /**
@@ -91,6 +116,7 @@ export const AIConversation = React.memo<AIConversationProps>(
91
116
  emptyState,
92
117
  onToolAction,
93
118
  renderSystemMessage,
119
+ onRestoreCheckpoint,
94
120
  ...conversationProps
95
121
  }) => {
96
122
  const isEmpty = React.useMemo(
@@ -209,6 +235,9 @@ export const AIConversation = React.memo<AIConversationProps>(
209
235
  }
210
236
  currentGroup.blocks = updatedBlocks;
211
237
  }
238
+ if (msg.checkpointId) {
239
+ currentGroup.checkpointId = msg.checkpointId;
240
+ }
212
241
  currentGroup.isLoading = msg.isLoading;
213
242
  } else {
214
243
  currentGroup = {
@@ -216,6 +245,7 @@ export const AIConversation = React.memo<AIConversationProps>(
216
245
  toolCalls: msg.toolCalls ? [...msg.toolCalls] : undefined,
217
246
  subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
218
247
  blocks: msg.blocks ? [...msg.blocks] : undefined,
248
+ checkpointId: msg.checkpointId,
219
249
  };
220
250
  result.push(currentGroup);
221
251
  }
@@ -269,7 +299,7 @@ export const AIConversation = React.memo<AIConversationProps>(
269
299
  )
270
300
  }
271
301
 
272
- if (message.role === "orchestrator") {
302
+ if (message.role === "orchestrator" || message.role === "assistant" || (!message.role && message.type === "ai")) {
273
303
  // Extract sub-agents from the message
274
304
  const subAgents = message.subAgents || []
275
305
 
@@ -296,39 +326,66 @@ export const AIConversation = React.memo<AIConversationProps>(
296
326
  const isStreaming = message.isLoading || reasoningCalls.some((tc) => tc.status === "pending") || subAgents.some(sa => sa.status === "active") || directToolCalls.some(tc => tc.status === "pending")
297
327
 
298
328
  return (
299
- <OrchestratorMessage
300
- key={message.id}
301
- message={{
302
- id: message.id,
303
- content: displayContentStr,
304
- avatarSrc: message.avatarSrc,
305
- avatarName: message.avatarName,
306
- isLoading: message.isLoading && !hasReasoning,
307
- }}
308
- showAvatar={showAvatars && hasDisplayContent}
309
- >
310
- {/* Render reasoning-section for hidden tool results and subagents */}
311
- {hasReasoning && (
312
- <ReasoningDisplay
313
- content={reasoningText}
314
- items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
315
- isStreaming={isStreaming}
316
- onToolAction={onToolAction}
317
- defaultOpen={isStreaming || index === groupedMessages.length - 1}
318
- >
319
- {message.blocks && message.blocks.length > 0 ? (
320
- message.blocks.map((block, i) => {
321
- if (block.type === 'text') {
322
- return block.text && block.text.trim() ? (
323
- <div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
324
- {block.text}
325
- </div>
326
- ) : null;
327
- } else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
328
- return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
329
- } else if (block.type === 'subAgent' && block.subAgent) {
330
- const subAgent = block.subAgent;
331
- return (
329
+ <React.Fragment key={message.id}>
330
+ <OrchestratorMessage
331
+ message={{
332
+ id: message.id,
333
+ content: displayContentStr,
334
+ avatarSrc: message.avatarSrc,
335
+ avatarName: message.avatarName,
336
+ isLoading: message.isLoading && !hasReasoning,
337
+ }}
338
+ showAvatar={showAvatars && hasDisplayContent}
339
+ >
340
+ {/* Render reasoning-section for hidden tool results and subagents */}
341
+ {hasReasoning && (
342
+ <ReasoningDisplay
343
+ content={reasoningText}
344
+ items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
345
+ isStreaming={isStreaming}
346
+ onToolAction={onToolAction}
347
+ defaultOpen={isStreaming || index === groupedMessages.length - 1}
348
+ >
349
+ {message.blocks && message.blocks.length > 0 ? (
350
+ message.blocks.map((block, i) => {
351
+ if (block.type === 'text') {
352
+ return block.text && block.text.trim() ? (
353
+ <div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
354
+ {block.text}
355
+ </div>
356
+ ) : null;
357
+ } else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
358
+ return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
359
+ } else if (block.type === 'subAgent' && block.subAgent) {
360
+ const subAgent = block.subAgent;
361
+ return (
362
+ <SpecialistMessage
363
+ key={subAgent.id}
364
+ message={{
365
+ id: subAgent.id,
366
+ name: subAgent.subAgentName,
367
+ description: undefined,
368
+ input: subAgent.input,
369
+ content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
370
+ status: subAgent.status,
371
+ toolCalls: [],
372
+ subAgents: toNestedSubAgents(subAgent.subAgents),
373
+ }}
374
+ isNested={true}
375
+ />
376
+ );
377
+ }
378
+ return null;
379
+ })
380
+ ) : (
381
+ <>
382
+ {/* Render direct tool calls fallback */}
383
+ {directToolCalls.map((tc) => (
384
+ <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
385
+ ))}
386
+
387
+ {/* Render specialist sub-agents fallback */}
388
+ {subAgents.map((subAgent) => (
332
389
  <SpecialistMessage
333
390
  key={subAgent.id}
334
391
  message={{
@@ -339,60 +396,61 @@ export const AIConversation = React.memo<AIConversationProps>(
339
396
  content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
340
397
  status: subAgent.status,
341
398
  toolCalls: [],
399
+ subAgents: toNestedSubAgents(subAgent.subAgents),
342
400
  }}
343
401
  isNested={true}
344
402
  />
345
- );
346
- }
347
- return null;
348
- })
349
- ) : (
350
- <>
351
- {/* Render direct tool calls fallback */}
352
- {directToolCalls.map((tc) => (
353
- <ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
354
- ))}
355
-
356
- {/* Render specialist sub-agents fallback */}
357
- {subAgents.map((subAgent) => (
358
- <SpecialistMessage
359
- key={subAgent.id}
360
- message={{
361
- id: subAgent.id,
362
- name: subAgent.subAgentName,
363
- description: undefined,
364
- input: subAgent.input,
365
- content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
366
- status: subAgent.status,
367
- toolCalls: [],
368
- }}
369
- isNested={true}
370
- />
371
- ))}
372
- </>
373
- )}
374
- </ReasoningDisplay>
403
+ ))}
404
+ </>
405
+ )}
406
+ </ReasoningDisplay>
407
+ )}
408
+ </OrchestratorMessage>
409
+ {message.checkpointId && onRestoreCheckpoint && !message.isLoading && !hasReasoning && (
410
+ <Checkpoint>
411
+ <CheckpointIcon />
412
+ <CheckpointTrigger
413
+ onClick={() =>
414
+ onRestoreCheckpoint(message.id, message.checkpointId!)
415
+ }
416
+ >
417
+ Restore checkpoint
418
+ </CheckpointTrigger>
419
+ </Checkpoint>
375
420
  )}
376
- </OrchestratorMessage>
421
+ </React.Fragment>
377
422
  )
378
423
  }
379
424
 
380
425
  // Default to specialist message for any other AI messages
381
426
  return (
382
- <SpecialistMessage
383
- key={message.id}
384
- message={{
385
- id: message.id,
386
- name: message.avatarName || "Agent",
387
- content: contentStr,
388
- toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
389
- status: "completed",
390
- }}
391
- isNested={false}
392
- />
427
+ <React.Fragment key={message.id}>
428
+ <SpecialistMessage
429
+ message={{
430
+ id: message.id,
431
+ name: message.avatarName || "Agent",
432
+ content: contentStr,
433
+ toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
434
+ status: "completed",
435
+ }}
436
+ isNested={false}
437
+ />
438
+ {message.checkpointId && onRestoreCheckpoint && !message.isLoading && (
439
+ <Checkpoint>
440
+ <CheckpointIcon />
441
+ <CheckpointTrigger
442
+ onClick={() =>
443
+ onRestoreCheckpoint(message.id, message.checkpointId!)
444
+ }
445
+ >
446
+ Restore checkpoint
447
+ </CheckpointTrigger>
448
+ </Checkpoint>
449
+ )}
450
+ </React.Fragment>
393
451
  )
394
452
  }),
395
- [groupedMessages, showAvatars, onToolAction]
453
+ [groupedMessages, showAvatars, onToolAction, onRestoreCheckpoint]
396
454
  )
397
455
 
398
456
  return (
@@ -0,0 +1,2 @@
1
+ export * from './interfaces';
2
+ export * from './preview-layout';
@@ -0,0 +1,21 @@
1
+ export interface AppRoute {
2
+ id: string;
3
+ path: string;
4
+ label: string;
5
+ isInitial?: boolean;
6
+ }
7
+
8
+ export interface AppNavLink {
9
+ fromRouteId: string;
10
+ toRouteId: string;
11
+ label?: string;
12
+ sourceHandle?: string;
13
+ }
14
+
15
+ export interface AppRouteManifest {
16
+ appId: string;
17
+ routes: AppRoute[];
18
+ links: AppNavLink[];
19
+ }
20
+
21
+ export type PreviewCanvasViewMode = 'play' | 'single' | 'interactive' | 'grid';
@@ -0,0 +1,112 @@
1
+ import { getLayoutedElements } from '@/lib/workflow/layout-engine';
2
+ import { DEVICE_PRESETS } from '@/components/composites/DevicePreviewNode/interfaces';
3
+ import type { WorkflowEdge, WorkflowNode } from '@/lib/workflow/interfaces';
4
+ import type { AppRouteManifest } from './interfaces';
5
+
6
+ export interface PreviewLayoutOptions {
7
+ presetId?: string;
8
+ loading?: boolean;
9
+ isEmpty?: boolean;
10
+ error?: string | null;
11
+ interactive?: boolean;
12
+ hideControls?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Compute the canvas-space pixel size of a DevicePreviewNode for a given preset.
17
+ * Matches the inline style logic in DevicePreviewNode.tsx:
18
+ * width = preset.width + (hideControls ? 16 : 32)
19
+ * height = preset.height + (hideControls ? 16 : 88)
20
+ */
21
+ export function deviceNodeSize(
22
+ presetId: string,
23
+ hideControls = true,
24
+ scale = 0.45,
25
+ ): { width: number; height: number } {
26
+ const preset =
27
+ DEVICE_PRESETS.find((p) => p.id === presetId) ??
28
+ DEVICE_PRESETS.find((p) => p.id === 'iphone-16-pro') ??
29
+ DEVICE_PRESETS[0];
30
+
31
+ const baseWidth = preset.isResponsive ? 420 : preset.width;
32
+ const baseHeight = preset.isResponsive ? 780 : preset.height;
33
+
34
+ const effectiveScale = hideControls ? scale : 1.0;
35
+ return {
36
+ width: Math.round(baseWidth * effectiveScale) + (hideControls ? 12 : 32),
37
+ height: Math.round(baseHeight * effectiveScale) + (hideControls ? 12 : 88),
38
+ };
39
+ }
40
+
41
+ /** ELK canvas-space size of a transition/diamond condition node. */
42
+ export const TRANSITION_NODE_SIZE = { width: 180, height: 52 };
43
+
44
+ /**
45
+ * Build unpositioned nodes + edges from a manifest, then run them through the
46
+ * same ELK `layered` engine the builder uses — but with correct per-node
47
+ * dimensions for `devicePreview` vs `transition` nodes.
48
+ *
49
+ * Direction is RIGHT (left-to-right) to match the Bookgeek reference UX.
50
+ */
51
+ export async function routesToPreviewCanvas(
52
+ manifest: AppRouteManifest,
53
+ src: string,
54
+ options: PreviewLayoutOptions = {},
55
+ ): Promise<{ nodes: WorkflowNode[]; edges: WorkflowEdge[] }> {
56
+ const {
57
+ presetId = 'iphone-16-pro',
58
+ loading = false,
59
+ isEmpty = false,
60
+ error = null,
61
+ interactive = false,
62
+ hideControls = true,
63
+ } = options;
64
+
65
+ const devSize = deviceNodeSize(presetId, hideControls);
66
+
67
+ const routes = manifest.routes || [];
68
+ const links = manifest.links || [];
69
+
70
+ // Build unlaid-out nodes (positions will be overwritten by ELK)
71
+ const nodes: WorkflowNode[] = routes.map((r) => ({
72
+ id: `preview-${r.id}`,
73
+ type: 'devicePreview',
74
+ position: { x: 0, y: 0 }, // placeholder – ELK will override
75
+ data: {
76
+ type: 'devicePreview',
77
+ src,
78
+ route: r.path,
79
+ label: r.label,
80
+ presetId,
81
+ isInitial: r.isInitial,
82
+ interactive,
83
+ hideControls,
84
+ loading,
85
+ isEmpty,
86
+ error,
87
+ },
88
+ }));
89
+
90
+ const edges: WorkflowEdge[] = links.map((link, idx) => ({
91
+ id: `e-${link.fromRouteId}-${link.toRouteId}-${idx}`,
92
+ source: `preview-${link.fromRouteId}`,
93
+ target: `preview-${link.toRouteId}`,
94
+ // preserve explicit handle IDs so action-port handles route correctly
95
+ sourceHandle: link.sourceHandle ?? null,
96
+ label: link.label,
97
+ type: 'straight',
98
+ }));
99
+
100
+ return getLayoutedElements(nodes, edges, {
101
+ direction: 'RIGHT',
102
+ // generous horizontal gap so device bezels don't crowd each other
103
+ nodeSpacingX: devSize.width * 0.4,
104
+ // generous vertical gap so branching rows have breathing room
105
+ nodeSpacingY: devSize.height * 0.3,
106
+ nodeSizeResolver: (node) => {
107
+ if (node.type === 'devicePreview') return devSize;
108
+ if (node.type === 'transition') return TRANSITION_NODE_SIZE;
109
+ return null; // fall back to ELK defaults (180×52)
110
+ },
111
+ });
112
+ }
@@ -3,6 +3,7 @@ import { useState } from "react";
3
3
  import type { NodeChange, EdgeChange, Connection } from "@xyflow/react";
4
4
  import { applyNodeChanges, applyEdgeChanges } from "@xyflow/react";
5
5
  import { WorkflowCanvas, type WorkflowNode, type WorkflowEdge } from "./WorkflowCanvas";
6
+ import { DevicePreviewToolbar } from "@/components/composites/DevicePreviewToolbar";
6
7
  import "@xyflow/react/dist/style.css";
7
8
 
8
9
  /**
@@ -277,3 +278,62 @@ export const Empty: Story = {
277
278
  );
278
279
  },
279
280
  };
281
+
282
+ /**
283
+ * Device Preview mode — `devicePreview` nodes + the DevicePreviewToolbar in
284
+ * the topCenter panel slot (the config the BuilderPage 'Preview' tab uses).
285
+ */
286
+ export const DevicePreviewMode: Story = {
287
+ render: () => {
288
+ const [nodes, setNodes] = useState<WorkflowNode[]>(() => {
289
+ const routes = ["/login", "/signup", "/dashboard", "/settings"];
290
+ return routes.map((route, i) => ({
291
+ id: `preview-${i}`,
292
+ type: "devicePreview",
293
+ position: { x: (i % 2) * 500, y: Math.floor(i / 2) * 760 },
294
+ data: {
295
+ type: "devicePreview",
296
+ src: "about:blank",
297
+ label: `demo-app · ${route}`,
298
+ route,
299
+ presetId: "iphone-16-pro",
300
+ showRouteBadge: true,
301
+ },
302
+ }));
303
+ });
304
+ const [viewMode, setViewMode] = useState<"play" | "single" | "interactive" | "grid">("grid");
305
+ const [activeRoute, setActiveRoute] = useState("/login");
306
+ const [presetId, setPresetId] = useState("iphone-16-pro");
307
+
308
+ const onNodesChange = (changes: NodeChange[]) => {
309
+ setNodes((nds) => applyNodeChanges(changes, nds) as WorkflowNode[]);
310
+ };
311
+
312
+ return (
313
+ <div style={{ width: "100vw", height: "100vh" }}>
314
+ <WorkflowCanvas
315
+ nodes={nodes}
316
+ edges={[]}
317
+ onNodesChange={onNodesChange}
318
+ interactive
319
+ topCenter={
320
+ <DevicePreviewToolbar
321
+ viewMode={viewMode}
322
+ onViewModeChange={setViewMode}
323
+ routes={[
324
+ { id: "/login", label: "Login" },
325
+ { id: "/signup", label: "Signup" },
326
+ { id: "/dashboard", label: "Dashboard" },
327
+ { id: "/settings", label: "Settings" },
328
+ ]}
329
+ activeRoute={activeRoute}
330
+ onRouteChange={setActiveRoute}
331
+ devicePresetId={presetId}
332
+ onDevicePresetChange={setPresetId}
333
+ />
334
+ }
335
+ />
336
+ </div>
337
+ );
338
+ },
339
+ };
@@ -17,7 +17,8 @@ import { StateNode } from "@/components/composites/StateNode";
17
17
  import { TransitionNode } from "@/components/composites/TransitionNode";
18
18
  import { TriggerNode } from "@/components/composites/TriggerNode";
19
19
  import { SpatialContainerNode } from "@/components/composites/SpatialContainerNode";
20
- import type { WorkflowCanvasProps, WorkflowEdge } from "./interfaces";
20
+ import { DevicePreviewNode } from "@/components/composites/DevicePreviewNode";
21
+ import type { WorkflowCanvasProps, WorkflowEdge } from "@/lib/workflow/interfaces";
21
22
  import "@xyflow/react/dist/style.css";
22
23
 
23
24
  const edgeTypes = {
@@ -31,6 +32,7 @@ const nodeTypes = {
31
32
  transition: TransitionNode,
32
33
  trigger: TriggerNode,
33
34
  spatialContainer: SpatialContainerNode,
35
+ devicePreview: DevicePreviewNode,
34
36
  };
35
37
 
36
38
  function WorkflowCanvasInner({
@@ -47,6 +49,7 @@ function WorkflowCanvasInner({
47
49
  interactive = false,
48
50
  topLeft,
49
51
  topRight,
52
+ topCenter,
50
53
  className,
51
54
  }: WorkflowCanvasProps) {
52
55
  const { fitView } = useReactFlow();
@@ -117,6 +120,14 @@ function WorkflowCanvasInner({
117
120
  {topRight}
118
121
  </Panel>
119
122
  )}
123
+ {topCenter && (
124
+ <Panel
125
+ className="pointer-events-auto border-none bg-transparent p-0"
126
+ position="top-center"
127
+ >
128
+ {topCenter}
129
+ </Panel>
130
+ )}
120
131
  <Panel
121
132
  className="workflow-controls-panel border-none bg-transparent p-0"
122
133
  position="bottom-left"
@@ -1,3 +1,3 @@
1
1
  export { WorkflowCanvas } from "./WorkflowCanvas";
2
- export type { WorkflowCanvasProps, WorkflowNode, WorkflowEdge, WorkflowNodeData } from "./interfaces";
3
- export { getLayoutedElements } from "./layout-engine";
2
+ export type { WorkflowCanvasProps, WorkflowNode, WorkflowEdge, WorkflowNodeData } from "@/lib/workflow/interfaces";
3
+ export { getLayoutedElements } from "@/lib/workflow/layout-engine";
@@ -13,6 +13,11 @@ export type { FileChangeQueueProps } from './FileChangeQueue'
13
13
  // Blocks
14
14
  export * from './SectionLayout'
15
15
 
16
+ // ExpoAppPreview lives in ai-elements (it's a generic sandboxed iframe
17
+ // container); re-exported here for backward compatibility.
18
+ export { ExpoAppPreview } from '@/components/ai-elements/ExpoAppPreview'
19
+ export type { ExpoAppPreviewProps } from '@/components/ai-elements/ExpoAppPreview'
20
+
16
21
  export { WorkflowCanvas, getLayoutedElements } from './WorkflowCanvas'
17
22
  export type { WorkflowCanvasProps, WorkflowNode, WorkflowEdge, WorkflowNodeData } from './WorkflowCanvas'
18
23
 
@@ -60,21 +60,20 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
60
60
 
61
61
  // Compute default sizes (server-safe — no localStorage access)
62
62
  const defaultSizes = React.useMemo(() => {
63
- const raw = sections.map(section => section.defaultSize ?? (100 / sections.length))
63
+ const raw = sections.map((section) => section.defaultSize ?? (100 / sections.length))
64
64
  const total = raw.reduce((sum, size) => sum + size, 0)
65
- return raw.map(size => (size / total) * 100)
65
+ return raw.map((size) => (size / total) * 100)
66
66
  }, [sections])
67
67
 
68
68
  const [sizes, setSizes] = React.useState<number[]>(defaultSizes)
69
69
 
70
70
  // Sync sizes if the number of sections changes (e.g. data loads and new section appears)
71
+ const prevSectionsLengthRef = React.useRef(sections.length)
71
72
  React.useEffect(() => {
72
- setSizes((prevSizes) => {
73
- if (prevSizes.length !== sections.length) {
74
- return defaultSizes;
75
- }
76
- return prevSizes;
77
- });
73
+ if (prevSectionsLengthRef.current !== sections.length) {
74
+ prevSectionsLengthRef.current = sections.length
75
+ setSizes(defaultSizes)
76
+ }
78
77
  }, [sections.length, defaultSizes])
79
78
 
80
79
  // After hydration, overwrite with persisted sizes if available
@@ -90,8 +89,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
90
89
  } catch {
91
90
  // ignore malformed storage
92
91
  }
93
- // eslint-disable-next-line react-hooks/exhaustive-deps
94
- }, [storageKey])
92
+ }, [storageKey, sections.length])
95
93
 
96
94
  const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null)
97
95
  const [startX, setStartX] = React.useState(0)
@@ -17,6 +17,7 @@ export interface SubAgent {
17
17
  input: string | Record<string, unknown>
18
18
  output?: string | Record<string, unknown>
19
19
  status: "pending" | "active" | "completed" | "error" | "running" | "in_progress"
20
+ subAgents?: SubAgent[]
20
21
  }
21
22
 
22
23
  export interface AgentIndicatorProps {