@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,237 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The React Flow half of `@elabs-ai/components-ai`'s in-chat agent
|
|
5
|
+
* workspace graph — `Canvas`, `Controls`, `Edge`, `Node`, `Panel`, `Toolbar` —
|
|
6
|
+
* split out so the engine can be `lazy()`-loaded.
|
|
7
|
+
*
|
|
8
|
+
* `@xyflow/react` declares no `sideEffects`, so the six static imports it used to
|
|
9
|
+
* have (one per public module, plus `@xyflow/react/dist/style.css`) put the whole
|
|
10
|
+
* engine into the entry chunk of every consumer, canvas rendered or not. **The
|
|
11
|
+
* stylesheet import has to live here too**: a bare
|
|
12
|
+
* `import "@xyflow/react/dist/style.css"` in `canvas.tsx` keeps the edge alive on
|
|
13
|
+
* its own.
|
|
14
|
+
*
|
|
15
|
+
* This is ONE boundary for all six parts on purpose (see `_flow-lazy.ts`): six
|
|
16
|
+
* boundaries would be six chunks all pulling the same engine. The public modules
|
|
17
|
+
* keep their `import type { … } from "@xyflow/react"` lines (types erase) and
|
|
18
|
+
* their prop types, and render a `lazy()` wrapper over the implementations here.
|
|
19
|
+
* `connection.tsx` is type-only already and needs no boundary.
|
|
20
|
+
*
|
|
21
|
+
* See ADR 0019 and `pnpm heavy-deps:check`.
|
|
22
|
+
*
|
|
23
|
+
* @lazy-boundary This module must only ever be reached via `import()`. The gate
|
|
24
|
+
* fails if anything imports it statically, which would put React Flow back in
|
|
25
|
+
* the entry chunk and make the `lazy()` pointless.
|
|
26
|
+
*/
|
|
27
|
+
import { Card } from "@elabs-ai/components-ui";
|
|
28
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
29
|
+
import type { EdgeProps, InternalNode, Node as FlowNode } from "@xyflow/react";
|
|
30
|
+
import {
|
|
31
|
+
Background,
|
|
32
|
+
BaseEdge,
|
|
33
|
+
Controls as ControlsPrimitive,
|
|
34
|
+
getBezierPath,
|
|
35
|
+
getSimpleBezierPath,
|
|
36
|
+
Handle,
|
|
37
|
+
NodeToolbar,
|
|
38
|
+
Panel as PanelPrimitive,
|
|
39
|
+
Position,
|
|
40
|
+
ReactFlow,
|
|
41
|
+
useInternalNode,
|
|
42
|
+
} from "@xyflow/react";
|
|
43
|
+
|
|
44
|
+
import "@xyflow/react/dist/style.css";
|
|
45
|
+
|
|
46
|
+
import type { CanvasProps } from "./canvas";
|
|
47
|
+
import type { ControlsProps } from "./controls";
|
|
48
|
+
import type { NodeProps } from "./node";
|
|
49
|
+
import type { PanelProps } from "./panel";
|
|
50
|
+
import type { ToolbarProps } from "./toolbar";
|
|
51
|
+
|
|
52
|
+
const deleteKeyCode = ["Backspace", "Delete"];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The React Flow attribution badge is hidden on both canvas surfaces (here and
|
|
56
|
+
* `@elabs-ai/components-flow`'s `CanvasShell`). `@xyflow/react` is
|
|
57
|
+
* MIT — the licence requires the notice in source copies, not a rendered badge —
|
|
58
|
+
* and xyflow asks that the badge only be hidden under a React Flow Pro
|
|
59
|
+
* subscription. Keeping it is therefore a product/commercial decision, and this
|
|
60
|
+
* repo's is to hide it. A consumer can restore it per-canvas with
|
|
61
|
+
* `proOptions={{ hideAttribution: false }}`, which still wins via `...props`.
|
|
62
|
+
*/
|
|
63
|
+
const proOptions = { hideAttribution: true };
|
|
64
|
+
|
|
65
|
+
export const Canvas = ({ children, ...props }: CanvasProps) => (
|
|
66
|
+
<ReactFlow
|
|
67
|
+
deleteKeyCode={deleteKeyCode}
|
|
68
|
+
fitView
|
|
69
|
+
panOnDrag={false}
|
|
70
|
+
panOnScroll
|
|
71
|
+
proOptions={proOptions}
|
|
72
|
+
selectionOnDrag={true}
|
|
73
|
+
zoomOnDoubleClick={false}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<Background bgColor="var(--sidebar)" />
|
|
77
|
+
{children}
|
|
78
|
+
</ReactFlow>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const Controls = ({ className, ...props }: ControlsProps) => (
|
|
82
|
+
<ControlsPrimitive
|
|
83
|
+
className={cn(
|
|
84
|
+
"gap-px overflow-hidden rounded-md border bg-card p-1 shadow-none!",
|
|
85
|
+
"[&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export const EdgeTemporary = ({
|
|
93
|
+
id,
|
|
94
|
+
sourceX,
|
|
95
|
+
sourceY,
|
|
96
|
+
targetX,
|
|
97
|
+
targetY,
|
|
98
|
+
sourcePosition,
|
|
99
|
+
targetPosition,
|
|
100
|
+
}: EdgeProps) => {
|
|
101
|
+
const [edgePath] = getSimpleBezierPath({
|
|
102
|
+
sourcePosition,
|
|
103
|
+
sourceX,
|
|
104
|
+
sourceY,
|
|
105
|
+
targetPosition,
|
|
106
|
+
targetX,
|
|
107
|
+
targetY,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<BaseEdge
|
|
112
|
+
className="stroke-1 stroke-ring"
|
|
113
|
+
id={id}
|
|
114
|
+
path={edgePath}
|
|
115
|
+
style={{
|
|
116
|
+
strokeDasharray: "5, 5",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getHandleCoordsByPosition = (node: InternalNode<FlowNode>, handlePosition: Position) => {
|
|
123
|
+
// Choose the handle type based on position - Left is for target, Right is for source
|
|
124
|
+
const handleType = handlePosition === Position.Left ? "target" : "source";
|
|
125
|
+
|
|
126
|
+
const handle = node.internals.handleBounds?.[handleType]?.find(
|
|
127
|
+
(h) => h.position === handlePosition,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!handle) {
|
|
131
|
+
return [0, 0] as const;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let offsetX = handle.width / 2;
|
|
135
|
+
let offsetY = handle.height / 2;
|
|
136
|
+
|
|
137
|
+
// this is a tiny detail to make the markerEnd of an edge visible.
|
|
138
|
+
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
|
139
|
+
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
|
140
|
+
switch (handlePosition) {
|
|
141
|
+
case Position.Left: {
|
|
142
|
+
offsetX = 0;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case Position.Right: {
|
|
146
|
+
offsetX = handle.width;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case Position.Top: {
|
|
150
|
+
offsetY = 0;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case Position.Bottom: {
|
|
154
|
+
offsetY = handle.height;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
throw new Error(`Invalid handle position: ${handlePosition}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
|
|
163
|
+
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
|
|
164
|
+
|
|
165
|
+
return [x, y] as const;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getEdgeParams = (source: InternalNode<FlowNode>, target: InternalNode<FlowNode>) => {
|
|
169
|
+
const sourcePos = Position.Right;
|
|
170
|
+
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
|
171
|
+
const targetPos = Position.Left;
|
|
172
|
+
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
sourcePos,
|
|
176
|
+
sx,
|
|
177
|
+
sy,
|
|
178
|
+
targetPos,
|
|
179
|
+
tx,
|
|
180
|
+
ty,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const EdgeAnimated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
|
185
|
+
const sourceNode = useInternalNode(source);
|
|
186
|
+
const targetNode = useInternalNode(target);
|
|
187
|
+
|
|
188
|
+
if (!(sourceNode && targetNode)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
|
193
|
+
|
|
194
|
+
const [edgePath] = getBezierPath({
|
|
195
|
+
sourcePosition: sourcePos,
|
|
196
|
+
sourceX: sx,
|
|
197
|
+
sourceY: sy,
|
|
198
|
+
targetPosition: targetPos,
|
|
199
|
+
targetX: tx,
|
|
200
|
+
targetY: ty,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
|
|
206
|
+
<circle fill="var(--primary)" r="4">
|
|
207
|
+
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
|
|
208
|
+
</circle>
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const Node = ({ handles, className, ...props }: NodeProps) => (
|
|
214
|
+
<Card
|
|
215
|
+
className={cn("node-container relative size-full h-auto w-sm gap-0 rounded-md p-0", className)}
|
|
216
|
+
{...props}
|
|
217
|
+
>
|
|
218
|
+
{handles.target && <Handle position={Position.Left} type="target" />}
|
|
219
|
+
{handles.source && <Handle position={Position.Right} type="source" />}
|
|
220
|
+
{props.children}
|
|
221
|
+
</Card>
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
export const Panel = ({ className, ...props }: PanelProps) => (
|
|
225
|
+
<PanelPrimitive
|
|
226
|
+
className={cn("m-4 overflow-hidden rounded-md border bg-card p-1", className)}
|
|
227
|
+
{...props}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
|
232
|
+
<NodeToolbar
|
|
233
|
+
className={cn("flex items-center gap-1 rounded-sm border bg-background p-1.5", className)}
|
|
234
|
+
position={Position.Bottom}
|
|
235
|
+
{...props}
|
|
236
|
+
/>
|
|
237
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one dynamic-import seam every React-Flow-backed component in
|
|
3
|
+
* `@elabs-ai/components-ai` goes through.
|
|
4
|
+
*
|
|
5
|
+
* `@xyflow/react` declares no `sideEffects`, so a static import from any of the
|
|
6
|
+
* six public modules (`canvas`, `controls`, `edge`, `node`, `panel`, `toolbar`)
|
|
7
|
+
* put the whole engine into the entry chunk of every consumer. They all reach it
|
|
8
|
+
* through **one** boundary module — `_flow-boundary.tsx` — rather than six, so
|
|
9
|
+
* the engine lands in exactly one chunk: six boundaries would mean six chunks
|
|
10
|
+
* each pulling the same engine, and a `<Node>` rendered inside an already-loaded
|
|
11
|
+
* `<Canvas>` would fetch a second copy.
|
|
12
|
+
*
|
|
13
|
+
* Because every part resolves from the same specifier, the parts _inside_ a
|
|
14
|
+
* canvas resolve from the module cache the canvas already populated.
|
|
15
|
+
*
|
|
16
|
+
* This module is deliberately NOT itself a lazy boundary (it carries no such
|
|
17
|
+
* docblock tag, and `pnpm heavy-deps:check` would fail the six static imports
|
|
18
|
+
* below if it did): it holds no engine import of its own, only the `import()`
|
|
19
|
+
* call, so the public modules may import it statically. See ADR 0019.
|
|
20
|
+
*/
|
|
21
|
+
import { lazy } from "react";
|
|
22
|
+
import type { ComponentType, LazyExoticComponent } from "react";
|
|
23
|
+
|
|
24
|
+
type FlowBoundaryModule = typeof import("./_flow-boundary");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap one implementation from the React Flow boundary in a `lazy()` component.
|
|
28
|
+
*
|
|
29
|
+
* @param pick Selects the implementation from the boundary module.
|
|
30
|
+
*/
|
|
31
|
+
export const lazyFlowPart = <P>(
|
|
32
|
+
pick: (module: FlowBoundaryModule) => ComponentType<P>,
|
|
33
|
+
): LazyExoticComponent<ComponentType<P>> =>
|
|
34
|
+
lazy(() => import("./_flow-boundary").then((m) => ({ default: pick(m) })));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The xterm.js half of `InteractiveTerminal`, split out so it can be reached
|
|
3
|
+
* through a dynamic `import()`.
|
|
4
|
+
*
|
|
5
|
+
* `@xterm/xterm` declares no `sideEffects`, so a static import from
|
|
6
|
+
* `interactive-terminal.tsx` put the whole emulator — plus its stylesheet — into
|
|
7
|
+
* the entry chunk of every `@elabs-ai/components-ai` consumer, including
|
|
8
|
+
* the vast majority that never render a terminal. The **stylesheet import has to
|
|
9
|
+
* live here too**: a bare `import "@xterm/xterm/css/xterm.css"` in the public
|
|
10
|
+
* module keeps the edge alive on its own.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `_persona-rive.tsx` (a `lazy()` component boundary), this one follows
|
|
13
|
+
* `_lazy-mermaid.ts`: the engine is reached through a **late-called interface**.
|
|
14
|
+
* `InteractiveTerminal` only ever constructs xterm inside its mount effect —
|
|
15
|
+
* never during render — so the public component keeps its `forwardRef`, its
|
|
16
|
+
* imperative handle and its container `<div>` unchanged, and awaits this module
|
|
17
|
+
* inside that effect. No `Suspense` seam, no skeleton swap, no change to the box
|
|
18
|
+
* the terminal paints into (the container is sized by the consumer either way).
|
|
19
|
+
*
|
|
20
|
+
* `ITheme` and the two constructor types stay `import type` in the public module
|
|
21
|
+
* (types erase), which is what keeps `buildInteractiveTerminalTheme` public.
|
|
22
|
+
*
|
|
23
|
+
* See ADR 0019 and `pnpm heavy-deps:check`.
|
|
24
|
+
*
|
|
25
|
+
* @lazy-boundary This module must only ever be reached via `import()`. The gate
|
|
26
|
+
* fails if anything imports it statically, which would put xterm back in the
|
|
27
|
+
* entry chunk and make the dynamic import pointless.
|
|
28
|
+
*/
|
|
29
|
+
import "@xterm/xterm/css/xterm.css";
|
|
30
|
+
|
|
31
|
+
export { FitAddon } from "@xterm/addon-fit";
|
|
32
|
+
export { Terminal as XTerm } from "@xterm/xterm";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createLazyMermaidPlugin, lazyMermaid, preloadMermaid } from "./_lazy-mermaid";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Mermaid engine is mocked so we can observe how the plugin drives it.
|
|
7
|
+
*
|
|
8
|
+
* NOTE this file tests the plugin's *behaviour*. That the engine is genuinely
|
|
9
|
+
* lazy is a property of the BUILD, not of jsdom — `vi.mock` factories are
|
|
10
|
+
* hoisted and cached, so a counter here would prove nothing. The static proof is
|
|
11
|
+
* `pnpm heavy-deps:check`, which asserts no `packages/ai/src` module statically
|
|
12
|
+
* imports mermaid, plus the built `packages/ai/dist/index.js` carrying
|
|
13
|
+
* `import("mermaid")` and no `@streamdown/mermaid` edge.
|
|
14
|
+
*/
|
|
15
|
+
const initialize = vi.fn();
|
|
16
|
+
const render = vi.fn(async (id: string) => ({ svg: `<svg id="${id}"/>` }));
|
|
17
|
+
|
|
18
|
+
vi.mock("mermaid", () => ({ default: { initialize, render } }));
|
|
19
|
+
|
|
20
|
+
describe("lazy mermaid plugin", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
initialize.mockClear();
|
|
23
|
+
render.mockClear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("exposes the DiagramPlugin contract Streamdown expects", () => {
|
|
27
|
+
expect(lazyMermaid).toMatchObject({
|
|
28
|
+
language: "mermaid",
|
|
29
|
+
name: "mermaid",
|
|
30
|
+
type: "diagram",
|
|
31
|
+
});
|
|
32
|
+
expect(typeof lazyMermaid.getMermaid).toBe("function");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("touches the engine only on render, never on getMermaid", () => {
|
|
36
|
+
// Streamdown calls getMermaid() immediately before awaiting render(); the
|
|
37
|
+
// plugin object being present from the first frame is what avoids a
|
|
38
|
+
// "raw source flashes, then becomes a diagram" transition.
|
|
39
|
+
createLazyMermaidPlugin().getMermaid();
|
|
40
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
41
|
+
expect(render).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("delegates render to the engine and returns its svg", async () => {
|
|
45
|
+
const result = await createLazyMermaidPlugin().getMermaid().render("d1", "graph TD; A-->B;");
|
|
46
|
+
|
|
47
|
+
expect(render).toHaveBeenCalledWith("d1", "graph TD; A-->B;");
|
|
48
|
+
expect(result.svg).toContain("d1");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("initializes exactly once across repeated renders", async () => {
|
|
52
|
+
const instance = createLazyMermaidPlugin().getMermaid();
|
|
53
|
+
|
|
54
|
+
await instance.render("d1", "graph TD; A-->B;");
|
|
55
|
+
await instance.render("d2", "graph TD; C-->D;");
|
|
56
|
+
|
|
57
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(render).toHaveBeenCalledTimes(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("applies the upstream brand defaults, including securityLevel: strict", async () => {
|
|
62
|
+
await createLazyMermaidPlugin().getMermaid().render("d1", "graph TD; A-->B;");
|
|
63
|
+
|
|
64
|
+
expect(initialize).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
fontFamily: "monospace",
|
|
67
|
+
securityLevel: "strict",
|
|
68
|
+
startOnLoad: false,
|
|
69
|
+
suppressErrorRendering: true,
|
|
70
|
+
theme: "default",
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("merges a per-plugin config over the defaults", async () => {
|
|
76
|
+
await createLazyMermaidPlugin({ config: { theme: "dark" } })
|
|
77
|
+
.getMermaid()
|
|
78
|
+
.render("d1", "graph TD; A-->B;");
|
|
79
|
+
|
|
80
|
+
expect(initialize).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({ securityLevel: "strict", theme: "dark" }),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("lets getMermaid(config) override, and re-initializes for it", async () => {
|
|
86
|
+
const plugin = createLazyMermaidPlugin();
|
|
87
|
+
|
|
88
|
+
await plugin.getMermaid().render("d1", "graph TD; A-->B;");
|
|
89
|
+
await plugin.getMermaid({ theme: "forest" }).render("d2", "graph TD; C-->D;");
|
|
90
|
+
|
|
91
|
+
expect(initialize).toHaveBeenCalledTimes(2);
|
|
92
|
+
expect(initialize).toHaveBeenLastCalledWith(
|
|
93
|
+
expect.objectContaining({ securityLevel: "strict", theme: "forest" }),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("preloadMermaid resolves without rendering anything", async () => {
|
|
98
|
+
expect(() => preloadMermaid()).not.toThrow();
|
|
99
|
+
expect(render).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A drop-in replacement for `@streamdown/mermaid`'s `mermaid` plugin that loads
|
|
3
|
+
* the Mermaid engine **on first diagram render** instead of at import time.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* `@streamdown/mermaid`'s dist opens with a static `import n from "mermaid"`, and
|
|
8
|
+
* neither it nor `mermaid`/`dompurify`/`d3` declares `sideEffects`, so a bundler
|
|
9
|
+
* must keep the whole edge. `@elabs-ai/components-ai` imported that plugin from three modules
|
|
10
|
+
* (`message`, `reasoning`, `markdown-view`), which put Mermaid + d3 + DOMPurify —
|
|
11
|
+
* several MB — in the **entry chunk** of every consumer, including the vast
|
|
12
|
+
* majority that never render a diagram.
|
|
13
|
+
*
|
|
14
|
+
* How it avoids a render flash
|
|
15
|
+
* ----------------------------
|
|
16
|
+
* `DiagramPlugin` is not the engine; it is a lazy accessor. Streamdown calls
|
|
17
|
+
* `getMermaid()` only from inside its async diagram-render path (immediately
|
|
18
|
+
* before `await instance.render(...)`), never at module or component init. So the
|
|
19
|
+
* plugin object can be present from the very first frame — Streamdown always
|
|
20
|
+
* treats a ```mermaid fence as a diagram — while the engine itself arrives with a
|
|
21
|
+
* dynamic import inside `render()`. No content-sniffing, no plugin swap, no
|
|
22
|
+
* suspense seam at the call sites, and no "raw source flashes then becomes a
|
|
23
|
+
* diagram" transition.
|
|
24
|
+
*
|
|
25
|
+
* The upstream plugin is ~10 lines; its defaults and initialize-once semantics
|
|
26
|
+
* are mirrored exactly below. The `DiagramPlugin` type is still imported (as a
|
|
27
|
+
* type, so it erases) from `@streamdown/mermaid`, which keeps this honest against
|
|
28
|
+
* the real contract at typecheck time.
|
|
29
|
+
*/
|
|
30
|
+
import type { DiagramPlugin, MermaidConfig, MermaidInstance } from "@streamdown/mermaid";
|
|
31
|
+
|
|
32
|
+
/** Mirrors `@streamdown/mermaid`'s defaults — notably `securityLevel: "strict"`. */
|
|
33
|
+
const DEFAULT_CONFIG: MermaidConfig = {
|
|
34
|
+
fontFamily: "monospace",
|
|
35
|
+
securityLevel: "strict",
|
|
36
|
+
startOnLoad: false,
|
|
37
|
+
suppressErrorRendering: true,
|
|
38
|
+
theme: "default",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type MermaidModule = {
|
|
42
|
+
initialize: (config: MermaidConfig) => void;
|
|
43
|
+
render: (id: string, source: string) => Promise<{ svg: string }>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Module-level singleton: the engine is fetched at most once per app, however
|
|
48
|
+
* many diagrams or Streamdown instances render.
|
|
49
|
+
*/
|
|
50
|
+
let enginePromise: Promise<MermaidModule> | undefined;
|
|
51
|
+
|
|
52
|
+
const loadEngine = (): Promise<MermaidModule> => {
|
|
53
|
+
enginePromise ??= import("mermaid").then((m) => m.default as unknown as MermaidModule);
|
|
54
|
+
return enginePromise;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start fetching the Mermaid engine ahead of time.
|
|
59
|
+
*
|
|
60
|
+
* Optional. Call it when you already know the surface will render diagrams (on
|
|
61
|
+
* route entry, or when a conversation is known to contain them) so the first
|
|
62
|
+
* diagram doesn't pay the import latency. Safe to call repeatedly; safe to
|
|
63
|
+
* ignore — rendering a diagram loads the engine either way.
|
|
64
|
+
*/
|
|
65
|
+
export const preloadMermaid = (): void => {
|
|
66
|
+
void loadEngine();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a lazy Mermaid diagram plugin for Streamdown.
|
|
71
|
+
*
|
|
72
|
+
* @param options.config Mermaid config merged over the brand defaults.
|
|
73
|
+
*/
|
|
74
|
+
export const createLazyMermaidPlugin = (
|
|
75
|
+
options: { config?: MermaidConfig } = {},
|
|
76
|
+
): DiagramPlugin => {
|
|
77
|
+
let initialized = false;
|
|
78
|
+
let config: MermaidConfig = { ...DEFAULT_CONFIG, ...options.config };
|
|
79
|
+
|
|
80
|
+
const instance: MermaidInstance = {
|
|
81
|
+
// Upstream calls `mermaid.initialize` synchronously here. We can't (the
|
|
82
|
+
// engine may not have loaded yet), so record the config and apply it inside
|
|
83
|
+
// `render`, which is the only place it can matter — Streamdown always calls
|
|
84
|
+
// `getMermaid()` immediately before awaiting `render()`.
|
|
85
|
+
initialize(next: MermaidConfig) {
|
|
86
|
+
config = { ...DEFAULT_CONFIG, ...options.config, ...next };
|
|
87
|
+
initialized = false;
|
|
88
|
+
},
|
|
89
|
+
async render(id: string, source: string) {
|
|
90
|
+
const engine = await loadEngine();
|
|
91
|
+
if (!initialized) {
|
|
92
|
+
engine.initialize(config);
|
|
93
|
+
initialized = true;
|
|
94
|
+
}
|
|
95
|
+
return engine.render(id, source);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
getMermaid(next?: MermaidConfig) {
|
|
101
|
+
if (next) {
|
|
102
|
+
instance.initialize(next);
|
|
103
|
+
}
|
|
104
|
+
return instance;
|
|
105
|
+
},
|
|
106
|
+
language: "mermaid",
|
|
107
|
+
name: "mermaid",
|
|
108
|
+
type: "diagram",
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The shared lazy Mermaid plugin — the `@elabs-ai/components-ai` replacement for
|
|
114
|
+
* `@streamdown/mermaid`'s eager `mermaid` export. One instance for the whole
|
|
115
|
+
* app, so the engine and its initialize-once state are shared.
|
|
116
|
+
*/
|
|
117
|
+
export const lazyMermaid: DiagramPlugin = createLazyMermaidPlugin();
|