ai-design-system 0.1.75 → 0.1.77
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 +43 -0
- package/components/blocks/AIConversation/AIConversation.tsx +118 -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/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/PromptInput/PromptInput.tsx +17 -0
- package/components/composites/index.ts +8 -0
- 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/index.ts +15 -3
- package/dist/index.cjs +681 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +97 -0
- package/dist/index.d.ts +252 -55
- package/dist/index.js +678 -102
- 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
|
@@ -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,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:
|
|
58
|
+
borderRadius: 8,
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
return
|
|
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:
|
|
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> = {
|
|
@@ -161,4 +162,46 @@ export const UserOnly: Story = {
|
|
|
161
162
|
},
|
|
162
163
|
],
|
|
163
164
|
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const WithCheckpointRestore: Story = {
|
|
168
|
+
args: {
|
|
169
|
+
messages: [
|
|
170
|
+
{
|
|
171
|
+
id: '1',
|
|
172
|
+
type: 'human',
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: 'What is React?',
|
|
175
|
+
avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
|
|
176
|
+
avatarName: 'User',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: '2',
|
|
180
|
+
type: 'ai',
|
|
181
|
+
role: 'orchestrator',
|
|
182
|
+
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.',
|
|
183
|
+
avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
|
|
184
|
+
avatarName: 'Coordinator',
|
|
185
|
+
checkpointId: 'cp-step-1',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: '3',
|
|
189
|
+
type: 'human',
|
|
190
|
+
role: 'user',
|
|
191
|
+
content: 'How does component state work?',
|
|
192
|
+
avatarSrc: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
|
|
193
|
+
avatarName: 'User',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: '4',
|
|
197
|
+
type: 'ai',
|
|
198
|
+
role: 'orchestrator',
|
|
199
|
+
content: 'State in React represents values that can change over time and trigger component re-renders when updated.',
|
|
200
|
+
avatarSrc: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
|
|
201
|
+
avatarName: 'Coordinator',
|
|
202
|
+
checkpointId: 'cp-step-2',
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
onRestoreCheckpoint: fn(),
|
|
206
|
+
},
|
|
164
207
|
}
|
|
@@ -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"
|
|
@@ -40,6 +45,7 @@ interface AIMessage {
|
|
|
40
45
|
subAgents?: SubAgent[];
|
|
41
46
|
isLoading?: boolean;
|
|
42
47
|
blocks?: AIMessageBlock[];
|
|
48
|
+
checkpointId?: string;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export interface AIConversationProps
|
|
@@ -77,6 +83,10 @@ export interface AIConversationProps
|
|
|
77
83
|
* Receives the raw content string and returns a ReactNode.
|
|
78
84
|
*/
|
|
79
85
|
renderSystemMessage?: (content: string) => React.ReactNode
|
|
86
|
+
/**
|
|
87
|
+
* Callback fired when user restores a conversation checkpoint
|
|
88
|
+
*/
|
|
89
|
+
onRestoreCheckpoint?: (messageId: string, checkpointId: string) => void
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/**
|
|
@@ -91,6 +101,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
91
101
|
emptyState,
|
|
92
102
|
onToolAction,
|
|
93
103
|
renderSystemMessage,
|
|
104
|
+
onRestoreCheckpoint,
|
|
94
105
|
...conversationProps
|
|
95
106
|
}) => {
|
|
96
107
|
const isEmpty = React.useMemo(
|
|
@@ -209,6 +220,9 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
209
220
|
}
|
|
210
221
|
currentGroup.blocks = updatedBlocks;
|
|
211
222
|
}
|
|
223
|
+
if (msg.checkpointId) {
|
|
224
|
+
currentGroup.checkpointId = msg.checkpointId;
|
|
225
|
+
}
|
|
212
226
|
currentGroup.isLoading = msg.isLoading;
|
|
213
227
|
} else {
|
|
214
228
|
currentGroup = {
|
|
@@ -216,6 +230,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
216
230
|
toolCalls: msg.toolCalls ? [...msg.toolCalls] : undefined,
|
|
217
231
|
subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
|
|
218
232
|
blocks: msg.blocks ? [...msg.blocks] : undefined,
|
|
233
|
+
checkpointId: msg.checkpointId,
|
|
219
234
|
};
|
|
220
235
|
result.push(currentGroup);
|
|
221
236
|
}
|
|
@@ -269,7 +284,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
269
284
|
)
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
if (message.role === "orchestrator") {
|
|
287
|
+
if (message.role === "orchestrator" || message.role === "assistant" || (!message.role && message.type === "ai")) {
|
|
273
288
|
// Extract sub-agents from the message
|
|
274
289
|
const subAgents = message.subAgents || []
|
|
275
290
|
|
|
@@ -296,39 +311,65 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
296
311
|
const isStreaming = message.isLoading || reasoningCalls.some((tc) => tc.status === "pending") || subAgents.some(sa => sa.status === "active") || directToolCalls.some(tc => tc.status === "pending")
|
|
297
312
|
|
|
298
313
|
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
|
-
|
|
314
|
+
<React.Fragment key={message.id}>
|
|
315
|
+
<OrchestratorMessage
|
|
316
|
+
message={{
|
|
317
|
+
id: message.id,
|
|
318
|
+
content: displayContentStr,
|
|
319
|
+
avatarSrc: message.avatarSrc,
|
|
320
|
+
avatarName: message.avatarName,
|
|
321
|
+
isLoading: message.isLoading && !hasReasoning,
|
|
322
|
+
}}
|
|
323
|
+
showAvatar={showAvatars && hasDisplayContent}
|
|
324
|
+
>
|
|
325
|
+
{/* Render reasoning-section for hidden tool results and subagents */}
|
|
326
|
+
{hasReasoning && (
|
|
327
|
+
<ReasoningDisplay
|
|
328
|
+
content={reasoningText}
|
|
329
|
+
items={message.blocks && message.blocks.length > 0 ? [] : reasoningCalls}
|
|
330
|
+
isStreaming={isStreaming}
|
|
331
|
+
onToolAction={onToolAction}
|
|
332
|
+
defaultOpen={isStreaming || index === groupedMessages.length - 1}
|
|
333
|
+
>
|
|
334
|
+
{message.blocks && message.blocks.length > 0 ? (
|
|
335
|
+
message.blocks.map((block, i) => {
|
|
336
|
+
if (block.type === 'text') {
|
|
337
|
+
return block.text && block.text.trim() ? (
|
|
338
|
+
<div key={`text-${i}`} className="mb-4 text-muted-foreground whitespace-pre-wrap">
|
|
339
|
+
{block.text}
|
|
340
|
+
</div>
|
|
341
|
+
) : null;
|
|
342
|
+
} else if (block.type === 'toolCall' && block.toolCall && block.toolCall.name !== 'ask_user' && block.toolCall.name !== 'ask_question') {
|
|
343
|
+
return <ToolCallDisplay key={block.id} toolCall={block.toolCall} onToolAction={onToolAction} />;
|
|
344
|
+
} else if (block.type === 'subAgent' && block.subAgent) {
|
|
345
|
+
const subAgent = block.subAgent;
|
|
346
|
+
return (
|
|
347
|
+
<SpecialistMessage
|
|
348
|
+
key={subAgent.id}
|
|
349
|
+
message={{
|
|
350
|
+
id: subAgent.id,
|
|
351
|
+
name: subAgent.subAgentName,
|
|
352
|
+
description: undefined,
|
|
353
|
+
input: subAgent.input,
|
|
354
|
+
content: typeof subAgent.output === 'string' ? subAgent.output : (subAgent.output ? JSON.stringify(subAgent.output) : ''),
|
|
355
|
+
status: subAgent.status,
|
|
356
|
+
toolCalls: [],
|
|
357
|
+
}}
|
|
358
|
+
isNested={true}
|
|
359
|
+
/>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
})
|
|
364
|
+
) : (
|
|
365
|
+
<>
|
|
366
|
+
{/* Render direct tool calls fallback */}
|
|
367
|
+
{directToolCalls.map((tc) => (
|
|
368
|
+
<ToolCallDisplay key={tc.id} toolCall={tc} onToolAction={onToolAction} />
|
|
369
|
+
))}
|
|
370
|
+
|
|
371
|
+
{/* Render specialist sub-agents fallback */}
|
|
372
|
+
{subAgents.map((subAgent) => (
|
|
332
373
|
<SpecialistMessage
|
|
333
374
|
key={subAgent.id}
|
|
334
375
|
message={{
|
|
@@ -342,57 +383,57 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
342
383
|
}}
|
|
343
384
|
isNested={true}
|
|
344
385
|
/>
|
|
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>
|
|
386
|
+
))}
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</ReasoningDisplay>
|
|
390
|
+
)}
|
|
391
|
+
</OrchestratorMessage>
|
|
392
|
+
{message.checkpointId && onRestoreCheckpoint && !message.isLoading && !hasReasoning && (
|
|
393
|
+
<Checkpoint>
|
|
394
|
+
<CheckpointIcon />
|
|
395
|
+
<CheckpointTrigger
|
|
396
|
+
onClick={() =>
|
|
397
|
+
onRestoreCheckpoint(message.id, message.checkpointId!)
|
|
398
|
+
}
|
|
399
|
+
>
|
|
400
|
+
Restore checkpoint
|
|
401
|
+
</CheckpointTrigger>
|
|
402
|
+
</Checkpoint>
|
|
375
403
|
)}
|
|
376
|
-
</
|
|
404
|
+
</React.Fragment>
|
|
377
405
|
)
|
|
378
406
|
}
|
|
379
407
|
|
|
380
408
|
// Default to specialist message for any other AI messages
|
|
381
409
|
return (
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
410
|
+
<React.Fragment key={message.id}>
|
|
411
|
+
<SpecialistMessage
|
|
412
|
+
message={{
|
|
413
|
+
id: message.id,
|
|
414
|
+
name: message.avatarName || "Agent",
|
|
415
|
+
content: contentStr,
|
|
416
|
+
toolCalls: message.toolCalls?.filter((tc) => tc.name !== "task"),
|
|
417
|
+
status: "completed",
|
|
418
|
+
}}
|
|
419
|
+
isNested={false}
|
|
420
|
+
/>
|
|
421
|
+
{message.checkpointId && onRestoreCheckpoint && !message.isLoading && (
|
|
422
|
+
<Checkpoint>
|
|
423
|
+
<CheckpointIcon />
|
|
424
|
+
<CheckpointTrigger
|
|
425
|
+
onClick={() =>
|
|
426
|
+
onRestoreCheckpoint(message.id, message.checkpointId!)
|
|
427
|
+
}
|
|
428
|
+
>
|
|
429
|
+
Restore checkpoint
|
|
430
|
+
</CheckpointTrigger>
|
|
431
|
+
</Checkpoint>
|
|
432
|
+
)}
|
|
433
|
+
</React.Fragment>
|
|
393
434
|
)
|
|
394
435
|
}),
|
|
395
|
-
[groupedMessages, showAvatars, onToolAction]
|
|
436
|
+
[groupedMessages, showAvatars, onToolAction, onRestoreCheckpoint]
|
|
396
437
|
)
|
|
397
438
|
|
|
398
439
|
return (
|