@elabs-ai/components-ai 4.0.0 → 4.1.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 +61 -11
- 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 +1109 -202
- package/dist/index.js +4867 -2878
- package/dist/index.js.map +1 -1
- package/package.json +31 -16
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-engine-boundary.tsx +61 -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 +21 -1
- package/src/_streamdown-safety.ts +170 -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 +28 -14
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +15 -12
- 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/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 +263 -3
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +2 -1
- package/src/conversation.stories.tsx +63 -2
- package/src/conversation.test.tsx +13 -0
- package/src/conversation.tsx +20 -2
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +188 -0
- package/src/diff-view.tsx +642 -0
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +232 -1
- package/src/markdown-view.tsx +150 -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 +29 -1
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +176 -0
- package/src/message.tsx +90 -4
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/{model-selector.test.tsx → model-provider-logo.test.tsx} +10 -10
- package/src/model-provider-logo.tsx +149 -0
- 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/reasoning.tsx +27 -13
- package/src/sandbox.stories.tsx +9 -1
- package/src/schema-display.tsx +5 -2
- 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/stack-trace.tsx +6 -2
- package/src/streamdown-i18n.test.tsx +1 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +67 -53
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.tsx +15 -8
- 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/web-preview.tsx +8 -3
- 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.tsx +0 -225
- package/src/terminal.tsx +0 -244
|
@@ -18,50 +18,56 @@ const ICON_STROKE_WIDTH = 2;
|
|
|
18
18
|
|
|
19
19
|
type ModelId = string;
|
|
20
20
|
|
|
21
|
-
interface
|
|
21
|
+
interface TokenUsageSchema {
|
|
22
22
|
usedTokens: number;
|
|
23
23
|
maxTokens: number;
|
|
24
24
|
usage?: LanguageModelUsage;
|
|
25
25
|
modelId?: ModelId;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const TokenUsageContext = createContext<TokenUsageSchema | null>(null);
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const context = useContext(
|
|
30
|
+
const useTokenUsageValue = () => {
|
|
31
|
+
const context = useContext(TokenUsageContext);
|
|
32
32
|
|
|
33
33
|
if (!context) {
|
|
34
|
-
throw new Error("
|
|
34
|
+
throw new Error("TokenUsage components must be used within TokenUsage");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
return context;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export type
|
|
40
|
+
export type TokenUsageProps = ComponentProps<typeof HoverCard> & TokenUsageSchema;
|
|
41
41
|
|
|
42
|
-
export const
|
|
42
|
+
export const TokenUsage = ({
|
|
43
|
+
usedTokens,
|
|
44
|
+
maxTokens,
|
|
45
|
+
usage,
|
|
46
|
+
modelId,
|
|
47
|
+
...props
|
|
48
|
+
}: TokenUsageProps) => {
|
|
43
49
|
const contextValue = useMemo(
|
|
44
50
|
() => ({ maxTokens, modelId, usage, usedTokens }),
|
|
45
51
|
[maxTokens, modelId, usage, usedTokens],
|
|
46
52
|
);
|
|
47
53
|
|
|
48
54
|
return (
|
|
49
|
-
<
|
|
55
|
+
<TokenUsageContext.Provider value={contextValue}>
|
|
50
56
|
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
51
|
-
</
|
|
57
|
+
</TokenUsageContext.Provider>
|
|
52
58
|
);
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
const
|
|
61
|
+
const TokenUsageIcon = () => {
|
|
56
62
|
const { t } = useLocale();
|
|
57
|
-
const { usedTokens, maxTokens } =
|
|
63
|
+
const { usedTokens, maxTokens } = useTokenUsageValue();
|
|
58
64
|
const circumference = 2 * Math.PI * ICON_RADIUS;
|
|
59
65
|
const usedPercent = usedTokens / maxTokens;
|
|
60
66
|
const dashOffset = circumference * (1 - usedPercent);
|
|
61
67
|
|
|
62
68
|
return (
|
|
63
69
|
<svg
|
|
64
|
-
aria-label={t("ai.
|
|
70
|
+
aria-label={t("ai.tokenUsage.usage")}
|
|
65
71
|
height="20"
|
|
66
72
|
role="img"
|
|
67
73
|
style={{ color: "currentcolor" }}
|
|
@@ -94,11 +100,11 @@ const ContextIcon = () => {
|
|
|
94
100
|
);
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
export type
|
|
103
|
+
export type TokenUsageTriggerProps = ComponentProps<typeof Button>;
|
|
98
104
|
|
|
99
|
-
export const
|
|
105
|
+
export const TokenUsageTrigger = ({ children, ...props }: TokenUsageTriggerProps) => {
|
|
100
106
|
const { formatNumber } = useLocale();
|
|
101
|
-
const { usedTokens, maxTokens } =
|
|
107
|
+
const { usedTokens, maxTokens } = useTokenUsageValue();
|
|
102
108
|
const usedPercent = usedTokens / maxTokens;
|
|
103
109
|
const renderedPercent = formatNumber(usedPercent, {
|
|
104
110
|
maximumFractionDigits: 1,
|
|
@@ -110,28 +116,28 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
|
|
110
116
|
{children ?? (
|
|
111
117
|
<Button type="button" variant="ghost" {...props}>
|
|
112
118
|
<span className="font-medium text-muted-foreground">{renderedPercent}</span>
|
|
113
|
-
<
|
|
119
|
+
<TokenUsageIcon />
|
|
114
120
|
</Button>
|
|
115
121
|
)}
|
|
116
122
|
</HoverCardTrigger>
|
|
117
123
|
);
|
|
118
124
|
};
|
|
119
125
|
|
|
120
|
-
export type
|
|
126
|
+
export type TokenUsageContentProps = ComponentProps<typeof HoverCardContent>;
|
|
121
127
|
|
|
122
|
-
export const
|
|
128
|
+
export const TokenUsageContent = ({ className, ...props }: TokenUsageContentProps) => (
|
|
123
129
|
<HoverCardContent className={cn("min-w-60 divide-y overflow-hidden p-0", className)} {...props} />
|
|
124
130
|
);
|
|
125
131
|
|
|
126
|
-
export type
|
|
132
|
+
export type TokenUsageContentHeaderProps = ComponentProps<"div">;
|
|
127
133
|
|
|
128
|
-
export const
|
|
134
|
+
export const TokenUsageContentHeader = ({
|
|
129
135
|
children,
|
|
130
136
|
className,
|
|
131
137
|
...props
|
|
132
|
-
}:
|
|
138
|
+
}: TokenUsageContentHeaderProps) => {
|
|
133
139
|
const { formatNumber } = useLocale();
|
|
134
|
-
const { usedTokens, maxTokens } =
|
|
140
|
+
const { usedTokens, maxTokens } = useTokenUsageValue();
|
|
135
141
|
const usedPercent = usedTokens / maxTokens;
|
|
136
142
|
const displayPct = formatNumber(usedPercent, {
|
|
137
143
|
maximumFractionDigits: 1,
|
|
@@ -163,23 +169,27 @@ export const ContextContentHeader = ({
|
|
|
163
169
|
);
|
|
164
170
|
};
|
|
165
171
|
|
|
166
|
-
export type
|
|
172
|
+
export type TokenUsageContentBodyProps = ComponentProps<"div">;
|
|
167
173
|
|
|
168
|
-
export const
|
|
174
|
+
export const TokenUsageContentBody = ({
|
|
175
|
+
children,
|
|
176
|
+
className,
|
|
177
|
+
...props
|
|
178
|
+
}: TokenUsageContentBodyProps) => (
|
|
169
179
|
<div className={cn("w-full p-3", className)} {...props}>
|
|
170
180
|
{children}
|
|
171
181
|
</div>
|
|
172
182
|
);
|
|
173
183
|
|
|
174
|
-
export type
|
|
184
|
+
export type TokenUsageContentFooterProps = ComponentProps<"div">;
|
|
175
185
|
|
|
176
|
-
export const
|
|
186
|
+
export const TokenUsageContentFooter = ({
|
|
177
187
|
children,
|
|
178
188
|
className,
|
|
179
189
|
...props
|
|
180
|
-
}:
|
|
181
|
-
const { formatNumber } = useLocale();
|
|
182
|
-
const { modelId, usage } =
|
|
190
|
+
}: TokenUsageContentFooterProps) => {
|
|
191
|
+
const { formatNumber, t } = useLocale();
|
|
192
|
+
const { modelId, usage } = useTokenUsageValue();
|
|
183
193
|
const costUSD = modelId
|
|
184
194
|
? getUsage({
|
|
185
195
|
modelId,
|
|
@@ -204,7 +214,7 @@ export const ContextContentFooter = ({
|
|
|
204
214
|
>
|
|
205
215
|
{children ?? (
|
|
206
216
|
<>
|
|
207
|
-
<span className="text-muted-foreground">
|
|
217
|
+
<span className="text-muted-foreground">{t("ai.tokenUsage.totalCost")}</span>
|
|
208
218
|
<span>{totalCost}</span>
|
|
209
219
|
</>
|
|
210
220
|
)}
|
|
@@ -222,11 +232,11 @@ const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: stri
|
|
|
222
232
|
);
|
|
223
233
|
};
|
|
224
234
|
|
|
225
|
-
export type
|
|
235
|
+
export type TokenUsageInputProps = ComponentProps<"div">;
|
|
226
236
|
|
|
227
|
-
export const
|
|
228
|
-
const { formatNumber } = useLocale();
|
|
229
|
-
const { usage, modelId } =
|
|
237
|
+
export const TokenUsageInput = ({ className, children, ...props }: TokenUsageInputProps) => {
|
|
238
|
+
const { formatNumber, t } = useLocale();
|
|
239
|
+
const { usage, modelId } = useTokenUsageValue();
|
|
230
240
|
const inputTokens = usage?.inputTokens ?? 0;
|
|
231
241
|
|
|
232
242
|
if (children) {
|
|
@@ -250,17 +260,17 @@ export const ContextInputUsage = ({ className, children, ...props }: ContextInpu
|
|
|
250
260
|
|
|
251
261
|
return (
|
|
252
262
|
<div className={cn("flex items-center justify-between text-xs", className)} {...props}>
|
|
253
|
-
<span className="text-muted-foreground">
|
|
263
|
+
<span className="text-muted-foreground">{t("ai.tokenUsage.input")}</span>
|
|
254
264
|
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
|
|
255
265
|
</div>
|
|
256
266
|
);
|
|
257
267
|
};
|
|
258
268
|
|
|
259
|
-
export type
|
|
269
|
+
export type TokenUsageOutputProps = ComponentProps<"div">;
|
|
260
270
|
|
|
261
|
-
export const
|
|
262
|
-
const { formatNumber } = useLocale();
|
|
263
|
-
const { usage, modelId } =
|
|
271
|
+
export const TokenUsageOutput = ({ className, children, ...props }: TokenUsageOutputProps) => {
|
|
272
|
+
const { formatNumber, t } = useLocale();
|
|
273
|
+
const { usage, modelId } = useTokenUsageValue();
|
|
264
274
|
const outputTokens = usage?.outputTokens ?? 0;
|
|
265
275
|
|
|
266
276
|
if (children) {
|
|
@@ -284,22 +294,24 @@ export const ContextOutputUsage = ({ className, children, ...props }: ContextOut
|
|
|
284
294
|
|
|
285
295
|
return (
|
|
286
296
|
<div className={cn("flex items-center justify-between text-xs", className)} {...props}>
|
|
287
|
-
<span className="text-muted-foreground">
|
|
297
|
+
<span className="text-muted-foreground">{t("ai.tokenUsage.output")}</span>
|
|
288
298
|
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
|
|
289
299
|
</div>
|
|
290
300
|
);
|
|
291
301
|
};
|
|
292
302
|
|
|
293
|
-
export type
|
|
303
|
+
export type TokenUsageReasoningProps = ComponentProps<"div">;
|
|
294
304
|
|
|
295
|
-
export const
|
|
305
|
+
export const TokenUsageReasoning = ({
|
|
296
306
|
className,
|
|
297
307
|
children,
|
|
298
308
|
...props
|
|
299
|
-
}:
|
|
300
|
-
const { formatNumber } = useLocale();
|
|
301
|
-
const { usage, modelId } =
|
|
302
|
-
|
|
309
|
+
}: TokenUsageReasoningProps) => {
|
|
310
|
+
const { formatNumber, t } = useLocale();
|
|
311
|
+
const { usage, modelId } = useTokenUsageValue();
|
|
312
|
+
// `reasoningTokens` moved under `outputTokenDetails` in ai@7 (the flat,
|
|
313
|
+
// deprecated top-level alias from ai@6 was removed).
|
|
314
|
+
const reasoningTokens = usage?.outputTokenDetails?.reasoningTokens ?? 0;
|
|
303
315
|
|
|
304
316
|
if (children) {
|
|
305
317
|
return children;
|
|
@@ -322,18 +334,20 @@ export const ContextReasoningUsage = ({
|
|
|
322
334
|
|
|
323
335
|
return (
|
|
324
336
|
<div className={cn("flex items-center justify-between text-xs", className)} {...props}>
|
|
325
|
-
<span className="text-muted-foreground">
|
|
337
|
+
<span className="text-muted-foreground">{t("ai.tokenUsage.reasoning")}</span>
|
|
326
338
|
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
|
|
327
339
|
</div>
|
|
328
340
|
);
|
|
329
341
|
};
|
|
330
342
|
|
|
331
|
-
export type
|
|
343
|
+
export type TokenUsageCacheProps = ComponentProps<"div">;
|
|
332
344
|
|
|
333
|
-
export const
|
|
334
|
-
const { formatNumber } = useLocale();
|
|
335
|
-
const { usage, modelId } =
|
|
336
|
-
|
|
345
|
+
export const TokenUsageCache = ({ className, children, ...props }: TokenUsageCacheProps) => {
|
|
346
|
+
const { formatNumber, t } = useLocale();
|
|
347
|
+
const { usage, modelId } = useTokenUsageValue();
|
|
348
|
+
// `cachedInputTokens` moved under `inputTokenDetails.cacheReadTokens` in
|
|
349
|
+
// ai@7 (the flat, deprecated top-level alias from ai@6 was removed).
|
|
350
|
+
const cacheTokens = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
337
351
|
|
|
338
352
|
if (children) {
|
|
339
353
|
return children;
|
|
@@ -356,7 +370,7 @@ export const ContextCacheUsage = ({ className, children, ...props }: ContextCach
|
|
|
356
370
|
|
|
357
371
|
return (
|
|
358
372
|
<div className={cn("flex items-center justify-between text-xs", className)} {...props}>
|
|
359
|
-
<span className="text-muted-foreground">
|
|
373
|
+
<span className="text-muted-foreground">{t("ai.tokenUsage.cache")}</span>
|
|
360
374
|
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
|
|
361
375
|
</div>
|
|
362
376
|
);
|
|
@@ -9,7 +9,15 @@ import { ToolResultCard } from "./tool-result-card";
|
|
|
9
9
|
const meta = {
|
|
10
10
|
title: "AI/ToolResultCard",
|
|
11
11
|
component: ToolResultCard,
|
|
12
|
-
parameters: {
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: "padded",
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
"The CHAT produced-artifact card; a console transcript renders the same moment through `Terminal/TerminalToolCall` — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). A separate component from `AI/Tool` by design: `Tool` is the step that RAN (JSON behind disclosure), `ToolResultCard` hosts what it produced — a chart, a table, a file preview — as `children`, on the elevation channel (raised fill, no border).",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
13
21
|
tags: ["autodocs"],
|
|
14
22
|
} satisfies Meta<typeof ToolResultCard>;
|
|
15
23
|
export default meta;
|
package/src/tool.stories.tsx
CHANGED
|
@@ -8,9 +8,19 @@ import {
|
|
|
8
8
|
ToolOutput,
|
|
9
9
|
statusFromToolState,
|
|
10
10
|
} from "./tool";
|
|
11
|
-
const meta = {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "AI/Tool",
|
|
13
|
+
component: Tool,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: "padded",
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
"The CHAT tool-call row; the console skin is `Terminal/TerminalToolCall`, and both speak the same success / error / pending vocabulary — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). `Tool` owns the rail-and-inspect idiom: a header carrying the derived tool name, a `StatusBadge` and a business summary, with the raw payload behind a default-collapsed `ToolDetails`. The artifact a call PRODUCED is `AI/ToolResultCard` instead.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof Tool>;
|
|
14
24
|
export default meta;
|
|
15
25
|
type Story = StoryObj<typeof meta>;
|
|
16
26
|
// JSON-behind-disclosure is the package default (#192, research 10 §B.5): the
|
package/src/tool.tsx
CHANGED
|
@@ -145,14 +145,19 @@ export type ToolInputProps = ComponentProps<"div"> & {
|
|
|
145
145
|
input: ToolPart["input"];
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
export const ToolInput = ({ className, input, ...props }: ToolInputProps) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<div className="
|
|
152
|
-
<
|
|
148
|
+
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => {
|
|
149
|
+
const { t } = useLocale();
|
|
150
|
+
return (
|
|
151
|
+
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
|
|
152
|
+
<h4 className="text-meta uppercase text-muted-foreground">
|
|
153
|
+
{t("ai.schemaDisplay.parameters")}
|
|
154
|
+
</h4>
|
|
155
|
+
<div className="rounded-md bg-muted/50">
|
|
156
|
+
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
|
157
|
+
</div>
|
|
153
158
|
</div>
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
);
|
|
160
|
+
};
|
|
156
161
|
|
|
157
162
|
export type ToolOutputProps = ComponentProps<"div"> & {
|
|
158
163
|
/**
|
|
@@ -223,7 +228,9 @@ export const ToolOutput = ({
|
|
|
223
228
|
<div
|
|
224
229
|
className={cn(
|
|
225
230
|
"overflow-x-auto rounded-md text-caption [&_table]:w-full",
|
|
226
|
-
|
|
231
|
+
// #124: this colours the ambient text of the error/result body —
|
|
232
|
+
// running text, so the error branch takes the ink rung.
|
|
233
|
+
showError ? "bg-destructive/10 text-destructive-text" : "bg-muted/50 text-foreground",
|
|
227
234
|
)}
|
|
228
235
|
>
|
|
229
236
|
{showError && <div>{errorText}</div>}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { TurnStatus } from "./turn-status";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "AI/TurnStatus",
|
|
7
|
+
component: TurnStatus,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "padded",
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"The CHAT in-turn footer; the console skin is `Terminal/TerminalWorking` — see " +
|
|
14
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
15
|
+
"The in-turn footer: label, elapsed time, token counts, turn progress and a stop affordance in one place (#105). Exactly one role=status live region announces the label and the settled state — the ticking elapsed/tokens/turn stats are plain visible text so they never flood assistive tech.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
elapsedMs: 8000,
|
|
21
|
+
label: "Working…",
|
|
22
|
+
status: "working",
|
|
23
|
+
tokens: { input: 1200, output: 340 },
|
|
24
|
+
turn: 2,
|
|
25
|
+
turnTotal: 5,
|
|
26
|
+
},
|
|
27
|
+
tags: ["autodocs"],
|
|
28
|
+
} satisfies Meta<typeof TurnStatus>;
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof meta>;
|
|
32
|
+
|
|
33
|
+
/** A running turn: label, elapsed, tokens, turn progress and a stop button. */
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
onStop: () => undefined,
|
|
37
|
+
},
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
40
|
+
<TurnStatus {...args} />
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** No stop control — just the label and live stats. */
|
|
46
|
+
export const WithoutStop: Story = {
|
|
47
|
+
render: (args) => (
|
|
48
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
49
|
+
<TurnStatus {...args} />
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** A minimal working state — only a label, no stats yet. */
|
|
55
|
+
export const LabelOnly: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
elapsedMs: undefined,
|
|
58
|
+
tokens: undefined,
|
|
59
|
+
turn: undefined,
|
|
60
|
+
turnTotal: undefined,
|
|
61
|
+
},
|
|
62
|
+
render: (args) => (
|
|
63
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
64
|
+
<TurnStatus {...args} />
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The completed-turn line — stats and the stop control disappear; the live region stops announcing further ticks. */
|
|
70
|
+
export const Settled: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
status: "settled",
|
|
73
|
+
},
|
|
74
|
+
render: (args) => (
|
|
75
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
76
|
+
<TurnStatus {...args} />
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Adds a "scroll to bottom" affordance beside the stop control. */
|
|
82
|
+
export const WithScrollToBottom: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
onScrollToBottom: () => undefined,
|
|
85
|
+
onStop: () => undefined,
|
|
86
|
+
showScrollToBottom: true,
|
|
87
|
+
},
|
|
88
|
+
render: (args) => (
|
|
89
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
90
|
+
<TurnStatus {...args} />
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** A live-ticking demo — elapsed time advances every second until Stop settles the turn. */
|
|
96
|
+
export const Live: Story = {
|
|
97
|
+
render: () => {
|
|
98
|
+
const LiveDemo = () => {
|
|
99
|
+
const [elapsedMs, setElapsedMs] = useState(0);
|
|
100
|
+
const [status, setStatus] = useState<"working" | "settled">("working");
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (status !== "working") return;
|
|
104
|
+
const id = setInterval(() => setElapsedMs((ms) => ms + 1000), 1000);
|
|
105
|
+
return () => clearInterval(id);
|
|
106
|
+
}, [status]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="max-w-lg rounded-md border bg-card p-2">
|
|
110
|
+
<TurnStatus
|
|
111
|
+
elapsedMs={elapsedMs}
|
|
112
|
+
label="Editing files…"
|
|
113
|
+
onStop={() => setStatus("settled")}
|
|
114
|
+
status={status}
|
|
115
|
+
tokens={{ input: 800 + elapsedMs, output: 120 + elapsedMs / 2 }}
|
|
116
|
+
turn={1}
|
|
117
|
+
turnTotal={3}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
return <LiveDemo />;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { formatElapsed } from "@elabs-ai/components-ui";
|
|
4
|
+
import { TurnStatus } from "./turn-status";
|
|
5
|
+
|
|
6
|
+
// `formatElapsed` now lives in `@elabs-ai/components-ui` (docs/decisions/
|
|
7
|
+
// 2026-09-01-brainless-adoption-architecture.md § 6.1) — `TurnStatus` imports
|
|
8
|
+
// it rather than declaring it. These tests stay here because `TurnStatus` is
|
|
9
|
+
// the surface whose behaviour they were written to lock.
|
|
10
|
+
describe("formatElapsed", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
// [elapsedMs, expected]
|
|
13
|
+
[0, "0ms"],
|
|
14
|
+
[420, "420ms"],
|
|
15
|
+
[999, "999ms"],
|
|
16
|
+
[1000, "1.0s"],
|
|
17
|
+
[8000, "8.0s"],
|
|
18
|
+
[9949, "9.9s"],
|
|
19
|
+
[10000, "10s"],
|
|
20
|
+
[42000, "42s"],
|
|
21
|
+
[59000, "59s"],
|
|
22
|
+
[60000, "1m00s"],
|
|
23
|
+
[77000, "1m17s"],
|
|
24
|
+
[605000, "10m05s"],
|
|
25
|
+
] as const)("formats %dms as %s", (elapsedMs, expected) => {
|
|
26
|
+
expect(formatElapsed(elapsedMs)).toBe(expected);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("TurnStatus", () => {
|
|
31
|
+
it("renders exactly one live region when label, elapsed, tokens and turn are all supplied", () => {
|
|
32
|
+
render(
|
|
33
|
+
<TurnStatus
|
|
34
|
+
elapsedMs={8000}
|
|
35
|
+
label="Working…"
|
|
36
|
+
tokens={{ input: 120, output: 340 }}
|
|
37
|
+
turn={2}
|
|
38
|
+
turnTotal={5}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const liveRegions = screen.getAllByRole("status");
|
|
43
|
+
expect(liveRegions).toHaveLength(1);
|
|
44
|
+
expect(liveRegions[0]).toHaveTextContent("Working…");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders the elapsed time and does not put it inside the live region", () => {
|
|
48
|
+
render(<TurnStatus elapsedMs={8000} label="Working…" />);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText("8.0s")).toBeInTheDocument();
|
|
51
|
+
const [liveRegion] = screen.getAllByRole("status");
|
|
52
|
+
expect(liveRegion).not.toHaveTextContent("8.0s");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders a focusable stop button while working, reachable via onStop", () => {
|
|
56
|
+
const onStop = () => undefined;
|
|
57
|
+
render(<TurnStatus label="Working…" onStop={onStop} status="working" />);
|
|
58
|
+
|
|
59
|
+
const stop = screen.getByRole("button", { name: "Stop" });
|
|
60
|
+
expect(stop).toBeInTheDocument();
|
|
61
|
+
expect(stop.tagName).toBe("BUTTON");
|
|
62
|
+
stop.focus();
|
|
63
|
+
expect(stop).toHaveFocus();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders the completed-turn line and stops announcing when settled", () => {
|
|
67
|
+
render(<TurnStatus elapsedMs={8000} status="settled" />);
|
|
68
|
+
|
|
69
|
+
expect(screen.queryByRole("button", { name: "Stop" })).not.toBeInTheDocument();
|
|
70
|
+
const liveRegions = screen.getAllByRole("status");
|
|
71
|
+
expect(liveRegions).toHaveLength(1);
|
|
72
|
+
expect(liveRegions[0]).toHaveTextContent(/8\.0s/);
|
|
73
|
+
});
|
|
74
|
+
});
|