@elabs-ai/components-ai 4.0.0
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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
- package/dist/_flow-boundary-D63PJ65S.js +186 -0
- package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
- package/dist/_persona-rive-RFR2EUWP.js +164 -0
- package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
- package/dist/index.d.ts +4079 -0
- package/dist/index.js +11770 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/src/_audio-player-media-chrome.tsx +146 -0
- package/src/_chat-shell-rail.tsx +18 -0
- package/src/_code-block-theme.test.ts +134 -0
- package/src/_code-block-theme.ts +184 -0
- package/src/_flow-boundary.tsx +237 -0
- package/src/_flow-lazy.ts +34 -0
- package/src/_interactive-terminal-xterm.ts +32 -0
- package/src/_lazy-mermaid.test.ts +101 -0
- package/src/_lazy-mermaid.ts +117 -0
- package/src/_persona-rive.tsx +252 -0
- package/src/_streamdown-i18n.ts +119 -0
- package/src/agent-timeline.stories.tsx +66 -0
- package/src/agent-timeline.test.tsx +74 -0
- package/src/agent-timeline.tsx +106 -0
- package/src/agent.tsx +113 -0
- package/src/agentic-workspace.stories.tsx +1099 -0
- package/src/artifact.stories.tsx +62 -0
- package/src/artifact.test.tsx +112 -0
- package/src/artifact.tsx +167 -0
- package/src/asset-preview.stories.tsx +100 -0
- package/src/asset-preview.test.tsx +126 -0
- package/src/asset-preview.tsx +218 -0
- package/src/attachments.tsx +393 -0
- package/src/audio-player.stories.tsx +99 -0
- package/src/audio-player.tsx +170 -0
- package/src/blocks-ai-composer.stories.tsx +83 -0
- package/src/canvas.stories.tsx +142 -0
- package/src/canvas.tsx +29 -0
- package/src/chain-of-thought.tsx +193 -0
- package/src/chat-greeting.stories.tsx +96 -0
- package/src/chat-greeting.test.tsx +91 -0
- package/src/chat-greeting.tsx +91 -0
- package/src/chat-shell.stories.tsx +123 -0
- package/src/chat-shell.test.tsx +113 -0
- package/src/chat-shell.tsx +97 -0
- package/src/chat.stories.tsx +99 -0
- package/src/checkpoint.tsx +54 -0
- package/src/code-block.stories.tsx +81 -0
- package/src/code-block.test.tsx +191 -0
- package/src/code-block.tsx +673 -0
- package/src/commit.tsx +343 -0
- package/src/composer.stories.tsx +188 -0
- package/src/composer.test.tsx +182 -0
- package/src/composer.tsx +202 -0
- package/src/confirmation.stories.tsx +61 -0
- package/src/confirmation.test.tsx +136 -0
- package/src/confirmation.tsx +273 -0
- package/src/connection.tsx +23 -0
- package/src/context-panel.stories.tsx +137 -0
- package/src/context-panel.test.tsx +198 -0
- package/src/context-panel.tsx +609 -0
- package/src/context.stories.tsx +16 -0
- package/src/context.tsx +363 -0
- package/src/controls.tsx +20 -0
- package/src/conversation.stories.tsx +26 -0
- package/src/conversation.test.tsx +123 -0
- package/src/conversation.tsx +143 -0
- package/src/dark-theme-variant.test.ts +47 -0
- package/src/edge.tsx +33 -0
- package/src/environment-variables.tsx +304 -0
- package/src/file-tree.stories.tsx +55 -0
- package/src/file-tree.test.tsx +101 -0
- package/src/file-tree.tsx +390 -0
- package/src/gallery.stories.tsx +182 -0
- package/src/gallery.test.tsx +150 -0
- package/src/gallery.tsx +737 -0
- package/src/grouped-parts.stories.tsx +104 -0
- package/src/grouped-parts.test.tsx +167 -0
- package/src/grouped-parts.tsx +247 -0
- package/src/image.stories.tsx +67 -0
- package/src/image.test.tsx +105 -0
- package/src/image.tsx +115 -0
- package/src/index.ts +93 -0
- package/src/inline-citation.stories.tsx +91 -0
- package/src/inline-citation.test.tsx +96 -0
- package/src/inline-citation.tsx +314 -0
- package/src/interactive-terminal.stories.tsx +165 -0
- package/src/interactive-terminal.test.tsx +448 -0
- package/src/interactive-terminal.tsx +444 -0
- package/src/jsx-preview.stories.tsx +265 -0
- package/src/jsx-preview.test.tsx +75 -0
- package/src/jsx-preview.tsx +409 -0
- package/src/markdown-view.stories.tsx +57 -0
- package/src/markdown-view.test.tsx +36 -0
- package/src/markdown-view.tsx +113 -0
- package/src/message-edit.stories.tsx +150 -0
- package/src/message-edit.test.tsx +115 -0
- package/src/message-edit.tsx +336 -0
- package/src/message-feedback.stories.tsx +53 -0
- package/src/message-feedback.test.tsx +62 -0
- package/src/message-feedback.tsx +117 -0
- package/src/message-form-spec.ts +349 -0
- package/src/message-form.stories.tsx +182 -0
- package/src/message-form.test.tsx +227 -0
- package/src/message-form.tsx +845 -0
- package/src/message-table-spec.ts +281 -0
- package/src/message-table.stories.tsx +176 -0
- package/src/message-table.test.tsx +144 -0
- package/src/message-table.tsx +366 -0
- package/src/message.stories.tsx +262 -0
- package/src/message.test.tsx +508 -0
- package/src/message.tsx +619 -0
- package/src/mic-selector.tsx +339 -0
- package/src/microcopy.test.tsx +123 -0
- package/src/model-selector.stories.tsx +73 -0
- package/src/model-selector.test.tsx +55 -0
- package/src/model-selector.tsx +225 -0
- package/src/motion-config.tsx +49 -0
- package/src/node.tsx +71 -0
- package/src/open-in-chat.tsx +340 -0
- package/src/package-info.tsx +205 -0
- package/src/panel.tsx +20 -0
- package/src/part-groups.ts +258 -0
- package/src/persona-sources.ts +59 -0
- package/src/persona.stories.tsx +111 -0
- package/src/persona.test.tsx +67 -0
- package/src/persona.tsx +148 -0
- package/src/plan.tsx +135 -0
- package/src/prompt-input.stories.tsx +261 -0
- package/src/prompt-input.test.tsx +392 -0
- package/src/prompt-input.tsx +1668 -0
- package/src/queue.tsx +237 -0
- package/src/reasoning.stories.tsx +81 -0
- package/src/reasoning.test.tsx +50 -0
- package/src/reasoning.tsx +229 -0
- package/src/sandbox.stories.tsx +62 -0
- package/src/sandbox.test.tsx +42 -0
- package/src/sandbox.tsx +149 -0
- package/src/schema-display.tsx +412 -0
- package/src/selection-toolbar.stories.tsx +86 -0
- package/src/selection-toolbar.test.tsx +89 -0
- package/src/selection-toolbar.tsx +192 -0
- package/src/shimmer.stories.tsx +10 -0
- package/src/shimmer.tsx +81 -0
- package/src/snippet.stories.tsx +19 -0
- package/src/snippet.test.tsx +116 -0
- package/src/snippet.tsx +138 -0
- package/src/sources.stories.tsx +34 -0
- package/src/sources.test.tsx +129 -0
- package/src/sources.tsx +93 -0
- package/src/speech-input.tsx +312 -0
- package/src/stack-trace.tsx +479 -0
- package/src/streamdown-i18n.test.tsx +163 -0
- package/src/suggestion.stories.tsx +75 -0
- package/src/suggestion.test.tsx +72 -0
- package/src/suggestion.tsx +120 -0
- package/src/task.stories.tsx +22 -0
- package/src/task.test.tsx +36 -0
- package/src/task.tsx +89 -0
- package/src/templates-ai-assistant.stories.tsx +142 -0
- package/src/terminal.tsx +244 -0
- package/src/test-results.stories.tsx +177 -0
- package/src/test-results.test.tsx +241 -0
- package/src/test-results.tsx +408 -0
- package/src/tool-result-card.stories.tsx +143 -0
- package/src/tool-result-card.test.tsx +72 -0
- package/src/tool-result-card.tsx +86 -0
- package/src/tool.stories.tsx +60 -0
- package/src/tool.test.tsx +63 -0
- package/src/tool.tsx +235 -0
- package/src/toolbar.tsx +44 -0
- package/src/transcription.tsx +118 -0
- package/src/voice-selector.tsx +469 -0
- package/src/web-preview.stories.tsx +67 -0
- package/src/web-preview.test.tsx +117 -0
- package/src/web-preview.tsx +283 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@elabs-ai/components-ui";
|
|
4
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
|
+
import { ArrowRightIcon, MinusIcon, PackageIcon, PlusIcon } from "lucide-react";
|
|
6
|
+
import type { HTMLAttributes } from "react";
|
|
7
|
+
import { createContext, useContext, useMemo } from "react";
|
|
8
|
+
|
|
9
|
+
type ChangeType = "major" | "minor" | "patch" | "added" | "removed";
|
|
10
|
+
|
|
11
|
+
interface PackageInfoContextType {
|
|
12
|
+
name: string;
|
|
13
|
+
currentVersion?: string;
|
|
14
|
+
newVersion?: string;
|
|
15
|
+
changeType?: ChangeType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PackageInfoContext = createContext<PackageInfoContextType>({
|
|
19
|
+
name: "",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type PackageInfoHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
23
|
+
|
|
24
|
+
export const PackageInfoHeader = ({ className, children, ...props }: PackageInfoHeaderProps) => (
|
|
25
|
+
<div className={cn("flex items-center justify-between gap-2", className)} {...props}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type PackageInfoNameProps = HTMLAttributes<HTMLDivElement>;
|
|
31
|
+
|
|
32
|
+
export const PackageInfoName = ({ className, children, ...props }: PackageInfoNameProps) => {
|
|
33
|
+
const { name } = useContext(PackageInfoContext);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={cn("flex items-center gap-2", className)} {...props}>
|
|
37
|
+
<PackageIcon className="size-4 text-muted-foreground" />
|
|
38
|
+
<span className="font-medium font-mono text-sm">{children ?? name}</span>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const changeTypeStyles: Record<ChangeType, string> = {
|
|
44
|
+
added: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
|
45
|
+
major: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
|
46
|
+
minor: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
|
|
47
|
+
patch: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
|
48
|
+
removed: "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const changeTypeIcons: Record<ChangeType, React.ReactNode> = {
|
|
52
|
+
added: <PlusIcon className="size-3" />,
|
|
53
|
+
major: <ArrowRightIcon className="size-3" />,
|
|
54
|
+
minor: <ArrowRightIcon className="size-3" />,
|
|
55
|
+
patch: <ArrowRightIcon className="size-3" />,
|
|
56
|
+
removed: <MinusIcon className="size-3" />,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type PackageInfoChangeTypeProps = HTMLAttributes<HTMLDivElement>;
|
|
60
|
+
|
|
61
|
+
export const PackageInfoChangeType = ({
|
|
62
|
+
className,
|
|
63
|
+
children,
|
|
64
|
+
...props
|
|
65
|
+
}: PackageInfoChangeTypeProps) => {
|
|
66
|
+
const { changeType } = useContext(PackageInfoContext);
|
|
67
|
+
|
|
68
|
+
if (!changeType) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Badge
|
|
74
|
+
className={cn("gap-1 text-xs capitalize", changeTypeStyles[changeType], className)}
|
|
75
|
+
variant="secondary"
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{changeTypeIcons[changeType]}
|
|
79
|
+
{children ?? changeType}
|
|
80
|
+
</Badge>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type PackageInfoVersionProps = HTMLAttributes<HTMLDivElement>;
|
|
85
|
+
|
|
86
|
+
export const PackageInfoVersion = ({ className, children, ...props }: PackageInfoVersionProps) => {
|
|
87
|
+
const { currentVersion, newVersion } = useContext(PackageInfoContext);
|
|
88
|
+
|
|
89
|
+
if (!(currentVersion || newVersion)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
"mt-2 flex items-center gap-2 font-mono text-muted-foreground text-sm",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{children ?? (
|
|
102
|
+
<>
|
|
103
|
+
{currentVersion && <span>{currentVersion}</span>}
|
|
104
|
+
{currentVersion && newVersion && <ArrowRightIcon className="size-3" />}
|
|
105
|
+
{newVersion && <span className="font-medium text-foreground">{newVersion}</span>}
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type PackageInfoProps = HTMLAttributes<HTMLDivElement> & {
|
|
113
|
+
name: string;
|
|
114
|
+
currentVersion?: string;
|
|
115
|
+
newVersion?: string;
|
|
116
|
+
changeType?: ChangeType;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const PackageInfo = ({
|
|
120
|
+
name,
|
|
121
|
+
currentVersion,
|
|
122
|
+
newVersion,
|
|
123
|
+
changeType,
|
|
124
|
+
className,
|
|
125
|
+
children,
|
|
126
|
+
...props
|
|
127
|
+
}: PackageInfoProps) => {
|
|
128
|
+
const contextValue = useMemo(
|
|
129
|
+
() => ({ changeType, currentVersion, name, newVersion }),
|
|
130
|
+
[changeType, currentVersion, name, newVersion],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<PackageInfoContext.Provider value={contextValue}>
|
|
135
|
+
<div className={cn("rounded-lg border bg-background p-4", className)} {...props}>
|
|
136
|
+
{children ?? (
|
|
137
|
+
<>
|
|
138
|
+
<PackageInfoHeader>
|
|
139
|
+
<PackageInfoName />
|
|
140
|
+
{changeType && <PackageInfoChangeType />}
|
|
141
|
+
</PackageInfoHeader>
|
|
142
|
+
{(currentVersion || newVersion) && <PackageInfoVersion />}
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</PackageInfoContext.Provider>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type PackageInfoDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
151
|
+
|
|
152
|
+
export const PackageInfoDescription = ({
|
|
153
|
+
className,
|
|
154
|
+
children,
|
|
155
|
+
...props
|
|
156
|
+
}: PackageInfoDescriptionProps) => (
|
|
157
|
+
<p className={cn("mt-2 text-muted-foreground text-sm", className)} {...props}>
|
|
158
|
+
{children}
|
|
159
|
+
</p>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
export type PackageInfoContentProps = HTMLAttributes<HTMLDivElement>;
|
|
163
|
+
|
|
164
|
+
export const PackageInfoContent = ({ className, children, ...props }: PackageInfoContentProps) => (
|
|
165
|
+
<div className={cn("mt-3 border-t pt-3", className)} {...props}>
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
export type PackageInfoDependenciesProps = HTMLAttributes<HTMLDivElement>;
|
|
171
|
+
|
|
172
|
+
export const PackageInfoDependencies = ({
|
|
173
|
+
className,
|
|
174
|
+
children,
|
|
175
|
+
...props
|
|
176
|
+
}: PackageInfoDependenciesProps) => (
|
|
177
|
+
<div className={cn("space-y-2", className)} {...props}>
|
|
178
|
+
<span className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
179
|
+
Dependencies
|
|
180
|
+
</span>
|
|
181
|
+
<div className="space-y-1">{children}</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
export type PackageInfoDependencyProps = HTMLAttributes<HTMLDivElement> & {
|
|
186
|
+
name: string;
|
|
187
|
+
version?: string;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const PackageInfoDependency = ({
|
|
191
|
+
name,
|
|
192
|
+
version,
|
|
193
|
+
className,
|
|
194
|
+
children,
|
|
195
|
+
...props
|
|
196
|
+
}: PackageInfoDependencyProps) => (
|
|
197
|
+
<div className={cn("flex items-center justify-between text-sm", className)} {...props}>
|
|
198
|
+
{children ?? (
|
|
199
|
+
<>
|
|
200
|
+
<span className="font-mono text-muted-foreground">{name}</span>
|
|
201
|
+
{version && <span className="font-mono text-xs">{version}</span>}
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
package/src/panel.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Panel as PanelPrimitive } from "@xyflow/react";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { Suspense } from "react";
|
|
6
|
+
|
|
7
|
+
import { lazyFlowPart } from "./_flow-lazy";
|
|
8
|
+
|
|
9
|
+
export type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
|
10
|
+
|
|
11
|
+
/** React Flow lives behind a dynamic import — see `_flow-lazy.ts` and ADR 0019. */
|
|
12
|
+
const PanelImpl = lazyFlowPart<PanelProps>((m) => m.Panel);
|
|
13
|
+
|
|
14
|
+
export const Panel = (props: PanelProps) => (
|
|
15
|
+
// An overlay inside an already-loaded `<Canvas>`, so there is no box to
|
|
16
|
+
// reserve — the engine chunk it needs is the one the canvas already fetched.
|
|
17
|
+
<Suspense fallback={null}>
|
|
18
|
+
<PanelImpl {...props} />
|
|
19
|
+
</Suspense>
|
|
20
|
+
);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* part-groups.ts — Pure, framework-free part-grouping engine.
|
|
3
|
+
*
|
|
4
|
+
* Derives collapsible reasoning/tool traces from an ordered list of message
|
|
5
|
+
* parts. This is a CLIENT-SIDE VIEW TRANSFORM — never a model output format.
|
|
6
|
+
* Adapted from assistant-ui's grouping design onto this library's idiom.
|
|
7
|
+
*
|
|
8
|
+
* `groupBy(part, context)` maps each part to a group-key path (`group-…`);
|
|
9
|
+
* ADJACENT parts sharing a key path coalesce into synthetic group nodes
|
|
10
|
+
* (`{ type: "group-…", status, indices }`); multi-element paths nest. The
|
|
11
|
+
* `group-` namespace is reserved so a group key can never collide with a real
|
|
12
|
+
* part `type`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A reserved group-key. The `group-` prefix can't collide with real part types. */
|
|
16
|
+
export type GroupKey = `group-${string}`;
|
|
17
|
+
|
|
18
|
+
/** Rolled-up status of a group (any member running → the group is running). */
|
|
19
|
+
export type GroupStatus = "pending" | "running" | "complete" | "error";
|
|
20
|
+
|
|
21
|
+
/** The minimum a groupable part must expose. */
|
|
22
|
+
export interface GroupablePart {
|
|
23
|
+
type: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Context passed to `groupBy` for each part. */
|
|
28
|
+
export interface GroupByContext<T extends GroupablePart> {
|
|
29
|
+
index: number;
|
|
30
|
+
parts: readonly T[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Maps a part to its group-key path (`[]` = not grouped, a top-level leaf). */
|
|
34
|
+
export type GroupBy<T extends GroupablePart> = (
|
|
35
|
+
part: T,
|
|
36
|
+
context: GroupByContext<T>,
|
|
37
|
+
) => readonly GroupKey[];
|
|
38
|
+
|
|
39
|
+
/** Extract a part's status; return `undefined` to fall back to the default. */
|
|
40
|
+
export type GetPartStatus<T extends GroupablePart> = (part: T) => GroupStatus | undefined;
|
|
41
|
+
|
|
42
|
+
/** The synthetic node produced for a coalesced group. */
|
|
43
|
+
export interface PartGroupNode {
|
|
44
|
+
type: GroupKey;
|
|
45
|
+
status: GroupStatus;
|
|
46
|
+
/** Original indices of every leaf part under this group (recursive). */
|
|
47
|
+
indices: number[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One node in the grouped tree — a leaf part or a synthetic group. */
|
|
51
|
+
export type GroupedNode<T extends GroupablePart> =
|
|
52
|
+
| { kind: "leaf"; key: string; index: number; part: T }
|
|
53
|
+
| { kind: "group"; key: string; group: PartGroupNode; children: GroupedNode<T>[] };
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Default status extraction
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
const RUNNING_STATES = new Set([
|
|
60
|
+
"input-streaming",
|
|
61
|
+
"input-available",
|
|
62
|
+
"approval-requested",
|
|
63
|
+
"approval-responded",
|
|
64
|
+
"calling",
|
|
65
|
+
"streaming",
|
|
66
|
+
"in-progress",
|
|
67
|
+
]);
|
|
68
|
+
const ERROR_STATES = new Set(["output-error", "error", "failed"]);
|
|
69
|
+
const COMPLETE_STATES = new Set(["output-available", "output-denied", "done", "complete"]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Best-effort status from common AI-SDK part shapes (tool `state`, reasoning
|
|
73
|
+
* `state`, `isStreaming`). Overridable via `getStatus`.
|
|
74
|
+
*/
|
|
75
|
+
export function defaultPartStatus(part: GroupablePart): GroupStatus {
|
|
76
|
+
if (part.isStreaming === true) return "running";
|
|
77
|
+
const state = typeof part.state === "string" ? part.state : undefined;
|
|
78
|
+
if (state) {
|
|
79
|
+
if (ERROR_STATES.has(state)) return "error";
|
|
80
|
+
if (RUNNING_STATES.has(state)) return "running";
|
|
81
|
+
if (COMPLETE_STATES.has(state)) return "complete";
|
|
82
|
+
}
|
|
83
|
+
if (part.errorText || part.error) return "error";
|
|
84
|
+
return "complete";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Roll several member statuses up into one group status. */
|
|
88
|
+
export function rollupStatus(statuses: readonly GroupStatus[]): GroupStatus {
|
|
89
|
+
if (statuses.some((s) => s === "running")) return "running";
|
|
90
|
+
if (statuses.some((s) => s === "error")) return "error";
|
|
91
|
+
if (statuses.some((s) => s === "pending")) return "pending";
|
|
92
|
+
return "complete";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Stable identity keys
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** Stable leaf key so streaming rows don't remount (prefers a part id). */
|
|
100
|
+
function leafKey(part: GroupablePart, index: number): string {
|
|
101
|
+
const id =
|
|
102
|
+
(typeof part.id === "string" && part.id) ||
|
|
103
|
+
(typeof part.toolCallId === "string" && part.toolCallId) ||
|
|
104
|
+
null;
|
|
105
|
+
return id ?? `${part.type}#${index}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Tree building
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
interface Entry<T extends GroupablePart> {
|
|
113
|
+
part: T;
|
|
114
|
+
index: number;
|
|
115
|
+
path: readonly GroupKey[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildLevel<T extends GroupablePart>(
|
|
119
|
+
entries: Entry<T>[],
|
|
120
|
+
statusOf: (part: T) => GroupStatus,
|
|
121
|
+
): GroupedNode<T>[] {
|
|
122
|
+
const out: GroupedNode<T>[] = [];
|
|
123
|
+
let i = 0;
|
|
124
|
+
while (i < entries.length) {
|
|
125
|
+
const entry = entries[i]!;
|
|
126
|
+
if (entry.path.length === 0) {
|
|
127
|
+
out.push({
|
|
128
|
+
kind: "leaf",
|
|
129
|
+
key: leafKey(entry.part, entry.index),
|
|
130
|
+
index: entry.index,
|
|
131
|
+
part: entry.part,
|
|
132
|
+
});
|
|
133
|
+
i += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Coalesce the adjacent run of entries sharing this level's key.
|
|
138
|
+
const key0 = entry.path[0]!;
|
|
139
|
+
const run: Entry<T>[] = [];
|
|
140
|
+
let j = i;
|
|
141
|
+
while (j < entries.length && entries[j]!.path.length > 0 && entries[j]!.path[0] === key0) {
|
|
142
|
+
run.push(entries[j]!);
|
|
143
|
+
j += 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Recurse with each member's path shifted one level down (nesting).
|
|
147
|
+
const childEntries: Entry<T>[] = run.map((r) => ({
|
|
148
|
+
part: r.part,
|
|
149
|
+
index: r.index,
|
|
150
|
+
path: r.path.slice(1),
|
|
151
|
+
}));
|
|
152
|
+
const children = buildLevel(childEntries, statusOf);
|
|
153
|
+
const indices = run.map((r) => r.index);
|
|
154
|
+
const status = rollupStatus(run.map((r) => statusOf(r.part)));
|
|
155
|
+
|
|
156
|
+
out.push({
|
|
157
|
+
kind: "group",
|
|
158
|
+
// First member index keeps the group key stable across append-only streams.
|
|
159
|
+
key: `${key0}@${run[0]!.index}`,
|
|
160
|
+
group: { type: key0, status, indices },
|
|
161
|
+
children,
|
|
162
|
+
});
|
|
163
|
+
i = j;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the grouped tree from an ordered part list. Pure + deterministic —
|
|
170
|
+
* never throws. Adjacent parts with a shared group-key path coalesce; multi-key
|
|
171
|
+
* paths nest; ungrouped parts (path `[]`) stay as top-level leaves.
|
|
172
|
+
*/
|
|
173
|
+
export function buildPartGroups<T extends GroupablePart>(
|
|
174
|
+
parts: readonly T[],
|
|
175
|
+
groupBy: GroupBy<T>,
|
|
176
|
+
getStatus?: GetPartStatus<T>,
|
|
177
|
+
): GroupedNode<T>[] {
|
|
178
|
+
const statusOf = (part: T): GroupStatus => getStatus?.(part) ?? defaultPartStatus(part);
|
|
179
|
+
const entries: Entry<T>[] = parts.map((part, index) => {
|
|
180
|
+
let path: readonly GroupKey[] = [];
|
|
181
|
+
try {
|
|
182
|
+
path = groupBy(part, { index, parts }) ?? [];
|
|
183
|
+
} catch {
|
|
184
|
+
path = []; // a throwing groupBy must not break the transcript
|
|
185
|
+
}
|
|
186
|
+
return { part, index, path };
|
|
187
|
+
});
|
|
188
|
+
return buildLevel(entries, statusOf);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A cheap fingerprint of the parts' identity + type + status. Feed it to a
|
|
193
|
+
* `useMemo` so the grouped tree only rebuilds when something structural
|
|
194
|
+
* changed — streaming token updates that don't change status won't churn it.
|
|
195
|
+
*/
|
|
196
|
+
export function partsFingerprint<T extends GroupablePart>(
|
|
197
|
+
parts: readonly T[],
|
|
198
|
+
getStatus?: GetPartStatus<T>,
|
|
199
|
+
): string {
|
|
200
|
+
return parts
|
|
201
|
+
.map((part, index) => {
|
|
202
|
+
const status = getStatus?.(part) ?? defaultPartStatus(part);
|
|
203
|
+
return `${leafKey(part, index)}:${part.type}:${status}`;
|
|
204
|
+
})
|
|
205
|
+
.join("|");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// groupPartByType — the default grouping strategy + classification hint
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/** Whether a part folds into a trace (`inline`) or always stands alone. */
|
|
213
|
+
export type PartDisplay = "inline" | "standalone";
|
|
214
|
+
|
|
215
|
+
export interface GroupPartByTypeOptions<T extends GroupablePart> {
|
|
216
|
+
/** Group key for the collapsible trace. @default "group-work" */
|
|
217
|
+
groupKey?: GroupKey;
|
|
218
|
+
/** Part types folded into the trace. @default reasoning + tool-* + dynamic-tool + step-start */
|
|
219
|
+
inline?: (string | RegExp)[];
|
|
220
|
+
/** Part types forced standalone (never folded). @default approval / human / confirmation */
|
|
221
|
+
standalone?: (string | RegExp)[];
|
|
222
|
+
/** Explicit per-part classification; overrides the type lists. */
|
|
223
|
+
classify?: (part: T) => PartDisplay | undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const DEFAULT_INLINE: (string | RegExp)[] = [/^reasoning$/, /^tool-/, "dynamic-tool", "step-start"];
|
|
227
|
+
const DEFAULT_STANDALONE: (string | RegExp)[] = [/approval/i, /human/i, "confirmation"];
|
|
228
|
+
|
|
229
|
+
function matchesAny(type: string, patterns: (string | RegExp)[]): boolean {
|
|
230
|
+
return patterns.some((p) => (typeof p === "string" ? p === type : p.test(type)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build a `groupBy` that folds reasoning/tool parts into ONE collapsible trace
|
|
235
|
+
* while forcing human/approval-style parts to stand alone — an interaction card
|
|
236
|
+
* must never fold into a thinking accordion. The classification maps to the
|
|
237
|
+
* `display: "inline" | "standalone"` hint.
|
|
238
|
+
*/
|
|
239
|
+
export function groupPartByType<T extends GroupablePart>(
|
|
240
|
+
options: GroupPartByTypeOptions<T> = {},
|
|
241
|
+
): GroupBy<T> {
|
|
242
|
+
const groupKey = options.groupKey ?? "group-work";
|
|
243
|
+
const inline = options.inline ?? DEFAULT_INLINE;
|
|
244
|
+
const standalone = options.standalone ?? DEFAULT_STANDALONE;
|
|
245
|
+
return (part) => {
|
|
246
|
+
const explicit = options.classify?.(part);
|
|
247
|
+
if (explicit === "standalone") return [];
|
|
248
|
+
if (explicit === "inline") return [groupKey];
|
|
249
|
+
if (matchesAny(part.type, standalone)) return []; // forced standalone
|
|
250
|
+
if (matchesAny(part.type, inline)) return [groupKey];
|
|
251
|
+
return []; // ungrouped (text, sources, files, …) stays top-level
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Type guard: is this render subject a synthetic group node? */
|
|
256
|
+
export function isGroupNode(part: { type: string }): part is PartGroupNode {
|
|
257
|
+
return part.type.startsWith("group-");
|
|
258
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona artwork sources — a plain data module shared by `persona.tsx` (the
|
|
3
|
+
* public shell) and `_persona-rive.tsx` (the lazily-loaded Rive half), so
|
|
4
|
+
* neither has to import the other.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PersonaState = "idle" | "listening" | "thinking" | "speaking" | "asleep";
|
|
8
|
+
|
|
9
|
+
export interface PersonaSource {
|
|
10
|
+
/** The artwork exposes a colour view-model that follows the light/dark theme. */
|
|
11
|
+
dynamicColor: boolean;
|
|
12
|
+
/** The artwork has a Rive view-model (vs. a plain state machine). */
|
|
13
|
+
hasModel: boolean;
|
|
14
|
+
/** Absolute URL of the `.riv` artwork. */
|
|
15
|
+
source: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Where the shipped `.riv` artwork is fetched from.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ These are REMOTE origins fetched at runtime by the Rive runtime — a
|
|
22
|
+
* deployment with a restrictive `connect-src` blocks them, and the component
|
|
23
|
+
* falls back to a token-driven orb. Self-host the six files and pass
|
|
24
|
+
* `<Persona src={…}>` to point at your own copies. See `docs/CSP-AND-NETWORK.md`.
|
|
25
|
+
*/
|
|
26
|
+
export const PERSONA_SOURCES = {
|
|
27
|
+
command: {
|
|
28
|
+
dynamicColor: true,
|
|
29
|
+
hasModel: true,
|
|
30
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/command-2.0.riv",
|
|
31
|
+
},
|
|
32
|
+
glint: {
|
|
33
|
+
dynamicColor: true,
|
|
34
|
+
hasModel: true,
|
|
35
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/glint-2.0.riv",
|
|
36
|
+
},
|
|
37
|
+
halo: {
|
|
38
|
+
dynamicColor: true,
|
|
39
|
+
hasModel: true,
|
|
40
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/halo-2.0.riv",
|
|
41
|
+
},
|
|
42
|
+
mana: {
|
|
43
|
+
dynamicColor: false,
|
|
44
|
+
hasModel: true,
|
|
45
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/mana-2.0.riv",
|
|
46
|
+
},
|
|
47
|
+
obsidian: {
|
|
48
|
+
dynamicColor: true,
|
|
49
|
+
hasModel: true,
|
|
50
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv",
|
|
51
|
+
},
|
|
52
|
+
opal: {
|
|
53
|
+
dynamicColor: false,
|
|
54
|
+
hasModel: false,
|
|
55
|
+
source: "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/orb-1.2.riv",
|
|
56
|
+
},
|
|
57
|
+
} as const satisfies Record<string, PersonaSource>;
|
|
58
|
+
|
|
59
|
+
export type PersonaVariant = keyof typeof PERSONA_SOURCES;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona — the animated agent presence.
|
|
3
|
+
*
|
|
4
|
+
* The artwork is a Rive `.riv` file fetched from a **remote origin** at runtime,
|
|
5
|
+
* and the WebGL2 runtime is loaded lazily (ADR 0019). Both facts matter for
|
|
6
|
+
* deployment: under a restrictive `connect-src` the fetch is blocked and the
|
|
7
|
+
* component falls back to a token-driven orb rather than an empty box. See
|
|
8
|
+
* `docs/CSP-AND-NETWORK.md`, and pass `src` to self-host the artwork.
|
|
9
|
+
*/
|
|
10
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
11
|
+
|
|
12
|
+
import { Persona, type PersonaState } from "./persona";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The visible caption below mirrors the visually-hidden `role="status"` text
|
|
16
|
+
* `Persona` announces to assistive tech for each state (see `PersonaProps.statusLabel`).
|
|
17
|
+
*/
|
|
18
|
+
const STATUS_LABELS: Record<PersonaState, string> = {
|
|
19
|
+
idle: "Assistant idle",
|
|
20
|
+
listening: "Assistant listening",
|
|
21
|
+
thinking: "Assistant thinking…",
|
|
22
|
+
speaking: "Assistant speaking",
|
|
23
|
+
asleep: "Assistant asleep",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: "AI/Persona",
|
|
28
|
+
component: Persona,
|
|
29
|
+
parameters: {
|
|
30
|
+
layout: "centered",
|
|
31
|
+
docs: {
|
|
32
|
+
description: {
|
|
33
|
+
component:
|
|
34
|
+
"The animated agent presence. Artwork is a remote Rive `.riv` file and the WebGL2 runtime loads lazily, so the first frame is the fallback orb. Pass `src` to self-host, `fallback` to replace the placeholder. Blocked/offline loads render the fallback instead of an empty box.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
args: { state: "idle" },
|
|
39
|
+
tags: ["autodocs"],
|
|
40
|
+
} satisfies Meta<typeof Persona>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The offline / CSP-blocked state — and the default story on purpose.
|
|
47
|
+
*
|
|
48
|
+
* `src` points at a URL that cannot resolve, so this renders the fallback orb
|
|
49
|
+
* without reaching the network. Keeping the DEFAULT story offline means CI and
|
|
50
|
+
* the visual sweep never depend on a third-party origin being up.
|
|
51
|
+
*/
|
|
52
|
+
export const Default: Story = {
|
|
53
|
+
args: { src: "about:blank#persona-offline" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** An explicit custom fallback, for brands that want their own resting mark. */
|
|
57
|
+
export const CustomFallback: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
src: "about:blank#persona-offline",
|
|
60
|
+
fallback: (
|
|
61
|
+
<div className="grid size-16 place-items-center rounded-full border-2 border-primary/40 border-dashed">
|
|
62
|
+
<span className="text-meta text-muted-foreground">AI</span>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every conversational state, offline.
|
|
70
|
+
*
|
|
71
|
+
* With live artwork these animate; here they all show the fallback, which is
|
|
72
|
+
* exactly what a locked-down deployment sees. The caption under each orb is
|
|
73
|
+
* the SAME text `Persona` announces to assistive tech via its default
|
|
74
|
+
* `statusLabel` — shown visibly here purely for documentation.
|
|
75
|
+
*/
|
|
76
|
+
export const States: Story = {
|
|
77
|
+
render: (args) => (
|
|
78
|
+
<div className="flex items-center gap-6">
|
|
79
|
+
{(["idle", "listening", "thinking", "speaking", "asleep"] as const).map((state) => (
|
|
80
|
+
<figure className="flex flex-col items-center gap-2" key={state}>
|
|
81
|
+
<Persona {...args} src="about:blank#persona-offline" state={state} />
|
|
82
|
+
<figcaption className="text-meta text-muted-foreground">
|
|
83
|
+
{state}
|
|
84
|
+
<br />
|
|
85
|
+
<span className="italic">{STATUS_LABELS[state]}</span>
|
|
86
|
+
</figcaption>
|
|
87
|
+
</figure>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Opt out of the announcement with `statusLabel={null}` — for a surface that
|
|
95
|
+
* already renders its own `role="status"` line elsewhere (e.g. next to a
|
|
96
|
+
* chat composer) and doesn't want AT told twice.
|
|
97
|
+
*/
|
|
98
|
+
export const NoStatusAnnouncement: Story = {
|
|
99
|
+
args: { src: "about:blank#persona-offline", state: "thinking", statusLabel: null },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Live artwork from the default remote origin.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ This story REACHES THE NETWORK (`*.public.blob.vercel-storage.com`). It is
|
|
106
|
+
* the only one that does; it exists so the real animation can be reviewed, and
|
|
107
|
+
* it degrades to the fallback orb when the origin is unreachable.
|
|
108
|
+
*/
|
|
109
|
+
export const LiveRemoteArtwork: Story = {
|
|
110
|
+
args: { state: "thinking", variant: "obsidian" },
|
|
111
|
+
};
|