@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
|
@@ -9,6 +9,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { AppIcon } from "@elabs-ai/components-icons";
|
|
11
11
|
import {
|
|
12
|
+
Button,
|
|
12
13
|
Sidebar,
|
|
13
14
|
SidebarContent,
|
|
14
15
|
SidebarGroup,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
MessageContent,
|
|
32
33
|
MessageResponse,
|
|
33
34
|
} from "./index";
|
|
34
|
-
import { Bot, History, Home, Settings } from "lucide-react";
|
|
35
|
+
import { Bot, History, Home, MessageSquare, Settings } from "lucide-react";
|
|
35
36
|
|
|
36
37
|
interface ChatMessage {
|
|
37
38
|
id: string;
|
|
@@ -101,8 +102,27 @@ function AiAssistantTemplate() {
|
|
|
101
102
|
<ConversationContent>
|
|
102
103
|
{messages.length === 0 ? (
|
|
103
104
|
<ConversationEmptyState
|
|
105
|
+
icon={<MessageSquare className="size-8" aria-hidden="true" />}
|
|
104
106
|
title="Start the conversation"
|
|
105
107
|
description="Ask anything to begin."
|
|
108
|
+
actions={
|
|
109
|
+
<>
|
|
110
|
+
<Button
|
|
111
|
+
onClick={() => send("What can you help me with?")}
|
|
112
|
+
size="sm"
|
|
113
|
+
variant="outline"
|
|
114
|
+
>
|
|
115
|
+
What can you help me with?
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
onClick={() => send("Summarize my recent activity")}
|
|
119
|
+
size="sm"
|
|
120
|
+
variant="outline"
|
|
121
|
+
>
|
|
122
|
+
Summarize my recent activity
|
|
123
|
+
</Button>
|
|
124
|
+
</>
|
|
125
|
+
}
|
|
106
126
|
/>
|
|
107
127
|
) : (
|
|
108
128
|
messages.map((m) => (
|
package/src/test-results.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { StatusBadge, StatusIcon, type Status } from "@elabs-ai/components-ui";
|
|
3
|
+
import { StatusBadge, StatusIcon, useLocale, type Status } from "@elabs-ai/components-ui";
|
|
4
4
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
|
|
5
5
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
6
6
|
import { ChevronRightIcon } from "lucide-react";
|
|
@@ -64,7 +64,7 @@ export const TestResultsDuration = ({
|
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
66
|
<span
|
|
67
|
-
className={cn("shrink-0 text-muted-foreground text-
|
|
67
|
+
className={cn("shrink-0 text-muted-foreground text-body tabular-nums", className)}
|
|
68
68
|
{...props}
|
|
69
69
|
>
|
|
70
70
|
{children ?? formatDuration(summary.duration)}
|
|
@@ -76,6 +76,7 @@ export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
|
|
|
76
76
|
|
|
77
77
|
export const TestResultsSummary = ({ className, children, ...props }: TestResultsSummaryProps) => {
|
|
78
78
|
const { summary } = useContext(TestResultsContext);
|
|
79
|
+
const { t } = useLocale();
|
|
79
80
|
|
|
80
81
|
if (!summary) {
|
|
81
82
|
return null;
|
|
@@ -86,16 +87,16 @@ export const TestResultsSummary = ({ className, children, ...props }: TestResult
|
|
|
86
87
|
{children ?? (
|
|
87
88
|
<>
|
|
88
89
|
<StatusBadge size="sm" status="complete">
|
|
89
|
-
{summary.passed}
|
|
90
|
+
{t("ai.testResults.passedCount", { count: summary.passed })}
|
|
90
91
|
</StatusBadge>
|
|
91
92
|
{summary.failed > 0 && (
|
|
92
93
|
<StatusBadge size="sm" status="failed">
|
|
93
|
-
{summary.failed}
|
|
94
|
+
{t("ai.testResults.failedCount", { count: summary.failed })}
|
|
94
95
|
</StatusBadge>
|
|
95
96
|
)}
|
|
96
97
|
{summary.skipped > 0 && (
|
|
97
98
|
<StatusBadge size="sm" status="skipped">
|
|
98
|
-
{summary.skipped}
|
|
99
|
+
{t("ai.testResults.skippedCount", { count: summary.skipped })}
|
|
99
100
|
</StatusBadge>
|
|
100
101
|
)}
|
|
101
102
|
</>
|
|
@@ -134,6 +135,7 @@ export const TestResultsProgress = ({
|
|
|
134
135
|
...props
|
|
135
136
|
}: TestResultsProgressProps) => {
|
|
136
137
|
const { summary } = useContext(TestResultsContext);
|
|
138
|
+
const { t, formatNumber } = useLocale();
|
|
137
139
|
|
|
138
140
|
if (!summary) {
|
|
139
141
|
return null;
|
|
@@ -156,11 +158,14 @@ export const TestResultsProgress = ({
|
|
|
156
158
|
style={{ width: `${failedPercent}%` }}
|
|
157
159
|
/>
|
|
158
160
|
</div>
|
|
159
|
-
<div className="flex justify-between text-muted-foreground text-
|
|
161
|
+
<div className="flex justify-between text-muted-foreground text-meta tabular-nums">
|
|
160
162
|
<span>
|
|
161
|
-
{summary.passed
|
|
163
|
+
{t("ai.testResults.testsPassed", { passed: summary.passed, total: summary.total })}
|
|
164
|
+
</span>
|
|
165
|
+
<span>
|
|
166
|
+
{formatNumber(passedPercent, { maximumFractionDigits: 0 })}
|
|
167
|
+
{"%"}
|
|
162
168
|
</span>
|
|
163
|
-
<span>{passedPercent.toFixed(0)}%</span>
|
|
164
169
|
</div>
|
|
165
170
|
</>
|
|
166
171
|
)}
|
|
@@ -247,7 +252,7 @@ export const TestSuiteName = ({ className, children, ...props }: TestSuiteNamePr
|
|
|
247
252
|
>
|
|
248
253
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-fast ease-standard motion-reduce:transition-none group-data-[state=open]:rotate-90" />
|
|
249
254
|
<TestStatusIcon status={status} />
|
|
250
|
-
<span className="min-w-0 flex-1 truncate font-medium text-
|
|
255
|
+
<span className="min-w-0 flex-1 truncate font-medium text-body">{children ?? name}</span>
|
|
251
256
|
</CollapsibleTrigger>
|
|
252
257
|
);
|
|
253
258
|
};
|
|
@@ -265,20 +270,35 @@ export const TestSuiteStats = ({
|
|
|
265
270
|
className,
|
|
266
271
|
children,
|
|
267
272
|
...props
|
|
268
|
-
}: TestSuiteStatsProps) =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
)
|
|
273
|
+
}: TestSuiteStatsProps) => {
|
|
274
|
+
const { t } = useLocale();
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
className={cn("ms-auto flex shrink-0 items-center gap-3 text-meta tabular-nums", className)}
|
|
278
|
+
{...props}
|
|
279
|
+
>
|
|
280
|
+
{children ?? (
|
|
281
|
+
<>
|
|
282
|
+
{passed > 0 && (
|
|
283
|
+
<span className="text-success-text">
|
|
284
|
+
{t("ai.testResults.passedCount", { count: passed })}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
{failed > 0 && (
|
|
288
|
+
<span className="text-destructive-text">
|
|
289
|
+
{t("ai.testResults.failedCount", { count: failed })}
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
{skipped > 0 && (
|
|
293
|
+
<span className="text-muted-foreground">
|
|
294
|
+
{t("ai.testResults.skippedCount", { count: skipped })}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
282
302
|
|
|
283
303
|
export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
284
304
|
|
|
@@ -328,7 +348,7 @@ export const TestDuration = ({ className, children, ...props }: TestDurationProp
|
|
|
328
348
|
|
|
329
349
|
return (
|
|
330
350
|
<span
|
|
331
|
-
className={cn("shrink-0 text-muted-foreground text-
|
|
351
|
+
className={cn("shrink-0 text-muted-foreground text-meta tabular-nums", className)}
|
|
332
352
|
{...props}
|
|
333
353
|
>
|
|
334
354
|
{children ?? `${duration}ms`}
|
|
@@ -365,7 +385,7 @@ export const Test = ({ name, status, duration, className, children, ...props }:
|
|
|
365
385
|
return (
|
|
366
386
|
<TestContext.Provider value={contextValue}>
|
|
367
387
|
<div
|
|
368
|
-
className={cn("flex min-w-0 items-center gap-2 ps-10 pe-4 py-2 text-
|
|
388
|
+
className={cn("flex min-w-0 items-center gap-2 ps-10 pe-4 py-2 text-body", className)}
|
|
369
389
|
{...props}
|
|
370
390
|
>
|
|
371
391
|
{children ?? (
|
|
@@ -391,7 +411,7 @@ export const TestError = ({ className, children, ...props }: TestErrorProps) =>
|
|
|
391
411
|
export type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
|
|
392
412
|
|
|
393
413
|
export const TestErrorMessage = ({ className, children, ...props }: TestErrorMessageProps) => (
|
|
394
|
-
<p className={cn("font-medium text-destructive-text text-
|
|
414
|
+
<p className={cn("font-medium text-destructive-text text-body", className)} {...props}>
|
|
395
415
|
{children}
|
|
396
416
|
</p>
|
|
397
417
|
);
|
|
@@ -400,7 +420,7 @@ export type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;
|
|
|
400
420
|
|
|
401
421
|
export const TestErrorStack = ({ className, children, ...props }: TestErrorStackProps) => (
|
|
402
422
|
<pre
|
|
403
|
-
className={cn("mt-2 overflow-auto font-mono text-destructive-text text-
|
|
423
|
+
className={cn("mt-2 overflow-auto font-mono text-destructive-text text-meta", className)}
|
|
404
424
|
{...props}
|
|
405
425
|
>
|
|
406
426
|
{children}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { TokenUsage, TokenUsageTrigger } from "./token-usage";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "AI/TokenUsage",
|
|
5
|
+
component: TokenUsage,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "padded",
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
"The context-WINDOW usage ring: a compact `usedTokens` / `maxTokens` dial that opens a hover card with the input/output token split and, when `modelId` is known, an estimated cost. It was called `Context` until it was renamed for exactly this reason — the chat workspace’s right rail is `AI/ContextPanel`, a different component with a confusingly similar old name. See [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof TokenUsage>;
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<TokenUsage usedTokens={2400} maxTokens={8000}>
|
|
21
|
+
<TokenUsageTrigger />
|
|
22
|
+
</TokenUsage>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { LanguageModelUsage } from "ai";
|
|
2
|
+
import { DEFAULT_MESSAGES, LocaleProvider } from "@elabs-ai/components-ui";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { TokenUsage, TokenUsageCache, TokenUsageReasoning } from "./token-usage";
|
|
6
|
+
|
|
7
|
+
// `LanguageModelUsage`'s reasoning/cache figures live under nested detail
|
|
8
|
+
// objects (`outputTokenDetails.reasoningTokens`, `inputTokenDetails.cacheReadTokens`).
|
|
9
|
+
// ai@6 also carried deprecated flat top-level aliases (`reasoningTokens`,
|
|
10
|
+
// `cachedInputTokens`); ai@7 removed them. This mock deliberately omits the
|
|
11
|
+
// flat aliases so the test only passes if the components read the nested path.
|
|
12
|
+
const usage: LanguageModelUsage = {
|
|
13
|
+
inputTokens: 1000,
|
|
14
|
+
inputTokenDetails: {
|
|
15
|
+
noCacheTokens: 600,
|
|
16
|
+
cacheReadTokens: 400,
|
|
17
|
+
cacheWriteTokens: 0,
|
|
18
|
+
},
|
|
19
|
+
outputTokens: 200,
|
|
20
|
+
outputTokenDetails: {
|
|
21
|
+
textTokens: 150,
|
|
22
|
+
reasoningTokens: 50,
|
|
23
|
+
},
|
|
24
|
+
totalTokens: 1200,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("TokenUsage usage readouts (#30, ai@7 LanguageModelUsage shape)", () => {
|
|
28
|
+
it("TokenUsageReasoning reads outputTokenDetails.reasoningTokens, not a flat field", () => {
|
|
29
|
+
render(
|
|
30
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
31
|
+
<TokenUsageReasoning />
|
|
32
|
+
</TokenUsage>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText("Reasoning")).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText("50")).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("TokenUsageCache reads inputTokenDetails.cacheReadTokens, not a flat field", () => {
|
|
39
|
+
render(
|
|
40
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
41
|
+
<TokenUsageCache />
|
|
42
|
+
</TokenUsage>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("Cache")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("400")).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders nothing when the nested detail objects report zero", () => {
|
|
49
|
+
const zeroUsage: LanguageModelUsage = {
|
|
50
|
+
...usage,
|
|
51
|
+
inputTokenDetails: { ...usage.inputTokenDetails, cacheReadTokens: 0 },
|
|
52
|
+
outputTokenDetails: { ...usage.outputTokenDetails, reasoningTokens: 0 },
|
|
53
|
+
};
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<TokenUsage maxTokens={8000} usage={zeroUsage} usedTokens={1200}>
|
|
56
|
+
<TokenUsageReasoning />
|
|
57
|
+
<TokenUsageCache />
|
|
58
|
+
</TokenUsage>,
|
|
59
|
+
);
|
|
60
|
+
expect(container).toBeEmptyDOMElement();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("TokenUsage locale keys (#141, ai.context.* renamed to ai.tokenUsage.*)", () => {
|
|
65
|
+
it("the shipped dictionary defines no ai.context.* key", () => {
|
|
66
|
+
const staleKeys = Object.keys(DEFAULT_MESSAGES).filter((key) => key.startsWith("ai.context."));
|
|
67
|
+
expect(staleKeys).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders its labels from the ai.tokenUsage.* namespace, not a stale fallback", () => {
|
|
71
|
+
render(
|
|
72
|
+
<LocaleProvider
|
|
73
|
+
messages={{
|
|
74
|
+
"ai.tokenUsage.reasoning": "Custom reasoning label",
|
|
75
|
+
"ai.tokenUsage.cache": "Custom cache label",
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
79
|
+
<TokenUsageReasoning />
|
|
80
|
+
<TokenUsageCache />
|
|
81
|
+
</TokenUsage>
|
|
82
|
+
</LocaleProvider>,
|
|
83
|
+
);
|
|
84
|
+
// A component still reading the old `ai.context.*` namespace would ignore
|
|
85
|
+
// this override and keep rendering the untranslated English default
|
|
86
|
+
// ("Reasoning" / "Cache") instead.
|
|
87
|
+
expect(screen.getByText("Custom reasoning label")).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText("Custom cache label")).toBeInTheDocument();
|
|
89
|
+
expect(screen.queryByText("Reasoning")).not.toBeInTheDocument();
|
|
90
|
+
expect(screen.queryByText("Cache")).not.toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -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,
|
|
@@ -148,7 +154,7 @@ export const ContextContentHeader = ({
|
|
|
148
154
|
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
|
|
149
155
|
{children ?? (
|
|
150
156
|
<>
|
|
151
|
-
<div className="flex items-center justify-between gap-3 text-
|
|
157
|
+
<div className="flex items-center justify-between gap-3 text-meta">
|
|
152
158
|
<p>{displayPct}</p>
|
|
153
159
|
<p className="font-mono text-muted-foreground">
|
|
154
160
|
{used} / {total}
|
|
@@ -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,
|
|
@@ -197,14 +207,14 @@ export const ContextContentFooter = ({
|
|
|
197
207
|
return (
|
|
198
208
|
<div
|
|
199
209
|
className={cn(
|
|
200
|
-
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-
|
|
210
|
+
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-meta",
|
|
201
211
|
className,
|
|
202
212
|
)}
|
|
203
213
|
{...props}
|
|
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) {
|
|
@@ -249,18 +259,18 @@ export const ContextInputUsage = ({ className, children, ...props }: ContextInpu
|
|
|
249
259
|
});
|
|
250
260
|
|
|
251
261
|
return (
|
|
252
|
-
<div className={cn("flex items-center justify-between text-
|
|
253
|
-
<span className="text-muted-foreground">
|
|
262
|
+
<div className={cn("flex items-center justify-between text-meta", className)} {...props}>
|
|
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) {
|
|
@@ -283,23 +293,25 @@ export const ContextOutputUsage = ({ className, children, ...props }: ContextOut
|
|
|
283
293
|
});
|
|
284
294
|
|
|
285
295
|
return (
|
|
286
|
-
<div className={cn("flex items-center justify-between text-
|
|
287
|
-
<span className="text-muted-foreground">
|
|
296
|
+
<div className={cn("flex items-center justify-between text-meta", className)} {...props}>
|
|
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;
|
|
@@ -321,19 +333,21 @@ export const ContextReasoningUsage = ({
|
|
|
321
333
|
});
|
|
322
334
|
|
|
323
335
|
return (
|
|
324
|
-
<div className={cn("flex items-center justify-between text-
|
|
325
|
-
<span className="text-muted-foreground">
|
|
336
|
+
<div className={cn("flex items-center justify-between text-meta", className)} {...props}>
|
|
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;
|
|
@@ -355,8 +369,8 @@ export const ContextCacheUsage = ({ className, children, ...props }: ContextCach
|
|
|
355
369
|
});
|
|
356
370
|
|
|
357
371
|
return (
|
|
358
|
-
<div className={cn("flex items-center justify-between text-
|
|
359
|
-
<span className="text-muted-foreground">
|
|
372
|
+
<div className={cn("flex items-center justify-between text-meta", className)} {...props}>
|
|
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
|