@vvedantb/ui 0.17.0 → 0.18.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 +253 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- package/tokens/src/css/compat-shadcn-rgb.css +6 -0
- package/tokens/src/css/markdown.css +318 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import { createContext, useContext, useMemo } from "react";
|
|
5
|
+
|
|
6
|
+
import { Button } from "../ui/button";
|
|
7
|
+
import {
|
|
8
|
+
HoverCard,
|
|
9
|
+
HoverCardContent,
|
|
10
|
+
HoverCardTrigger,
|
|
11
|
+
} from "../ui/hover-card";
|
|
12
|
+
import { Progress } from "../ui/progress";
|
|
13
|
+
import { useCn } from "../ui/ui-config";
|
|
14
|
+
|
|
15
|
+
const PERCENT_MAX = 100;
|
|
16
|
+
const ICON_RADIUS = 10;
|
|
17
|
+
const ICON_VIEWBOX = 24;
|
|
18
|
+
const ICON_CENTER = 12;
|
|
19
|
+
const ICON_STROKE_WIDTH = 2;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-category token counts. Cache reads and writes are priced very differently
|
|
23
|
+
* (reads ~0.1x input, writes ~1.25x input), so they are tracked separately.
|
|
24
|
+
*/
|
|
25
|
+
interface LanguageModelUsage {
|
|
26
|
+
inputTokens?: number;
|
|
27
|
+
outputTokens?: number;
|
|
28
|
+
cachedInputReadTokens?: number;
|
|
29
|
+
cachedInputWriteTokens?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pre-computed costs (USD). Pass these when the authoritative cost is known
|
|
34
|
+
* (e.g. Claude returns `total_cost_usd` directly). Avoids needing a pricing
|
|
35
|
+
* catalog at render time.
|
|
36
|
+
*/
|
|
37
|
+
interface ContextCosts {
|
|
38
|
+
totalUSD?: number;
|
|
39
|
+
inputUSD?: number;
|
|
40
|
+
outputUSD?: number;
|
|
41
|
+
cacheReadUSD?: number;
|
|
42
|
+
cacheWriteUSD?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ContextSchema {
|
|
46
|
+
usedTokens: number;
|
|
47
|
+
maxTokens: number;
|
|
48
|
+
usage?: LanguageModelUsage;
|
|
49
|
+
costs?: ContextCosts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ContextContext = createContext<ContextSchema | null>(null);
|
|
53
|
+
|
|
54
|
+
const useContextValue = () => {
|
|
55
|
+
const context = useContext(ContextContext);
|
|
56
|
+
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("ContextUsage components must be used within ContextUsage");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return context;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ContextUsageProps = ComponentProps<typeof HoverCard> & ContextSchema;
|
|
65
|
+
|
|
66
|
+
export const ContextUsage = ({
|
|
67
|
+
usedTokens,
|
|
68
|
+
maxTokens,
|
|
69
|
+
usage,
|
|
70
|
+
costs,
|
|
71
|
+
...props
|
|
72
|
+
}: ContextUsageProps) => {
|
|
73
|
+
const contextValue = useMemo(
|
|
74
|
+
() => ({ costs, maxTokens, usage, usedTokens }),
|
|
75
|
+
[costs, maxTokens, usage, usedTokens],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<ContextContext.Provider value={contextValue}>
|
|
80
|
+
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
81
|
+
</ContextContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ContextIcon = () => {
|
|
86
|
+
const { usedTokens, maxTokens } = useContextValue();
|
|
87
|
+
const circumference = 2 * Math.PI * ICON_RADIUS;
|
|
88
|
+
const usedPercent = maxTokens > 0 ? usedTokens / maxTokens : 0;
|
|
89
|
+
const dashOffset = circumference * (1 - usedPercent);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<svg
|
|
93
|
+
aria-label="Model context usage"
|
|
94
|
+
height="16"
|
|
95
|
+
role="img"
|
|
96
|
+
style={{ color: "currentcolor" }}
|
|
97
|
+
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
|
98
|
+
width="16"
|
|
99
|
+
>
|
|
100
|
+
<circle
|
|
101
|
+
cx={ICON_CENTER}
|
|
102
|
+
cy={ICON_CENTER}
|
|
103
|
+
fill="none"
|
|
104
|
+
opacity="0.25"
|
|
105
|
+
r={ICON_RADIUS}
|
|
106
|
+
stroke="currentColor"
|
|
107
|
+
strokeWidth={ICON_STROKE_WIDTH}
|
|
108
|
+
/>
|
|
109
|
+
<circle
|
|
110
|
+
cx={ICON_CENTER}
|
|
111
|
+
cy={ICON_CENTER}
|
|
112
|
+
fill="none"
|
|
113
|
+
opacity="0.7"
|
|
114
|
+
r={ICON_RADIUS}
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
strokeDasharray={`${circumference} ${circumference}`}
|
|
117
|
+
strokeDashoffset={dashOffset}
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
strokeWidth={ICON_STROKE_WIDTH}
|
|
120
|
+
style={{ transform: "rotate(-90deg)", transformOrigin: "center" }}
|
|
121
|
+
/>
|
|
122
|
+
</svg>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type ContextUsageTriggerProps = ComponentProps<typeof Button>;
|
|
127
|
+
|
|
128
|
+
export const ContextUsageTrigger = ({ children, ...props }: ContextUsageTriggerProps) => {
|
|
129
|
+
const { usedTokens, maxTokens } = useContextValue();
|
|
130
|
+
const usedPercent = maxTokens > 0 ? usedTokens / maxTokens : 0;
|
|
131
|
+
const renderedPercent = new Intl.NumberFormat("en-US", {
|
|
132
|
+
maximumFractionDigits: 1,
|
|
133
|
+
style: "percent",
|
|
134
|
+
}).format(usedPercent);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<HoverCardTrigger asChild>
|
|
138
|
+
{children ?? (
|
|
139
|
+
<Button type="button" variant="ghost" size="sm" {...props}>
|
|
140
|
+
<span className="font-medium text-muted-foreground text-xs tabular-nums">
|
|
141
|
+
{renderedPercent}
|
|
142
|
+
</span>
|
|
143
|
+
<ContextIcon />
|
|
144
|
+
</Button>
|
|
145
|
+
)}
|
|
146
|
+
</HoverCardTrigger>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type ContextUsageContentProps = ComponentProps<typeof HoverCardContent>;
|
|
151
|
+
|
|
152
|
+
export const ContextUsageContent = ({
|
|
153
|
+
className,
|
|
154
|
+
...props
|
|
155
|
+
}: ContextUsageContentProps) => {
|
|
156
|
+
const merge = useCn();
|
|
157
|
+
return (
|
|
158
|
+
<HoverCardContent
|
|
159
|
+
className={merge("min-w-60 divide-y overflow-hidden p-0", className)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type ContextUsageContentHeaderProps = ComponentProps<"div">;
|
|
166
|
+
|
|
167
|
+
export const ContextUsageContentHeader = ({
|
|
168
|
+
children,
|
|
169
|
+
className,
|
|
170
|
+
...props
|
|
171
|
+
}: ContextUsageContentHeaderProps) => {
|
|
172
|
+
const merge = useCn();
|
|
173
|
+
const { usedTokens, maxTokens } = useContextValue();
|
|
174
|
+
const usedPercent = maxTokens > 0 ? usedTokens / maxTokens : 0;
|
|
175
|
+
const displayPct = new Intl.NumberFormat("en-US", {
|
|
176
|
+
maximumFractionDigits: 1,
|
|
177
|
+
style: "percent",
|
|
178
|
+
}).format(usedPercent);
|
|
179
|
+
const used = new Intl.NumberFormat("en-US", {
|
|
180
|
+
notation: "compact",
|
|
181
|
+
}).format(usedTokens);
|
|
182
|
+
const total = new Intl.NumberFormat("en-US", {
|
|
183
|
+
notation: "compact",
|
|
184
|
+
}).format(maxTokens);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className={merge("w-full space-y-2 p-3", className)} {...props}>
|
|
188
|
+
{children ?? (
|
|
189
|
+
<>
|
|
190
|
+
<div className="flex items-center justify-between gap-3 text-xs tabular-nums">
|
|
191
|
+
<p>{displayPct}</p>
|
|
192
|
+
<p className="font-mono text-muted-foreground">
|
|
193
|
+
{used} / {total}
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
<Progress className="bg-surface-secondary" value={usedPercent * PERCENT_MAX} />
|
|
198
|
+
</div>
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type ContextUsageContentBodyProps = ComponentProps<"div">;
|
|
206
|
+
|
|
207
|
+
export const ContextUsageContentBody = ({
|
|
208
|
+
children,
|
|
209
|
+
className,
|
|
210
|
+
...props
|
|
211
|
+
}: ContextUsageContentBodyProps) => {
|
|
212
|
+
const merge = useCn();
|
|
213
|
+
return (
|
|
214
|
+
<div className={merge("w-full p-3", className)} {...props}>
|
|
215
|
+
{children}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const formatUSD = (value: number) =>
|
|
221
|
+
new Intl.NumberFormat("en-US", {
|
|
222
|
+
currency: "USD",
|
|
223
|
+
// Claude's per-call cost is often sub-cent; show enough precision to be useful.
|
|
224
|
+
maximumFractionDigits: 4,
|
|
225
|
+
minimumFractionDigits: 2,
|
|
226
|
+
style: "currency",
|
|
227
|
+
}).format(value);
|
|
228
|
+
|
|
229
|
+
const formatTokens = (value: number) =>
|
|
230
|
+
new Intl.NumberFormat("en-US", { notation: "compact" }).format(value);
|
|
231
|
+
|
|
232
|
+
export type ContextUsageContentFooterProps = ComponentProps<"div">;
|
|
233
|
+
|
|
234
|
+
export const ContextUsageContentFooter = ({
|
|
235
|
+
children,
|
|
236
|
+
className,
|
|
237
|
+
...props
|
|
238
|
+
}: ContextUsageContentFooterProps) => {
|
|
239
|
+
const merge = useCn();
|
|
240
|
+
const { costs } = useContextValue();
|
|
241
|
+
const totalCost = costs?.totalUSD;
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
className={merge(
|
|
246
|
+
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
|
|
247
|
+
className,
|
|
248
|
+
)}
|
|
249
|
+
{...props}
|
|
250
|
+
>
|
|
251
|
+
{children ?? (
|
|
252
|
+
<>
|
|
253
|
+
<span className="text-muted-foreground">Total cost</span>
|
|
254
|
+
<span className="font-mono tabular-nums">
|
|
255
|
+
{totalCost === undefined ? "—" : formatUSD(totalCost)}
|
|
256
|
+
</span>
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const UsageRow = ({
|
|
264
|
+
label,
|
|
265
|
+
tokens,
|
|
266
|
+
costUSD,
|
|
267
|
+
className,
|
|
268
|
+
...rest
|
|
269
|
+
}: {
|
|
270
|
+
label: string;
|
|
271
|
+
tokens: number;
|
|
272
|
+
costUSD?: number;
|
|
273
|
+
} & ComponentProps<"div">) => {
|
|
274
|
+
const merge = useCn();
|
|
275
|
+
if (!tokens) return null;
|
|
276
|
+
return (
|
|
277
|
+
<div
|
|
278
|
+
className={merge("flex items-center justify-between text-xs", className)}
|
|
279
|
+
{...rest}
|
|
280
|
+
>
|
|
281
|
+
<span className="text-muted-foreground">{label}</span>
|
|
282
|
+
<span className="tabular-nums">
|
|
283
|
+
{formatTokens(tokens)}
|
|
284
|
+
{costUSD !== undefined ? (
|
|
285
|
+
<span className="ml-2 font-mono text-muted-foreground">
|
|
286
|
+
{formatUSD(costUSD)}
|
|
287
|
+
</span>
|
|
288
|
+
) : null}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export type ContextUsageInputProps = ComponentProps<"div">;
|
|
295
|
+
|
|
296
|
+
export const ContextUsageInput = (props: ContextUsageInputProps) => {
|
|
297
|
+
const { usage, costs } = useContextValue();
|
|
298
|
+
return (
|
|
299
|
+
<UsageRow
|
|
300
|
+
label="Input"
|
|
301
|
+
tokens={usage?.inputTokens ?? 0}
|
|
302
|
+
costUSD={costs?.inputUSD}
|
|
303
|
+
{...props}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export type ContextUsageOutputProps = ComponentProps<"div">;
|
|
309
|
+
|
|
310
|
+
export const ContextUsageOutput = (props: ContextUsageOutputProps) => {
|
|
311
|
+
const { usage, costs } = useContextValue();
|
|
312
|
+
return (
|
|
313
|
+
<UsageRow
|
|
314
|
+
label="Output"
|
|
315
|
+
tokens={usage?.outputTokens ?? 0}
|
|
316
|
+
costUSD={costs?.outputUSD}
|
|
317
|
+
{...props}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export type ContextUsageCacheReadProps = ComponentProps<"div">;
|
|
323
|
+
|
|
324
|
+
export const ContextUsageCacheRead = (props: ContextUsageCacheReadProps) => {
|
|
325
|
+
const { usage, costs } = useContextValue();
|
|
326
|
+
return (
|
|
327
|
+
<UsageRow
|
|
328
|
+
label="Cache read"
|
|
329
|
+
tokens={usage?.cachedInputReadTokens ?? 0}
|
|
330
|
+
costUSD={costs?.cacheReadUSD}
|
|
331
|
+
{...props}
|
|
332
|
+
/>
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export type ContextUsageCacheWriteProps = ComponentProps<"div">;
|
|
337
|
+
|
|
338
|
+
export const ContextUsageCacheWrite = (props: ContextUsageCacheWriteProps) => {
|
|
339
|
+
const { usage, costs } = useContextValue();
|
|
340
|
+
return (
|
|
341
|
+
<UsageRow
|
|
342
|
+
label="Cache write"
|
|
343
|
+
tokens={usage?.cachedInputWriteTokens ?? 0}
|
|
344
|
+
costUSD={costs?.cacheWriteUSD}
|
|
345
|
+
{...props}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @deprecated Use ContextUsageCacheRead and ContextUsageCacheWrite separately.
|
|
352
|
+
* Kept for backwards compat — shows combined cache reads + writes as one row.
|
|
353
|
+
*/
|
|
354
|
+
export const ContextUsageCache = (props: ComponentProps<"div">) => {
|
|
355
|
+
const { usage, costs } = useContextValue();
|
|
356
|
+
const cacheTokens =
|
|
357
|
+
(usage?.cachedInputReadTokens ?? 0) + (usage?.cachedInputWriteTokens ?? 0);
|
|
358
|
+
const cacheCost =
|
|
359
|
+
costs?.cacheReadUSD !== undefined || costs?.cacheWriteUSD !== undefined
|
|
360
|
+
? (costs?.cacheReadUSD ?? 0) + (costs?.cacheWriteUSD ?? 0)
|
|
361
|
+
: undefined;
|
|
362
|
+
return (
|
|
363
|
+
<UsageRow
|
|
364
|
+
label="Cache"
|
|
365
|
+
tokens={cacheTokens}
|
|
366
|
+
costUSD={cacheCost}
|
|
367
|
+
{...props}
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
370
|
+
};
|
|
@@ -1,34 +1,71 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import type { ComponentProps } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, domAnimation, LazyMotion, m } from "motion/react";
|
|
4
5
|
import { IconArrowDown, IconDownload } from "@tabler/icons-react";
|
|
5
6
|
import { Button } from "../ui/button";
|
|
6
7
|
import { cn } from "../utils/cn";
|
|
7
8
|
import { flatInteractiveClass } from "../ui/surface-classes";
|
|
9
|
+
import { useCn, useUiConfig } from "../ui/ui-config";
|
|
8
10
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
9
11
|
import { fastTransition } from "../motion/presets.alt";
|
|
12
|
+
type MotionDivProps = ComponentProps<typeof m.div>;
|
|
13
|
+
/**
|
|
14
|
+
* `UiConfig.Conversation`. `classes` replace built-in classes: `content`
|
|
15
|
+
* (`ConversationContent`), `scroll` (a default `scrollClassName`, merged
|
|
16
|
+
* before the prop), `download`, and the scroll button's `scrollButton`,
|
|
17
|
+
* `scrollButtonMotion` (the animated wrapper; `""` drops its class) and
|
|
18
|
+
* `scrollButtonFrame` (a static wrapper around the presence, absent by
|
|
19
|
+
* default). `scrollButton` holds the scroll button's prop defaults and
|
|
20
|
+
* motion.
|
|
21
|
+
*/
|
|
22
|
+
export interface ConversationConfig {
|
|
23
|
+
classes?: {
|
|
24
|
+
content?: string;
|
|
25
|
+
scroll?: string;
|
|
26
|
+
download?: string;
|
|
27
|
+
scrollButton?: string;
|
|
28
|
+
scrollButtonMotion?: string;
|
|
29
|
+
scrollButtonFrame?: string;
|
|
30
|
+
};
|
|
31
|
+
scrollButton?: Pick<ConversationScrollButtonProps, "label" | "icon" | "showDelay" | "size"> & {
|
|
32
|
+
initial?: MotionDivProps["initial"];
|
|
33
|
+
animate?: MotionDivProps["animate"];
|
|
34
|
+
exit?: MotionDivProps["exit"];
|
|
35
|
+
transition?: MotionDivProps["transition"];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
10
38
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|
11
|
-
export const Conversation = ({ className, ...props }: ConversationProps) =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
export const Conversation = ({ className, ...props }: ConversationProps) => {
|
|
40
|
+
const merge = useCn();
|
|
41
|
+
return (
|
|
42
|
+
<StickToBottom
|
|
43
|
+
className={merge("relative flex-1 overflow-y-hidden", className)}
|
|
44
|
+
initial="instant"
|
|
45
|
+
resize="smooth"
|
|
46
|
+
role="log"
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
20
51
|
export type ConversationContentProps = ComponentProps<
|
|
21
52
|
typeof StickToBottom.Content
|
|
22
53
|
>;
|
|
23
54
|
export const ConversationContent = ({
|
|
24
55
|
className,
|
|
25
56
|
...props
|
|
26
|
-
}: ConversationContentProps) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
}: ConversationContentProps) => {
|
|
58
|
+
const classes = useUiConfig("Conversation")?.classes;
|
|
59
|
+
const merge = useCn();
|
|
60
|
+
const scroll = classes?.scroll;
|
|
61
|
+
return (
|
|
62
|
+
<StickToBottom.Content
|
|
63
|
+
className={merge(classes?.content ?? "flex flex-col gap-4 px-3 py-3", className)}
|
|
64
|
+
{...props}
|
|
65
|
+
{...(scroll === undefined ? {} : { scrollClassName: merge(scroll, props.scrollClassName) })}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
32
69
|
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
|
33
70
|
title?: string;
|
|
34
71
|
description?: string;
|
|
@@ -41,59 +78,144 @@ export const ConversationEmptyState = ({
|
|
|
41
78
|
icon,
|
|
42
79
|
children,
|
|
43
80
|
...props
|
|
44
|
-
}: ConversationEmptyStateProps) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
className
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
81
|
+
}: ConversationEmptyStateProps) => {
|
|
82
|
+
const merge = useCn();
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={merge(
|
|
86
|
+
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
{children ?? (
|
|
92
|
+
<>
|
|
93
|
+
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
94
|
+
<div className="space-y-1">
|
|
95
|
+
<h3 className="font-medium text-sm">{title}</h3>
|
|
96
|
+
{description && <p className="text-muted-foreground text-sm">{description}</p>}
|
|
97
|
+
</div>
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
export type ConversationScrollButtonProps = ComponentProps<typeof Button> & {
|
|
104
|
+
/** Text after the icon; also the button's `aria-label` and `title`. */
|
|
105
|
+
label?: string;
|
|
106
|
+
/** Replaces the arrow icon. */
|
|
107
|
+
icon?: ReactNode;
|
|
108
|
+
/**
|
|
109
|
+
* Wait this long (ms) after leaving the bottom before showing the button,
|
|
110
|
+
* so brief layout settles do not flash it. Hiding is immediate. Default 0.
|
|
111
|
+
*/
|
|
112
|
+
showDelay?: number;
|
|
113
|
+
/**
|
|
114
|
+
* When this changes (for example the thread id), hide the button at once and
|
|
115
|
+
* cancel a pending show; it shows again on the next scroll away.
|
|
116
|
+
*/
|
|
117
|
+
resetKey?: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* `wanted`, or (with a delay or reset key) `wanted` held for `delay` ms, and
|
|
121
|
+
* a function that hides it at once.
|
|
122
|
+
*/
|
|
123
|
+
function useDelayedShow(
|
|
124
|
+
wanted: boolean,
|
|
125
|
+
delay: number,
|
|
126
|
+
resetKey: string | undefined,
|
|
127
|
+
): [boolean, () => void] {
|
|
128
|
+
const debounced = delay > 0 || resetKey !== undefined;
|
|
129
|
+
const [shown, setShown] = useState(false);
|
|
130
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
131
|
+
const hide = () => {
|
|
132
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
133
|
+
timerRef.current = null;
|
|
134
|
+
setShown(false);
|
|
135
|
+
};
|
|
136
|
+
// The reset also cancels the pending show timer, a side effect.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!debounced) return;
|
|
139
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
140
|
+
timerRef.current = null;
|
|
141
|
+
setShown(false);
|
|
142
|
+
}, [resetKey, debounced]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!debounced) return;
|
|
145
|
+
if (!wanted) {
|
|
146
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
147
|
+
timerRef.current = null;
|
|
148
|
+
setShown(false);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
timerRef.current = setTimeout(() => {
|
|
152
|
+
setShown(true);
|
|
153
|
+
timerRef.current = null;
|
|
154
|
+
}, delay);
|
|
155
|
+
return () => {
|
|
156
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
157
|
+
timerRef.current = null;
|
|
158
|
+
};
|
|
159
|
+
}, [wanted, delay, debounced]);
|
|
160
|
+
return [debounced ? shown : wanted, debounced ? hide : () => {}];
|
|
161
|
+
}
|
|
64
162
|
export const ConversationScrollButton = ({
|
|
65
163
|
className,
|
|
164
|
+
label: labelProp,
|
|
165
|
+
icon: iconProp,
|
|
166
|
+
showDelay,
|
|
167
|
+
resetKey,
|
|
168
|
+
size,
|
|
66
169
|
...props
|
|
67
170
|
}: ConversationScrollButtonProps) => {
|
|
171
|
+
const config = useUiConfig("Conversation");
|
|
172
|
+
const defaults = config?.scrollButton;
|
|
173
|
+
const classes = config?.classes;
|
|
174
|
+
const merge = useCn();
|
|
68
175
|
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
176
|
+
const [visible, hide] = useDelayedShow(
|
|
177
|
+
!isAtBottom,
|
|
178
|
+
showDelay ?? defaults?.showDelay ?? 0,
|
|
179
|
+
resetKey,
|
|
180
|
+
);
|
|
181
|
+
const label = labelProp ?? defaults?.label;
|
|
182
|
+
const icon = iconProp ?? defaults?.icon;
|
|
69
183
|
const handleScrollToBottom = () => {
|
|
184
|
+
hide();
|
|
70
185
|
scrollToBottom();
|
|
71
186
|
};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
className={cn(flatInteractiveClass, "rounded-full", className)}
|
|
85
|
-
onClick={handleScrollToBottom}
|
|
86
|
-
size="icon"
|
|
87
|
-
type="button"
|
|
88
|
-
variant="outline"
|
|
89
|
-
{...props}
|
|
187
|
+
const motionClass = classes?.scrollButtonMotion ?? "absolute bottom-4 left-0 right-0 z-10 flex justify-center";
|
|
188
|
+
const presence = (
|
|
189
|
+
<LazyMotion features={domAnimation}>
|
|
190
|
+
<AnimatePresence initial={false}>
|
|
191
|
+
{visible ? (
|
|
192
|
+
<m.div
|
|
193
|
+
key="scroll-to-bottom"
|
|
194
|
+
className={motionClass === "" ? undefined : motionClass}
|
|
195
|
+
initial={defaults?.initial ?? { opacity: 0, y: 8 }}
|
|
196
|
+
animate={defaults?.animate ?? { opacity: 1, y: 0 }}
|
|
197
|
+
exit={defaults?.exit ?? { opacity: 0, y: 8 }}
|
|
198
|
+
transition={defaults?.transition ?? fastTransition}
|
|
90
199
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
200
|
+
<Button
|
|
201
|
+
className={merge(classes?.scrollButton ?? [flatInteractiveClass, "rounded-full"], className)}
|
|
202
|
+
onClick={handleScrollToBottom}
|
|
203
|
+
size={size ?? defaults?.size ?? "icon"}
|
|
204
|
+
type="button"
|
|
205
|
+
variant="outline"
|
|
206
|
+
{...(label ? { "aria-label": label, title: label } : {})}
|
|
207
|
+
{...props}
|
|
208
|
+
>
|
|
209
|
+
{icon ?? <IconArrowDown className="size-4" />}
|
|
210
|
+
{label}
|
|
211
|
+
</Button>
|
|
212
|
+
</m.div>
|
|
213
|
+
) : null}
|
|
214
|
+
</AnimatePresence>
|
|
215
|
+
</LazyMotion>
|
|
96
216
|
);
|
|
217
|
+
const frame = classes?.scrollButtonFrame;
|
|
218
|
+
return frame === undefined ? presence : <div className={frame}>{presence}</div>;
|
|
97
219
|
};
|
|
98
220
|
export interface ConversationMessage {
|
|
99
221
|
role: "user" | "assistant" | "system" | "data" | "tool";
|
|
@@ -127,6 +249,8 @@ export const ConversationDownload = ({
|
|
|
127
249
|
children,
|
|
128
250
|
...props
|
|
129
251
|
}: ConversationDownloadProps) => {
|
|
252
|
+
const download = useUiConfig("Conversation")?.classes?.download;
|
|
253
|
+
const merge = useCn();
|
|
130
254
|
const handleDownload = () => {
|
|
131
255
|
const markdown = messagesToMarkdown(messages, formatMessage);
|
|
132
256
|
const blob = new Blob([markdown], { type: "text/markdown" });
|
|
@@ -141,8 +265,8 @@ export const ConversationDownload = ({
|
|
|
141
265
|
};
|
|
142
266
|
return (
|
|
143
267
|
<Button
|
|
144
|
-
className={
|
|
145
|
-
cn(flatInteractiveClass, "absolute top-4 right-4 rounded-full"),
|
|
268
|
+
className={merge(
|
|
269
|
+
download ?? cn(flatInteractiveClass, "absolute top-4 right-4 rounded-full"),
|
|
146
270
|
className,
|
|
147
271
|
)}
|
|
148
272
|
onClick={handleDownload}
|