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
@@ -0,0 +1,112 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+ import type { ExpoAppPreviewProps } from "./interfaces"
4
+
5
+ /**
6
+ * ExpoAppPreview Block
7
+ *
8
+ * A generic, full-bleed sandboxed iframe container for rendering an Expo /
9
+ * React Native app compiled to web. It is intentionally unopinionated about
10
+ * where the app is served from: `src` may point at a Metro dev server (live
11
+ * reload during development) or a static export (published app).
12
+ *
13
+ * ## Security model
14
+ * The default sandbox retains same-origin (`allow-same-origin`) per the
15
+ * waypoint preview contract. As documented on the props, `allow-scripts`
16
+ * combined with `allow-same-origin` is not a security boundary against the
17
+ * embedded app; pass a sandbox without `allow-same-origin` for untrusted
18
+ * content.
19
+ *
20
+ * ## Host <-> app messaging
21
+ * `onMessage` forwards `message` events whose `event.source` is this iframe's
22
+ * window. It is an optional passthrough of a browser primitive — standalone
23
+ * previews render without it.
24
+ */
25
+ export const ExpoAppPreview = React.memo<ExpoAppPreviewProps>(
26
+ ({
27
+ src,
28
+ title = "App preview",
29
+ sandbox = "allow-scripts allow-same-origin allow-forms allow-popups",
30
+ allow,
31
+ loading = false,
32
+ loadingLabel = "Loading app…",
33
+ isEmpty = false,
34
+ emptyLabel = "No preview available",
35
+ error = null,
36
+ errorLabel = "Preview unavailable",
37
+ onLoad,
38
+ onError,
39
+ onMessage,
40
+ registerIframe,
41
+ className,
42
+ iframeClassName,
43
+ ...props
44
+ }) => {
45
+ const iframeRef = React.useRef<HTMLIFrameElement>(null)
46
+ const onMessageRef = React.useRef(onMessage)
47
+ onMessageRef.current = onMessage
48
+
49
+ React.useEffect(() => {
50
+ if (!registerIframe) return undefined
51
+ registerIframe(iframeRef.current)
52
+ return () => registerIframe(null)
53
+ }, [registerIframe])
54
+
55
+ React.useEffect(() => {
56
+ if (!onMessageRef.current) return undefined
57
+ const handler = (event: MessageEvent): void => {
58
+ if (event.source !== iframeRef.current?.contentWindow) return
59
+ onMessageRef.current?.(event)
60
+ }
61
+ window.addEventListener("message", handler)
62
+ return () => window.removeEventListener("message", handler)
63
+ }, [])
64
+
65
+ return (
66
+ <div
67
+ className={cn(
68
+ "relative h-full w-full min-h-0 overflow-hidden bg-background",
69
+ className,
70
+ )}
71
+ {...props}
72
+ >
73
+ <iframe
74
+ ref={iframeRef}
75
+ src={src}
76
+ title={title}
77
+ sandbox={sandbox}
78
+ allow={allow}
79
+ onLoad={onLoad}
80
+ onError={onError}
81
+ className={cn("h-full w-full border-0", iframeClassName)}
82
+ />
83
+
84
+ {loading && (
85
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-background/80">
86
+ <div
87
+ className="h-8 w-8 animate-spin rounded-full border-2 border-primary/30 border-t-primary"
88
+ role="status"
89
+ aria-label={loadingLabel}
90
+ />
91
+ <span className="text-sm text-muted-foreground">{loadingLabel}</span>
92
+ </div>
93
+ )}
94
+
95
+ {!loading && isEmpty && (
96
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-background/80">
97
+ <span className="text-sm font-medium text-foreground">{emptyLabel}</span>
98
+ </div>
99
+ )}
100
+
101
+ {!loading && !isEmpty && error && (
102
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-background/80">
103
+ <span className="text-sm font-medium text-destructive">{errorLabel}</span>
104
+ <span className="max-w-md text-center text-sm text-muted-foreground">{error}</span>
105
+ </div>
106
+ )}
107
+ </div>
108
+ )
109
+ },
110
+ )
111
+
112
+ ExpoAppPreview.displayName = "ExpoAppPreview"
@@ -0,0 +1,2 @@
1
+ export { ExpoAppPreview } from "./ExpoAppPreview"
2
+ export type { ExpoAppPreviewProps } from "./interfaces"
@@ -0,0 +1,58 @@
1
+ import type * as React from "react"
2
+
3
+ /**
4
+ * Props for the ExpoAppPreview Block.
5
+ *
6
+ * A generic, dependency-free container that renders a full-bleed sandboxed
7
+ * iframe pointing at an Expo/React Native app compiled to web (dev server or
8
+ * static export). It owns no knowledge of how the `src` is produced or served —
9
+ * consumers (e.g. waypoint) compute the URL and pass it in.
10
+ */
11
+ export interface ExpoAppPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ /** Absolute or same-origin URL of the app to render in the iframe. */
13
+ src: string
14
+ /** Accessible name for the iframe (default: "App preview"). */
15
+ title?: string
16
+ /**
17
+ * iframe `sandbox` attribute. Defaults to ADR-033's contract:
18
+ * `allow-scripts allow-same-origin allow-forms allow-popups`.
19
+ *
20
+ * Security note: `allow-scripts` combined with `allow-same-origin` lets the
21
+ * embedded app remove its own sandbox restrictions, so this is an isolation
22
+ * boundary for reliable rendering, not a security boundary against the
23
+ * embedded app. Omit `allow-same-origin` when the embedded app is untrusted.
24
+ */
25
+ sandbox?: string
26
+ /** iframe `allow` attribute (e.g. feature policy overrides). Optional. */
27
+ allow?: string
28
+ /** Show a loading overlay (spinner + label) while true. */
29
+ loading?: boolean
30
+ /** Label shown with the spinner while loading. */
31
+ loadingLabel?: string
32
+ /** Show an empty-state overlay when true (e.g. no build deployed yet). */
33
+ isEmpty?: boolean
34
+ /** Label shown in the empty state. */
35
+ emptyLabel?: string
36
+ /** When truthy, show an error overlay with this message. */
37
+ error?: string | null
38
+ /** Label shown above the error message. */
39
+ errorLabel?: string
40
+ /** Fired when the iframe finishes loading. */
41
+ onLoad?: () => void
42
+ /** Fired when the iframe errors. */
43
+ onError?: () => void
44
+ /**
45
+ * Receives the iframe element once mounted (and `null` on unmount).
46
+ * Lets consumers capture a screenshot (e.g. html-to-image) without the
47
+ * block owning capture tooling.
48
+ */
49
+ registerIframe?: (el: HTMLIFrameElement | null) => void
50
+ /**
51
+ * Receives host-side `message` events originating from the iframe window
52
+ * (browser postMessage primitive; optional passthrough — not required for
53
+ * standalone rendering).
54
+ */
55
+ onMessage?: (event: MessageEvent) => void
56
+ /** Extra classes for the iframe element. */
57
+ iframeClassName?: string
58
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Separator } from "@/components/ui/separator";
5
+ import { cn } from "@/lib/utils";
6
+ import { BookmarkIcon } from "lucide-react";
7
+ import type { ComponentProps, HTMLAttributes } from "react";
8
+
9
+ // ── Checkpoint ────────────────────────────────────────────────────────────────
10
+ // Root container: renders [icon] [trigger] [Separator] in a flex row.
11
+ // The Separator stretches to fill remaining width automatically.
12
+
13
+ export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
14
+
15
+ export const Checkpoint = ({
16
+ className,
17
+ children,
18
+ ...props
19
+ }: CheckpointProps) => (
20
+ <div
21
+ className={cn(
22
+ "flex items-center gap-0.5 overflow-hidden text-muted-foreground",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <Separator className="flex-1" />
29
+ </div>
30
+ );
31
+
32
+ // ── CheckpointIcon ────────────────────────────────────────────────────────────
33
+ // Defaults to BookmarkIcon. Pass children to use a custom icon.
34
+
35
+ export type CheckpointIconProps = ComponentProps<typeof BookmarkIcon>;
36
+
37
+ export const CheckpointIcon = ({
38
+ className,
39
+ children,
40
+ ...props
41
+ }: CheckpointIconProps) =>
42
+ children ? (
43
+ <span className={cn("size-4 shrink-0", className)}>{children}</span>
44
+ ) : (
45
+ <BookmarkIcon
46
+ aria-hidden="true"
47
+ className={cn("size-4 shrink-0", className)}
48
+ {...props}
49
+ />
50
+ );
51
+
52
+ // ── CheckpointTrigger ─────────────────────────────────────────────────────────
53
+ // The clickable restore button. Extends shadcn/ui Button.
54
+
55
+ export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
56
+ /** Optional tooltip shown on hover and used as aria-label when no children. */
57
+ tooltip?: string;
58
+ };
59
+
60
+ export const CheckpointTrigger = ({
61
+ children,
62
+ tooltip,
63
+ variant = "ghost",
64
+ size = "sm",
65
+ className,
66
+ ...props
67
+ }: CheckpointTriggerProps) => (
68
+ <Button
69
+ aria-label={tooltip ?? "Restore checkpoint"}
70
+ className={cn(
71
+ "h-8 gap-1.5 px-3 text-muted-foreground hover:text-foreground",
72
+ className
73
+ )}
74
+ size={size}
75
+ title={tooltip}
76
+ type="button"
77
+ variant={variant}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </Button>
82
+ );
83
+
84
+ Checkpoint.displayName = "Checkpoint";
85
+ CheckpointIcon.displayName = "CheckpointIcon";
86
+ CheckpointTrigger.displayName = "CheckpointTrigger";
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BaseEdge,
3
+ EdgeLabelRenderer,
3
4
  type EdgeProps,
4
5
  getSimpleBezierPath,
5
6
  getSmoothStepPath,
@@ -45,18 +46,37 @@ const Strict = ({
45
46
  targetPosition,
46
47
  markerEnd,
47
48
  style,
49
+ label,
48
50
  }: EdgeProps) => {
49
- const [edgePath] = getSmoothStepPath({
51
+ const [edgePath, labelX, labelY] = getSmoothStepPath({
50
52
  sourceX,
51
53
  sourceY,
52
54
  sourcePosition,
53
55
  targetX,
54
56
  targetY,
55
57
  targetPosition,
56
- borderRadius: 0,
58
+ borderRadius: 8,
57
59
  });
58
60
 
59
- return <BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />;
61
+ return (
62
+ <>
63
+ <BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
64
+ {label && (
65
+ <EdgeLabelRenderer>
66
+ <div
67
+ style={{
68
+ position: "absolute",
69
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
70
+ pointerEvents: "all",
71
+ }}
72
+ className="nodrag nopan rounded-md bg-secondary/90 px-2 py-0.5 text-[11px] font-medium text-secondary-foreground border border-border/80 shadow-xs backdrop-blur-xs"
73
+ >
74
+ {label}
75
+ </div>
76
+ </EdgeLabelRenderer>
77
+ )}
78
+ </>
79
+ );
60
80
  };
61
81
 
62
82
  const Animated = ({
@@ -69,15 +89,16 @@ const Animated = ({
69
89
  targetPosition,
70
90
  markerEnd,
71
91
  style,
92
+ label,
72
93
  }: EdgeProps) => {
73
- const [edgePath] = getSmoothStepPath({
94
+ const [edgePath, labelX, labelY] = getSmoothStepPath({
74
95
  sourceX,
75
96
  sourceY,
76
97
  sourcePosition,
77
98
  targetX,
78
99
  targetY,
79
100
  targetPosition,
80
- borderRadius: 0,
101
+ borderRadius: 8,
81
102
  });
82
103
 
83
104
  return (
@@ -86,6 +107,20 @@ const Animated = ({
86
107
  <circle fill="var(--primary)" r="4">
87
108
  <animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
88
109
  </circle>
110
+ {label && (
111
+ <EdgeLabelRenderer>
112
+ <div
113
+ style={{
114
+ position: "absolute",
115
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
116
+ pointerEvents: "all",
117
+ }}
118
+ className="nodrag nopan rounded-md bg-secondary/90 px-2 py-0.5 text-[11px] font-medium text-secondary-foreground border border-border/80 shadow-xs backdrop-blur-xs"
119
+ >
120
+ {label}
121
+ </div>
122
+ </EdgeLabelRenderer>
123
+ )}
89
124
  </>
90
125
  );
91
126
  };
@@ -95,3 +130,4 @@ export const Edge = {
95
130
  Temporary,
96
131
  Animated,
97
132
  };
133
+
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { fn } from '@storybook/test'
2
3
  import { AIConversation } from './AIConversation'
3
4
 
4
5
  const meta: Meta<typeof AIConversation> = {
@@ -126,6 +127,92 @@ export const MultipleAgents: Story = {
126
127
  },
127
128
  }
128
129
 
130
+ export const NestedSubAgents: Story = {
131
+ args: {
132
+ messages: [
133
+ {
134
+ id: '1',
135
+ type: 'human',
136
+ role: 'user',
137
+ content: 'Generate a 30 second product demo video with motion graphics overlays',
138
+ avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
139
+ avatarName: 'User',
140
+ },
141
+ {
142
+ id: '2',
143
+ type: 'ai',
144
+ role: 'orchestrator',
145
+ content: '',
146
+ avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
147
+ avatarName: 'Coordinator',
148
+ subAgents: [
149
+ {
150
+ id: 'call_00_soED4oref3iyY11T',
151
+ name: 'task',
152
+ subAgentName: 'media-generator-agent',
153
+ input: 'Generate a 30 second product demo video with motion graphics overlays',
154
+ status: 'active',
155
+ // Nested children — video-sequencer and motion-graphics are both
156
+ // dispatched by media-generator-agent (task() calls), and stream
157
+ // their own tool-call-started/tool-output-delta/tool-finished
158
+ // events. This is what AIConversation must render one level deep.
159
+ subAgents: [
160
+ {
161
+ id: 'call_00_9LyCOvjRGrApV6II',
162
+ name: 'task',
163
+ subAgentName: 'video-sequencer',
164
+ input: 'Sequence the raw clips into a 30s cut with transitions',
165
+ output: 'Sequenced 12 clips into a 30s timeline. Exported sequence.json (30069 bytes).',
166
+ status: 'completed',
167
+ },
168
+ {
169
+ id: 'call_00_pQZBX2VUDWwmKQUz',
170
+ name: 'task',
171
+ subAgentName: 'motion-graphics',
172
+ input: 'Add lower-thirds, logo animation, and title card overlays',
173
+ output: 'Rendering title card overlay (2 of 4 assets)...',
174
+ status: 'active',
175
+ },
176
+ ],
177
+ },
178
+ ],
179
+ blocks: [
180
+ { type: 'text', id: 'text-1', text: "I'll generate this using two specialized sub-agents." },
181
+ {
182
+ type: 'subAgent',
183
+ id: 'call_00_soED4oref3iyY11T',
184
+ subAgent: {
185
+ id: 'call_00_soED4oref3iyY11T',
186
+ name: 'task',
187
+ subAgentName: 'media-generator-agent',
188
+ input: 'Generate a 30 second product demo video with motion graphics overlays',
189
+ status: 'active',
190
+ subAgents: [
191
+ {
192
+ id: 'call_00_9LyCOvjRGrApV6II',
193
+ name: 'task',
194
+ subAgentName: 'video-sequencer',
195
+ input: 'Sequence the raw clips into a 30s cut with transitions',
196
+ output: 'Sequenced 12 clips into a 30s timeline. Exported sequence.json (30069 bytes).',
197
+ status: 'completed',
198
+ },
199
+ {
200
+ id: 'call_00_pQZBX2VUDWwmKQUz',
201
+ name: 'task',
202
+ subAgentName: 'motion-graphics',
203
+ input: 'Add lower-thirds, logo animation, and title card overlays',
204
+ output: 'Rendering title card overlay (2 of 4 assets)...',
205
+ status: 'active',
206
+ },
207
+ ],
208
+ },
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ },
214
+ }
215
+
129
216
  export const Empty: Story = {
130
217
  args: {
131
218
  messages: [],
@@ -161,4 +248,46 @@ export const UserOnly: Story = {
161
248
  },
162
249
  ],
163
250
  },
251
+ }
252
+
253
+ export const WithCheckpointRestore: Story = {
254
+ args: {
255
+ messages: [
256
+ {
257
+ id: '1',
258
+ type: 'human',
259
+ role: 'user',
260
+ content: 'What is React?',
261
+ avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
262
+ avatarName: 'User',
263
+ },
264
+ {
265
+ id: '2',
266
+ type: 'ai',
267
+ role: 'orchestrator',
268
+ content: 'React is a JavaScript library for building user interfaces. It was developed by Facebook and is now maintained by Meta and a community of developers.',
269
+ avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
270
+ avatarName: 'Coordinator',
271
+ checkpointId: 'cp-step-1',
272
+ },
273
+ {
274
+ id: '3',
275
+ type: 'human',
276
+ role: 'user',
277
+ content: 'How does component state work?',
278
+ avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
279
+ avatarName: 'User',
280
+ },
281
+ {
282
+ id: '4',
283
+ type: 'ai',
284
+ role: 'orchestrator',
285
+ content: 'State in React represents values that can change over time and trigger component re-renders when updated.',
286
+ avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
287
+ avatarName: 'Coordinator',
288
+ checkpointId: 'cp-step-2',
289
+ },
290
+ ],
291
+ onRestoreCheckpoint: fn(),
292
+ },
164
293
  }