@elabs-ai/components-ai 4.0.0 → 4.2.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/README.md +62 -12
- package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
- package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
- package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
- package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
- package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
- package/dist/_persona-rive-JEG44YHX.js.map +1 -0
- package/dist/index.d.ts +1152 -218
- package/dist/index.js +5601 -3241
- package/dist/index.js.map +1 -1
- package/package.json +30 -15
- package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
- package/src/__contract__/chat-shell.contract.test.tsx +49 -0
- package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
- package/src/__contract__/image.contract.test.tsx +49 -0
- package/src/__contract__/markdown-view.contract.test.tsx +49 -0
- package/src/__contract__/message-feedback.contract.test.tsx +49 -0
- package/src/__contract__/message-form.contract.test.tsx +49 -0
- package/src/__contract__/message-table.contract.test.tsx +49 -0
- package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
- package/src/__contract__/persona.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_chat-shell-rail.tsx +2 -2
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-cjk.test.ts +43 -0
- package/src/_lazy-cjk.ts +73 -0
- package/src/_lazy-engine-boundary.tsx +61 -0
- package/src/_lazy-math.test.ts +63 -0
- package/src/_lazy-math.ts +90 -0
- package/src/_lazy-mermaid-absent.test.ts +53 -0
- package/src/_lazy-mermaid.test.ts +15 -0
- package/src/_lazy-mermaid.ts +24 -1
- package/src/_mermaid-error-panel.test.tsx +50 -0
- package/src/_mermaid-error-panel.tsx +66 -0
- package/src/_persona-rive.tsx +62 -10
- package/src/_streamdown-i18n.ts +94 -22
- package/src/_streamdown-safety.ts +170 -0
- package/src/_theme-scope-store.test.ts +83 -0
- package/src/_theme-scope-store.ts +103 -0
- package/src/agent-event.stories.tsx +97 -0
- package/src/agent-event.test.tsx +145 -0
- package/src/agent-event.tsx +187 -0
- package/src/agent-timeline.stories.tsx +9 -1
- package/src/agent.test.tsx +47 -0
- package/src/agent.tsx +43 -24
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +17 -14
- package/src/asset-preview.test.tsx +40 -0
- package/src/asset-preview.tsx +83 -12
- package/src/attachments.tsx +7 -4
- package/src/audio-player.test.tsx +78 -0
- package/src/audio-player.tsx +243 -56
- package/src/audio-visualizer.stories.tsx +126 -0
- package/src/audio-visualizer.test.tsx +438 -0
- package/src/audio-visualizer.tsx +367 -0
- package/src/canvas.stories.tsx +150 -1
- package/src/chat-shell.stories.tsx +18 -3
- package/src/chat.stories.tsx +16 -2
- package/src/code-block.stories.tsx +9 -1
- package/src/code-block.test.tsx +100 -1
- package/src/code-block.tsx +166 -103
- package/src/commit.tsx +30 -41
- package/src/composer.stories.tsx +372 -9
- package/src/composer.test.tsx +357 -6
- package/src/composer.tsx +217 -35
- package/src/confirmation.stories.tsx +72 -1
- package/src/confirmation.test.tsx +216 -2
- package/src/confirmation.tsx +264 -4
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +3 -2
- package/src/conversation.stories.tsx +86 -2
- package/src/conversation.test.tsx +66 -0
- package/src/conversation.tsx +52 -9
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +238 -0
- package/src/diff-view.tsx +697 -0
- package/src/environment-variables.tsx +20 -37
- package/src/file-tree.test.tsx +21 -0
- package/src/file-tree.tsx +12 -2
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/inline-citation.tsx +5 -5
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/jsx-preview.tsx +151 -43
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +237 -2
- package/src/markdown-view.tsx +157 -6
- package/src/message-compare.stories.tsx +175 -0
- package/src/message-compare.test.tsx +207 -0
- package/src/message-compare.tsx +453 -0
- package/src/message-form.stories.tsx +65 -4
- package/src/message-form.test.tsx +8 -2
- package/src/message-form.tsx +15 -7
- package/src/message-table.stories.tsx +2 -2
- package/src/message-table.test.tsx +7 -0
- package/src/message-table.tsx +8 -4
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +247 -1
- package/src/message.tsx +125 -15
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/model-provider-logo.test.tsx +109 -0
- package/src/model-provider-logo.tsx +183 -0
- package/src/open-in-chat.tsx +50 -29
- package/src/package-info.tsx +12 -12
- package/src/permission-mode-select.stories.tsx +82 -0
- package/src/permission-mode-select.test.tsx +100 -0
- package/src/permission-mode-select.tsx +137 -0
- package/src/persona-missing-peer.test.tsx +54 -0
- package/src/persona.tsx +68 -22
- package/src/plan.stories.tsx +166 -0
- package/src/plan.test.tsx +267 -0
- package/src/plan.tsx +182 -20
- package/src/prompt-input-effort.stories.tsx +123 -0
- package/src/prompt-input-effort.test.tsx +83 -0
- package/src/prompt-input-effort.tsx +136 -0
- package/src/prompt-input-mode.stories.tsx +108 -0
- package/src/prompt-input-mode.test.tsx +99 -0
- package/src/prompt-input-mode.tsx +169 -0
- package/src/prompt-input-slash.stories.tsx +211 -0
- package/src/prompt-input-slash.test.tsx +262 -0
- package/src/prompt-input-slash.tsx +541 -0
- package/src/prompt-input.stories.tsx +2 -2
- package/src/prompt-input.test.tsx +67 -1
- package/src/prompt-input.tsx +42 -4
- package/src/queue.tsx +4 -4
- package/src/reasoning.tsx +42 -17
- package/src/sandbox.stories.tsx +9 -1
- package/src/sandbox.tsx +3 -3
- package/src/schema-display.test.tsx +56 -0
- package/src/schema-display.tsx +71 -37
- package/src/selection-toolbar.stories.tsx +9 -1
- package/src/session-header.stories.tsx +128 -0
- package/src/session-header.test.tsx +138 -0
- package/src/session-header.tsx +243 -0
- package/src/session-status-bar.stories.tsx +73 -0
- package/src/session-status-bar.test.tsx +94 -0
- package/src/session-status-bar.tsx +165 -0
- package/src/snippet.stories.tsx +9 -1
- package/src/snippet.test.tsx +6 -2
- package/src/snippet.tsx +14 -34
- package/src/speech-input.test.tsx +109 -0
- package/src/speech-input.tsx +31 -3
- package/src/stack-trace.tsx +24 -36
- package/src/streamdown-i18n.test.tsx +24 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/test-results.tsx +47 -27
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +73 -59
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.test.tsx +65 -0
- package/src/tool.tsx +73 -30
- package/src/transcription.tsx +1 -1
- package/src/turn-status.stories.tsx +124 -0
- package/src/turn-status.test.tsx +74 -0
- package/src/turn-status.tsx +174 -0
- package/src/use-audio-level.ts +104 -0
- package/src/voice-selector.tsx +5 -5
- package/src/web-preview.test.tsx +51 -1
- package/src/web-preview.tsx +46 -10
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
- package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
- package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
- package/src/_interactive-terminal-xterm.ts +0 -32
- package/src/blocks-ai-composer.stories.tsx +0 -83
- package/src/context.stories.tsx +0 -16
- package/src/interactive-terminal.stories.tsx +0 -165
- package/src/interactive-terminal.test.tsx +0 -448
- package/src/interactive-terminal.tsx +0 -444
- package/src/model-selector.test.tsx +0 -55
- package/src/model-selector.tsx +0 -225
- package/src/terminal.tsx +0 -244
|
@@ -4,18 +4,10 @@ import { Badge } from "@elabs-ai/components-ui";
|
|
|
4
4
|
import { Button } from "@elabs-ai/components-ui";
|
|
5
5
|
import { Switch } from "@elabs-ai/components-ui";
|
|
6
6
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
7
|
-
import { useLocale } from "@elabs-ai/components-ui";
|
|
7
|
+
import { useCopyToClipboard, useLocale } from "@elabs-ai/components-ui";
|
|
8
8
|
import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "lucide-react";
|
|
9
9
|
import type { ComponentProps, HTMLAttributes } from "react";
|
|
10
|
-
import {
|
|
11
|
-
createContext,
|
|
12
|
-
useCallback,
|
|
13
|
-
useContext,
|
|
14
|
-
useEffect,
|
|
15
|
-
useMemo,
|
|
16
|
-
useRef,
|
|
17
|
-
useState,
|
|
18
|
-
} from "react";
|
|
10
|
+
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
19
11
|
|
|
20
12
|
interface EnvironmentVariablesContextType {
|
|
21
13
|
showValues: boolean;
|
|
@@ -86,7 +78,7 @@ export const EnvironmentVariablesTitle = ({
|
|
|
86
78
|
children,
|
|
87
79
|
...props
|
|
88
80
|
}: EnvironmentVariablesTitleProps) => (
|
|
89
|
-
<h3 className={cn("font-medium text-
|
|
81
|
+
<h3 className={cn("font-medium text-body", className)} {...props}>
|
|
90
82
|
{children ?? "Environment Variables"}
|
|
91
83
|
</h3>
|
|
92
84
|
);
|
|
@@ -102,7 +94,7 @@ export const EnvironmentVariablesToggle = ({
|
|
|
102
94
|
|
|
103
95
|
return (
|
|
104
96
|
<div className={cn("flex items-center gap-2", className)}>
|
|
105
|
-
<span className="text-muted-foreground text-
|
|
97
|
+
<span className="text-muted-foreground text-meta">
|
|
106
98
|
{showValues ? <EyeIcon size={14} /> : <EyeOffIcon size={14} />}
|
|
107
99
|
</span>
|
|
108
100
|
<Switch
|
|
@@ -159,7 +151,7 @@ export const EnvironmentVariableName = ({
|
|
|
159
151
|
const { name } = useContext(EnvironmentVariableContext);
|
|
160
152
|
|
|
161
153
|
return (
|
|
162
|
-
<span className={cn("font-mono text-
|
|
154
|
+
<span className={cn("font-mono text-body", className)} {...props}>
|
|
163
155
|
{children ?? name}
|
|
164
156
|
</span>
|
|
165
157
|
);
|
|
@@ -180,7 +172,7 @@ export const EnvironmentVariableValue = ({
|
|
|
180
172
|
return (
|
|
181
173
|
<span
|
|
182
174
|
className={cn(
|
|
183
|
-
"font-mono text-muted-foreground text-
|
|
175
|
+
"font-mono text-muted-foreground text-body",
|
|
184
176
|
!showValues && "select-none",
|
|
185
177
|
className,
|
|
186
178
|
)}
|
|
@@ -234,15 +226,19 @@ export type EnvironmentVariableCopyButtonProps = ComponentProps<typeof Button> &
|
|
|
234
226
|
export const EnvironmentVariableCopyButton = ({
|
|
235
227
|
onCopy,
|
|
236
228
|
onError,
|
|
237
|
-
timeout
|
|
229
|
+
timeout,
|
|
238
230
|
copyFormat = "value",
|
|
239
231
|
children,
|
|
240
232
|
className,
|
|
233
|
+
"aria-label": ariaLabel,
|
|
241
234
|
...props
|
|
242
235
|
}: EnvironmentVariableCopyButtonProps) => {
|
|
243
|
-
const
|
|
244
|
-
const timeoutRef = useRef<number>(0);
|
|
236
|
+
const { t } = useLocale();
|
|
245
237
|
const { name, value } = useContext(EnvironmentVariableContext);
|
|
238
|
+
// Shared implementation (`@elabs-ai/components-ui`) instead of a private
|
|
239
|
+
// copy of the same copy-to-clipboard state machine (issue-workflow.md
|
|
240
|
+
// dedupe finding) — see `CodeBlockCopyButton` for the reference usage.
|
|
241
|
+
const { copied: isCopied, copy } = useCopyToClipboard({ resetAfterMs: timeout ?? 2000 });
|
|
246
242
|
|
|
247
243
|
const getTextToCopy = useCallback((): string => {
|
|
248
244
|
const formatMap = {
|
|
@@ -254,32 +250,19 @@ export const EnvironmentVariableCopyButton = ({
|
|
|
254
250
|
}, [name, value, copyFormat]);
|
|
255
251
|
|
|
256
252
|
const copyToClipboard = useCallback(async () => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
await navigator.clipboard.writeText(getTextToCopy());
|
|
264
|
-
setIsCopied(true);
|
|
253
|
+
const ok = await copy(getTextToCopy());
|
|
254
|
+
if (ok) {
|
|
265
255
|
onCopy?.();
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
onError?.(error as Error);
|
|
256
|
+
} else {
|
|
257
|
+
onError?.(new Error("Clipboard API not available"));
|
|
269
258
|
}
|
|
270
|
-
}, [getTextToCopy, onCopy, onError
|
|
271
|
-
|
|
272
|
-
useEffect(
|
|
273
|
-
() => () => {
|
|
274
|
-
window.clearTimeout(timeoutRef.current);
|
|
275
|
-
},
|
|
276
|
-
[],
|
|
277
|
-
);
|
|
259
|
+
}, [copy, getTextToCopy, onCopy, onError]);
|
|
278
260
|
|
|
279
261
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
280
262
|
|
|
281
263
|
return (
|
|
282
264
|
<Button
|
|
265
|
+
aria-label={ariaLabel ?? t("copy")}
|
|
283
266
|
className={cn("size-6 shrink-0", className)}
|
|
284
267
|
onClick={copyToClipboard}
|
|
285
268
|
size="icon"
|
|
@@ -298,7 +281,7 @@ export const EnvironmentVariableRequired = ({
|
|
|
298
281
|
children,
|
|
299
282
|
...props
|
|
300
283
|
}: EnvironmentVariableRequiredProps) => (
|
|
301
|
-
<Badge className={cn("text-
|
|
284
|
+
<Badge className={cn("text-meta", className)} variant="secondary" {...props}>
|
|
302
285
|
{children ?? "Required"}
|
|
303
286
|
</Badge>
|
|
304
287
|
);
|
package/src/file-tree.test.tsx
CHANGED
|
@@ -25,6 +25,27 @@ describe("FileTree folder icons", () => {
|
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
describe("FileTreeFolder expand/collapse control (accessibility.md — icon-only controls need aria-label)", () => {
|
|
29
|
+
it("names the chevron toggle, and flips the name with expanded state", async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(
|
|
32
|
+
<FileTree defaultExpanded={new Set()}>
|
|
33
|
+
<FileTreeFolder path="src" name="src">
|
|
34
|
+
<FileTreeFile path="src/index.ts" name="index.ts" />
|
|
35
|
+
</FileTreeFolder>
|
|
36
|
+
</FileTree>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const toggle = screen.getByRole("button", { name: "Expand src" });
|
|
40
|
+
expect(toggle).toBeInTheDocument();
|
|
41
|
+
|
|
42
|
+
await user.click(toggle);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByRole("button", { name: "Collapse src" })).toBeInTheDocument();
|
|
45
|
+
expect(screen.queryByRole("button", { name: "Expand src" })).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
28
49
|
describe("FileTree variant axis (#193, research 04 §4 ASSET-1)", () => {
|
|
29
50
|
it("keeps the IDE look by default (code variant — non-breaking)", () => {
|
|
30
51
|
const { container } = render(
|
package/src/file-tree.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CollapsibleContent,
|
|
6
6
|
CollapsibleTrigger,
|
|
7
7
|
fileIconFor,
|
|
8
|
+
useLocale,
|
|
8
9
|
} from "@elabs-ai/components-ui";
|
|
9
10
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
10
11
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
@@ -50,7 +51,7 @@ const FileTreeContext = createContext<FileTreeContextType>({
|
|
|
50
51
|
const fileTreeVariants = cva("", {
|
|
51
52
|
variants: {
|
|
52
53
|
variant: {
|
|
53
|
-
code: "rounded-lg border bg-background font-mono text-
|
|
54
|
+
code: "rounded-lg border bg-background font-mono text-body",
|
|
54
55
|
document: "text-body",
|
|
55
56
|
},
|
|
56
57
|
},
|
|
@@ -158,6 +159,7 @@ export const FileTreeFolder = ({
|
|
|
158
159
|
children,
|
|
159
160
|
...props
|
|
160
161
|
}: FileTreeFolderProps) => {
|
|
162
|
+
const { t } = useLocale();
|
|
161
163
|
const { expandedPaths, togglePath, selectedPath, onSelect } = useContext(FileTreeContext);
|
|
162
164
|
const isExpanded = expandedPaths.has(path);
|
|
163
165
|
const isSelected = selectedPath === path;
|
|
@@ -184,10 +186,15 @@ export const FileTreeFolder = ({
|
|
|
184
186
|
>
|
|
185
187
|
<CollapsibleTrigger asChild>
|
|
186
188
|
<button
|
|
189
|
+
aria-label={t(
|
|
190
|
+
isExpanded ? "ai.fileTree.collapseFolder" : "ai.fileTree.expandFolder",
|
|
191
|
+
{ name },
|
|
192
|
+
)}
|
|
187
193
|
className="flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0"
|
|
188
194
|
type="button"
|
|
189
195
|
>
|
|
190
196
|
<ChevronRightIcon
|
|
197
|
+
aria-hidden="true"
|
|
191
198
|
className={cn(
|
|
192
199
|
"size-4 shrink-0 text-muted-foreground transition-transform",
|
|
193
200
|
isExpanded && "rotate-90",
|
|
@@ -353,6 +360,7 @@ export type ProducedAssetTreeProps = Omit<
|
|
|
353
360
|
*/
|
|
354
361
|
export const ProducedAssetTree = forwardRef<HTMLDivElement, ProducedAssetTreeProps>(
|
|
355
362
|
function ProducedAssetTree({ assets, selectedId, onSelect, ...props }, ref) {
|
|
363
|
+
const { t } = useLocale();
|
|
356
364
|
const assetsById = useMemo(() => new Map(assets.map((asset) => [asset.id, asset])), [assets]);
|
|
357
365
|
const handleSelect = useCallback(
|
|
358
366
|
(path: string) => {
|
|
@@ -370,7 +378,9 @@ export const ProducedAssetTree = forwardRef<HTMLDivElement, ProducedAssetTreePro
|
|
|
370
378
|
{...props}
|
|
371
379
|
>
|
|
372
380
|
{assets.length === 0 ? (
|
|
373
|
-
<p className="px-2 py-1 text-body text-muted-foreground">
|
|
381
|
+
<p className="px-2 py-1 text-body text-muted-foreground">
|
|
382
|
+
{t("ai.fileTree.noAssetsProduced")}
|
|
383
|
+
</p>
|
|
374
384
|
) : (
|
|
375
385
|
assets.map((asset) => {
|
|
376
386
|
const Icon = assetIcon(asset);
|
package/src/gallery.tsx
CHANGED
|
@@ -317,10 +317,11 @@ function GalleryTilt({ className, children }: { className?: string; children: Re
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
function GalleryEmpty() {
|
|
320
|
+
const { t } = useLocale();
|
|
320
321
|
return (
|
|
321
322
|
<div className="flex min-h-32 flex-col items-center justify-center gap-2 p-6 text-center text-muted-foreground">
|
|
322
323
|
<ImageOff className="size-6" aria-hidden="true" />
|
|
323
|
-
<p className="text-body">
|
|
324
|
+
<p className="text-body">{t("ai.gallery.noImages")}</p>
|
|
324
325
|
</div>
|
|
325
326
|
);
|
|
326
327
|
}
|
|
@@ -485,6 +486,7 @@ function GalleryDetailPanel({
|
|
|
485
486
|
index: number;
|
|
486
487
|
renderMeta?: GalleryProps["renderMeta"];
|
|
487
488
|
}) {
|
|
489
|
+
const { t } = useLocale();
|
|
488
490
|
const custom = renderMeta?.(image, index);
|
|
489
491
|
if (custom != null) {
|
|
490
492
|
return <div className="flex flex-col gap-4">{custom}</div>;
|
|
@@ -494,7 +496,7 @@ function GalleryDetailPanel({
|
|
|
494
496
|
const hasBody = Boolean(image.description) || hasMeta;
|
|
495
497
|
|
|
496
498
|
if (!hasBody) {
|
|
497
|
-
return <p className="text-body text-muted-foreground">
|
|
499
|
+
return <p className="text-body text-muted-foreground">{t("ai.gallery.noDetails")}</p>;
|
|
498
500
|
}
|
|
499
501
|
|
|
500
502
|
return (
|
package/src/index.ts
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
* See ATTRIBUTION.md and .claude/rules/attribution.md.
|
|
21
21
|
*/
|
|
22
22
|
export * from "./agent";
|
|
23
|
+
export * from "./agent-event";
|
|
23
24
|
export * from "./agent-timeline";
|
|
24
25
|
export * from "./artifact";
|
|
25
26
|
export * from "./asset-preview";
|
|
26
27
|
export * from "./attachments";
|
|
27
28
|
export * from "./audio-player";
|
|
29
|
+
export * from "./audio-visualizer";
|
|
28
30
|
export * from "./canvas";
|
|
29
31
|
export * from "./chain-of-thought";
|
|
30
32
|
export * from "./chat-greeting";
|
|
@@ -34,10 +36,10 @@ export * from "./commit";
|
|
|
34
36
|
export * from "./composer";
|
|
35
37
|
export * from "./confirmation";
|
|
36
38
|
export * from "./connection";
|
|
37
|
-
export * from "./context";
|
|
38
39
|
export * from "./context-panel";
|
|
39
40
|
export * from "./controls";
|
|
40
41
|
export * from "./conversation";
|
|
42
|
+
export * from "./diff-view";
|
|
41
43
|
export * from "./edge";
|
|
42
44
|
export * from "./environment-variables";
|
|
43
45
|
export * from "./file-tree";
|
|
@@ -45,13 +47,13 @@ export * from "./gallery";
|
|
|
45
47
|
export * from "./grouped-parts";
|
|
46
48
|
export * from "./image";
|
|
47
49
|
export * from "./inline-citation";
|
|
48
|
-
export * from "./interactive-terminal";
|
|
49
50
|
export * from "./jsx-preview";
|
|
50
51
|
// Mermaid loads on first diagram render, not in the entry chunk. `preloadMermaid`
|
|
51
52
|
// warms it early when a surface is known to render diagrams.
|
|
52
53
|
export { createLazyMermaidPlugin, lazyMermaid, preloadMermaid } from "./_lazy-mermaid";
|
|
53
54
|
export * from "./markdown-view";
|
|
54
55
|
export * from "./message";
|
|
56
|
+
export * from "./message-compare";
|
|
55
57
|
export * from "./message-edit";
|
|
56
58
|
export * from "./message-feedback";
|
|
57
59
|
export * from "./message-form";
|
|
@@ -59,21 +61,27 @@ export * from "./message-form-spec";
|
|
|
59
61
|
export * from "./message-table";
|
|
60
62
|
export * from "./message-table-spec";
|
|
61
63
|
export * from "./mic-selector";
|
|
62
|
-
export * from "./model-
|
|
64
|
+
export * from "./model-provider-logo";
|
|
63
65
|
export * from "./motion-config";
|
|
64
66
|
export * from "./node";
|
|
65
67
|
export * from "./open-in-chat";
|
|
66
68
|
export * from "./package-info";
|
|
67
69
|
export * from "./panel";
|
|
68
70
|
export * from "./part-groups";
|
|
71
|
+
export * from "./permission-mode-select";
|
|
69
72
|
export * from "./persona";
|
|
70
73
|
export * from "./plan";
|
|
71
74
|
export * from "./prompt-input";
|
|
75
|
+
export * from "./prompt-input-effort";
|
|
76
|
+
export * from "./prompt-input-mode";
|
|
77
|
+
export * from "./prompt-input-slash";
|
|
72
78
|
export * from "./queue";
|
|
73
79
|
export * from "./reasoning";
|
|
74
80
|
export * from "./sandbox";
|
|
75
81
|
export * from "./schema-display";
|
|
76
82
|
export * from "./selection-toolbar";
|
|
83
|
+
export * from "./session-header";
|
|
84
|
+
export * from "./session-status-bar";
|
|
77
85
|
export * from "./shimmer";
|
|
78
86
|
export * from "./snippet";
|
|
79
87
|
export * from "./sources";
|
|
@@ -81,12 +89,14 @@ export * from "./speech-input";
|
|
|
81
89
|
export * from "./stack-trace";
|
|
82
90
|
export * from "./suggestion";
|
|
83
91
|
export * from "./task";
|
|
84
|
-
export * from "./terminal";
|
|
85
92
|
export * from "./test-results";
|
|
93
|
+
export * from "./token-usage";
|
|
86
94
|
export * from "./tool";
|
|
87
95
|
export * from "./tool-result-card";
|
|
88
96
|
export * from "./toolbar";
|
|
89
97
|
export * from "./transcription";
|
|
98
|
+
export * from "./turn-status";
|
|
99
|
+
export * from "./use-audio-level";
|
|
90
100
|
export * from "./voice-selector";
|
|
91
101
|
export * from "./web-preview";
|
|
92
102
|
|
package/src/inline-citation.tsx
CHANGED
|
@@ -208,7 +208,7 @@ export const InlineCitationCarouselIndex = ({
|
|
|
208
208
|
return (
|
|
209
209
|
<div
|
|
210
210
|
className={cn(
|
|
211
|
-
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-
|
|
211
|
+
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-meta",
|
|
212
212
|
className,
|
|
213
213
|
)}
|
|
214
214
|
{...props}
|
|
@@ -289,10 +289,10 @@ export const InlineCitationSource = ({
|
|
|
289
289
|
...props
|
|
290
290
|
}: InlineCitationSourceProps) => (
|
|
291
291
|
<div className={cn("space-y-1", className)} {...props}>
|
|
292
|
-
{title && <h4 className="truncate font-medium text-
|
|
293
|
-
{url && <p className="truncate break-all text-muted-foreground text-
|
|
292
|
+
{title && <h4 className="truncate font-medium text-body leading-tight">{title}</h4>}
|
|
293
|
+
{url && <p className="truncate break-all text-muted-foreground text-meta">{url}</p>}
|
|
294
294
|
{description && (
|
|
295
|
-
<p className="line-clamp-3 text-muted-foreground text-
|
|
295
|
+
<p className="line-clamp-3 text-muted-foreground text-body leading-relaxed">{description}</p>
|
|
296
296
|
)}
|
|
297
297
|
{children}
|
|
298
298
|
</div>
|
|
@@ -306,7 +306,7 @@ export const InlineCitationQuote = ({
|
|
|
306
306
|
...props
|
|
307
307
|
}: InlineCitationQuoteProps) => (
|
|
308
308
|
<blockquote
|
|
309
|
-
className={cn("border-muted border-s-2 ps-3 text-muted-foreground text-
|
|
309
|
+
className={cn("border-muted border-s-2 ps-3 text-muted-foreground text-body italic", className)}
|
|
310
310
|
{...props}
|
|
311
311
|
>
|
|
312
312
|
{children}
|
|
@@ -18,7 +18,7 @@ function Stat({ label, value, delta }: { label: string; value: string; delta?: s
|
|
|
18
18
|
<div className="rounded-lg border bg-card p-4 text-card-foreground">
|
|
19
19
|
<div className="text-meta text-muted-foreground">{label}</div>
|
|
20
20
|
<div className="text-title tabular-nums">{value}</div>
|
|
21
|
-
{delta ? <div className="text-caption text-primary">{delta}</div> : null}
|
|
21
|
+
{delta ? <div className="text-caption text-primary-text">{delta}</div> : null}
|
|
22
22
|
</div>
|
|
23
23
|
);
|
|
24
24
|
}
|
|
@@ -50,7 +50,7 @@ const DASHBOARD =
|
|
|
50
50
|
"</div>";
|
|
51
51
|
|
|
52
52
|
const meta = {
|
|
53
|
-
title: "AI/
|
|
53
|
+
title: "AI/JSXPreview",
|
|
54
54
|
component: JSXPreview,
|
|
55
55
|
parameters: { layout: "padded" },
|
|
56
56
|
} satisfies Meta<typeof JSXPreview>;
|
package/src/jsx-preview.tsx
CHANGED
|
@@ -46,7 +46,23 @@ interface JSXPreviewContextValue {
|
|
|
46
46
|
|
|
47
47
|
const JSXPreviewContext = createContext<JSXPreviewContextValue | null>(null);
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* HTML elements that must never render from agent/model-authored JSX: script
|
|
51
|
+
* execution, style/link/meta/base injection, and embeddable third-party
|
|
52
|
+
* surfaces (iframe/object/embed) or a form that could post data out. Merged
|
|
53
|
+
* with `react-jsx-parser`'s own default (`script`) via `blacklistedTags`.
|
|
54
|
+
*/
|
|
55
|
+
const JSX_PREVIEW_BLACKLISTED_TAGS = [
|
|
56
|
+
"script",
|
|
57
|
+
"style",
|
|
58
|
+
"iframe",
|
|
59
|
+
"form",
|
|
60
|
+
"object",
|
|
61
|
+
"embed",
|
|
62
|
+
"link",
|
|
63
|
+
"meta",
|
|
64
|
+
"base",
|
|
65
|
+
];
|
|
50
66
|
|
|
51
67
|
export const useJSXPreview = () => {
|
|
52
68
|
const context = useContext(JSXPreviewContext);
|
|
@@ -56,37 +72,101 @@ export const useJSXPreview = () => {
|
|
|
56
72
|
return context;
|
|
57
73
|
};
|
|
58
74
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
75
|
+
const TAG_NAME_START = /[a-zA-Z]/;
|
|
76
|
+
const TAG_NAME_CHAR = /[a-zA-Z0-9]/;
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
interface JsxTagMatch {
|
|
79
|
+
tagName: string;
|
|
80
|
+
type: "opening" | "closing" | "self-closing";
|
|
81
|
+
endIndex: number;
|
|
82
|
+
}
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Finds the next complete JSX tag at or after `fromIndex`, tracking brace
|
|
86
|
+
* depth and quoted strings so a `>` inside an attribute expression (e.g.
|
|
87
|
+
* `onClick={() => foo()}`) is never mistaken for the tag's own closing
|
|
88
|
+
* bracket (the previous plain regex did exactly that). Walks the ORIGINAL
|
|
89
|
+
* string by absolute index — no per-iteration substring slicing — so one
|
|
90
|
+
* pass over the whole input is linear.
|
|
91
|
+
*/
|
|
92
|
+
function findNextJsxTag(code: string, fromIndex: number): JsxTagMatch | null {
|
|
93
|
+
const len = code.length;
|
|
94
|
+
let i = fromIndex;
|
|
95
|
+
|
|
96
|
+
while (i < len) {
|
|
97
|
+
if (code[i] !== "<") {
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
69
101
|
|
|
70
|
-
|
|
102
|
+
let j = i + 1;
|
|
103
|
+
const closing = code[j] === "/";
|
|
104
|
+
if (closing) j++;
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
const nameStart = j;
|
|
107
|
+
if (!TAG_NAME_START.test(code[nameStart] ?? "")) {
|
|
108
|
+
i++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
j++;
|
|
112
|
+
while (j < len && TAG_NAME_CHAR.test(code[j] ?? "")) {
|
|
113
|
+
j++;
|
|
114
|
+
}
|
|
115
|
+
const tagName = code.slice(nameStart, j);
|
|
116
|
+
|
|
117
|
+
let depth = 0;
|
|
118
|
+
let quote: string | null = null;
|
|
119
|
+
let k = j;
|
|
120
|
+
let closedAt = -1;
|
|
121
|
+
let selfClosing = false;
|
|
122
|
+
|
|
123
|
+
while (k < len) {
|
|
124
|
+
const ch = code[k];
|
|
125
|
+
if (quote) {
|
|
126
|
+
if (ch === quote) quote = null;
|
|
127
|
+
k++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === '"' || ch === "'") {
|
|
131
|
+
quote = ch;
|
|
132
|
+
k++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "{") {
|
|
136
|
+
depth++;
|
|
137
|
+
k++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === "}") {
|
|
141
|
+
depth = Math.max(0, depth - 1);
|
|
142
|
+
k++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (depth === 0 && ch === ">") {
|
|
146
|
+
let p = k - 1;
|
|
147
|
+
while (p > j && code[p] === " ") p--;
|
|
148
|
+
selfClosing = code[p] === "/";
|
|
149
|
+
closedAt = k + 1;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
k++;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// No closing '>' yet for this tag at this depth — it is incomplete, not
|
|
156
|
+
// a match; the caller treats everything from here on as trailing text.
|
|
157
|
+
if (closedAt === -1) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
endIndex: closedAt,
|
|
163
|
+
tagName,
|
|
164
|
+
type: closing ? "closing" : selfClosing ? "self-closing" : "opening",
|
|
165
|
+
};
|
|
79
166
|
}
|
|
80
167
|
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
endIndex: match.index + fullMatch.length,
|
|
84
|
-
startIndex: match.index,
|
|
85
|
-
tag: fullMatch,
|
|
86
|
-
tagName,
|
|
87
|
-
type,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
90
170
|
|
|
91
171
|
const stripIncompleteTag = (text: string) => {
|
|
92
172
|
// Find the last '<' that isn't part of a complete tag
|
|
@@ -106,32 +186,28 @@ const stripIncompleteTag = (text: string) => {
|
|
|
106
186
|
|
|
107
187
|
const completeJsxTag = (code: string) => {
|
|
108
188
|
const stack: string[] = [];
|
|
109
|
-
let
|
|
110
|
-
let currentPosition = 0;
|
|
189
|
+
let position = 0;
|
|
111
190
|
|
|
112
|
-
while (
|
|
113
|
-
const match =
|
|
191
|
+
while (position < code.length) {
|
|
192
|
+
const match = findNextJsxTag(code, position);
|
|
114
193
|
if (!match) {
|
|
115
|
-
// No more tags
|
|
116
|
-
|
|
194
|
+
// No more complete tags ahead; everything past `position` is trailing
|
|
195
|
+
// text (possibly a mid-typed tag), handled by stripIncompleteTag below.
|
|
117
196
|
break;
|
|
118
197
|
}
|
|
119
|
-
const { tagName, type, endIndex } = match;
|
|
120
198
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (type === "opening") {
|
|
125
|
-
stack.push(tagName);
|
|
126
|
-
} else if (type === "closing") {
|
|
199
|
+
if (match.type === "opening") {
|
|
200
|
+
stack.push(match.tagName);
|
|
201
|
+
} else if (match.type === "closing") {
|
|
127
202
|
stack.pop();
|
|
128
203
|
}
|
|
129
204
|
|
|
130
|
-
|
|
205
|
+
position = match.endIndex;
|
|
131
206
|
}
|
|
132
207
|
|
|
133
208
|
return (
|
|
134
|
-
|
|
209
|
+
code.slice(0, position) +
|
|
210
|
+
stripIncompleteTag(code.slice(position)) +
|
|
135
211
|
[...stack]
|
|
136
212
|
.reverse()
|
|
137
213
|
.map((tag) => `</${tag}>`)
|
|
@@ -139,6 +215,35 @@ const completeJsxTag = (code: string) => {
|
|
|
139
215
|
);
|
|
140
216
|
};
|
|
141
217
|
|
|
218
|
+
/** How often `completeJsxTag` may re-run while content is still streaming in. */
|
|
219
|
+
const STREAM_RECOMPUTE_THROTTLE_MS = 80;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Tag-balances `jsx` like `completeJsxTag`, but while `isStreaming` is true it
|
|
223
|
+
* skips a fresh (expensive, whole-string) recompute more often than once per
|
|
224
|
+
* `STREAM_RECOMPUTE_THROTTLE_MS` — otherwise every incoming token re-scans the
|
|
225
|
+
* entire accumulated string from scratch. Settled input (`isStreaming` false)
|
|
226
|
+
* always recomputes immediately so the final render is exact.
|
|
227
|
+
*/
|
|
228
|
+
function useCompletedJsx(jsx: string, isStreaming: boolean): string {
|
|
229
|
+
const cacheRef = useRef({ jsx, result: completeJsxTag(jsx), time: 0 });
|
|
230
|
+
|
|
231
|
+
return useMemo(() => {
|
|
232
|
+
const cache = cacheRef.current;
|
|
233
|
+
if (jsx === cache.jsx) return cache.result;
|
|
234
|
+
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (isStreaming && now - cache.time < STREAM_RECOMPUTE_THROTTLE_MS) {
|
|
237
|
+
return cache.result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = completeJsxTag(jsx);
|
|
241
|
+
cacheRef.current = { jsx, result, time: now };
|
|
242
|
+
return result;
|
|
243
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- cacheRef is a stable ref, not reactive state
|
|
244
|
+
}, [jsx, isStreaming]);
|
|
245
|
+
}
|
|
246
|
+
|
|
142
247
|
export type JSXPreviewProps = ComponentProps<"div"> & {
|
|
143
248
|
jsx: string;
|
|
144
249
|
/** Content is arriving incrementally — build up, suppress transient errors. */
|
|
@@ -177,8 +282,9 @@ export const JSXPreview = memo(
|
|
|
177
282
|
|
|
178
283
|
// The tag-balanced best-effort of the current input. When it differs from the
|
|
179
284
|
// raw jsx, the input ends mid-tag → it is INCOMPLETE (still streaming / being
|
|
180
|
-
// typed), not invalid. This is the timer-free "not-ready" signal.
|
|
181
|
-
|
|
285
|
+
// typed), not invalid. This is the timer-free "not-ready" signal. Throttled
|
|
286
|
+
// while streaming so every incoming token doesn't re-scan the whole string.
|
|
287
|
+
const completed = useCompletedJsx(jsx, isStreaming);
|
|
182
288
|
const isIncomplete = jsx.trim() !== "" && completed !== jsx;
|
|
183
289
|
|
|
184
290
|
const processedJsx = useMemo(() => {
|
|
@@ -293,7 +399,9 @@ export const JSXPreviewContent = memo(({ className, ...props }: JSXPreviewConten
|
|
|
293
399
|
return (
|
|
294
400
|
<div className={cn("jsx-preview-content", className)} {...props}>
|
|
295
401
|
<JsxParser
|
|
402
|
+
allowUnknownElements={false}
|
|
296
403
|
bindings={bindings}
|
|
404
|
+
blacklistedTags={JSX_PREVIEW_BLACKLISTED_TAGS}
|
|
297
405
|
components={components}
|
|
298
406
|
jsx={displayJsx}
|
|
299
407
|
onError={handleError}
|