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.
- package/components/ai-elements/ExpoAppPreview/ExpoAppPreview.tsx +112 -0
- package/components/ai-elements/ExpoAppPreview/index.ts +2 -0
- package/components/ai-elements/ExpoAppPreview/interfaces.ts +58 -0
- package/components/ai-elements/checkpoint.tsx +86 -0
- package/components/ai-elements/edge.tsx +41 -5
- package/components/blocks/AIConversation/AIConversation.stories.tsx +129 -0
- package/components/blocks/AIConversation/AIConversation.tsx +135 -77
- package/components/blocks/PreviewCanvas/index.ts +2 -0
- package/components/blocks/PreviewCanvas/interfaces.ts +21 -0
- package/components/blocks/PreviewCanvas/preview-layout.ts +112 -0
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.stories.tsx +60 -0
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +12 -1
- package/components/blocks/WorkflowCanvas/index.ts +2 -2
- package/components/blocks/index.ts +5 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +8 -10
- package/components/composites/AgentIndicator/AgentIndicator.tsx +1 -0
- package/components/composites/DevicePreviewNode/DevicePreviewNode.stories.tsx +214 -0
- package/components/composites/DevicePreviewNode/DevicePreviewNode.tsx +250 -0
- package/components/composites/DevicePreviewNode/ExpoAppPreview.stories.tsx +228 -0
- package/components/composites/DevicePreviewNode/index.ts +12 -0
- package/components/composites/DevicePreviewNode/interfaces.ts +98 -0
- package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.stories.tsx +81 -0
- package/components/composites/DevicePreviewToolbar/DevicePreviewToolbar.tsx +127 -0
- package/components/composites/DevicePreviewToolbar/index.ts +6 -0
- package/components/composites/DevicePreviewToolbar/interfaces.ts +38 -0
- package/components/composites/MediaPreview/MediaPreview.stories.tsx +60 -0
- package/components/composites/MediaPreview/MediaPreview.tsx +186 -0
- package/components/composites/MediaPreview/index.ts +9 -0
- package/components/composites/PromptInput/PromptInput.tsx +17 -0
- package/components/composites/SpecialistMessage/SpecialistMessage.tsx +37 -1
- package/components/composites/StreamingMarkdown.tsx +11 -5
- package/components/composites/SystemMessage/SystemMessage.tsx +49 -1
- package/components/composites/index.ts +11 -1
- package/components/features/ChatPanel/ChatPanel.stories.tsx +2 -0
- package/components/features/ChatPanel/ChatPanel.tsx +21 -0
- package/components/features/ChatPanel/useChatPanel.d.ts +3 -0
- package/components/features/ChatPanel/useChatPanel.mock.ts +12 -0
- package/components/features/NodeEditor/NodeEditor.stories.tsx +409 -0
- package/components/features/NodeEditor/NodeEditor.tsx +5 -0
- package/components/features/NodeEditor/PREVIEW_MANIFEST_MENTAL_MODEL.md +197 -0
- package/components/features/NodeEditor/bookgeek.preview.json +311 -0
- package/components/features/PageLayout/PageLayout.stories.tsx +2 -2
- package/components/features/TextEditor/TextEditor.tsx +25 -23
- package/components/features/TextEditor/index.ts +3 -0
- package/components/features/TextEditor/useTextEditor.mock.ts +1 -14
- package/components/index.ts +15 -3
- package/dist/index.cjs +963 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +135 -0
- package/dist/index.d.ts +295 -78
- package/dist/index.js +960 -136
- package/dist/index.js.map +1 -1
- package/{components/blocks/WorkflowCanvas → lib/workflow}/interfaces.ts +4 -1
- package/lib/workflow/layout-engine.ts +105 -0
- package/package.json +1 -1
- 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
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
</
|
|
421
|
+
</React.Fragment>
|
|
377
422
|
)
|
|
378
423
|
}
|
|
379
424
|
|
|
380
425
|
// Default to specialist message for any other AI messages
|
|
381
426
|
return (
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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,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
|
|
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 "
|
|
3
|
-
export { getLayoutedElements } from "
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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 {
|