agentic-ui-kit 0.5.1 → 0.7.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/dist/core.js +2 -2
- package/dist/index.js +918 -120
- package/dist/{src-Ce7xTfGU.js → src-D01l3nBc.js} +1 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +44 -1
- package/dist/types/core/src/events.d.ts +2 -0
- package/dist/types/core/src/visualize.d.ts +1 -1
- package/dist/types/react/src/components/attachments.d.ts +4 -1
- package/dist/types/react/src/components/chat-list.d.ts +4 -0
- package/dist/types/react/src/components/composer.d.ts +7 -1
- package/dist/types/react/src/components/context-meter.d.ts +24 -0
- package/dist/types/react/src/components/context-panel.d.ts +5 -0
- package/dist/types/react/src/components/copy-field.d.ts +15 -0
- package/dist/types/react/src/components/dictation.d.ts +73 -0
- package/dist/types/react/src/components/effort-slider.d.ts +1 -1
- package/dist/types/react/src/components/markdown.d.ts +19 -2
- package/dist/types/react/src/components/model-picker.d.ts +6 -1
- package/dist/types/react/src/components/tools.d.ts +7 -2
- package/dist/types/react/src/hooks/sidebar-state.d.ts +6 -0
- package/dist/types/react/src/hooks/sse-transport.d.ts +7 -1
- package/dist/types/react/src/hooks/use-chat.d.ts +14 -1
- package/dist/types/react/src/index.d.ts +11 -5
- package/dist/types/react/src/primitives/button.d.ts +11 -3
- package/dist/types/react/src/primitives/cx.d.ts +3 -0
- package/dist/types/react/src/primitives/icons.d.ts +6 -0
- package/dist/types/react/src/primitives/labels.d.ts +28 -0
- package/dist/types/react/src/primitives/menu.d.ts +23 -5
- package/dist/types/react/src/primitives/overlay.d.ts +13 -1
- package/dist/types/react/src/primitives/segmented-control.d.ts +30 -5
- package/dist/types/react/src/views/app-shell.d.ts +31 -3
- package/dist/types/react/src/views/chat-view.d.ts +6 -2
- package/dist/types/react/src/views/sidebar.d.ts +31 -5
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { A as awaitingToolCalls, C as widgetLoaderMessages, D as toBlocks, F as settleToolCall, L as salvageJson, O as activityTokens, T as readEventStream, _ as replacedId, b as resolveReplaces, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, k as applyEvent } from "./src-
|
|
2
|
+
import { A as awaitingToolCalls, C as widgetLoaderMessages, D as toBlocks, F as settleToolCall, L as salvageJson, O as activityTokens, T as readEventStream, _ as replacedId, b as resolveReplaces, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, k as applyEvent } from "./src-D01l3nBc.js";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
|
-
import {
|
|
5
|
-
import { Fragment,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
5
|
+
import { Children, Component, Fragment, createContext, createElement, isValidElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, DialogTrigger as DialogTrigger$1, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
8
9
|
import { UNSAFE_PortalProvider } from "react-aria";
|
|
9
10
|
import { createMathPlugin } from "@streamdown/math";
|
|
10
|
-
import { Streamdown, defaultRehypePlugins } from "streamdown";
|
|
11
|
+
import { Streamdown, defaultComponents, defaultRehypePlugins } from "streamdown";
|
|
12
|
+
// src/primitives/cx.ts
|
|
13
|
+
const twMerge = extendTailwindMerge({ extend: { theme: {
|
|
14
|
+
radius: [
|
|
15
|
+
"chat-sm",
|
|
16
|
+
"chat-md",
|
|
17
|
+
"chat-lg",
|
|
18
|
+
"chat-popover"
|
|
19
|
+
],
|
|
20
|
+
shadow: ["chat-popover"],
|
|
21
|
+
animate: [
|
|
22
|
+
"chat-in",
|
|
23
|
+
"chat-out",
|
|
24
|
+
"chat-fade-in",
|
|
25
|
+
"chat-fade-out",
|
|
26
|
+
"chat-drawer-in",
|
|
27
|
+
"chat-drawer-out",
|
|
28
|
+
"chat-drawer-in-right",
|
|
29
|
+
"chat-drawer-out-right",
|
|
30
|
+
"chat-sheet-in",
|
|
31
|
+
"chat-sheet-out"
|
|
32
|
+
]
|
|
33
|
+
} } });
|
|
34
|
+
/** `clsx` plus tailwind-merge: later classes win over earlier ones that set the same property, so a caller's `h-7` or `rounded-none` beats the kit's. */
|
|
35
|
+
function cx(...inputs) {
|
|
36
|
+
return twMerge(clsx(inputs));
|
|
37
|
+
}
|
|
11
38
|
// src/primitives/tooltip.tsx
|
|
12
39
|
function Tooltip({ content, children, placement = "top", delay = 600 }) {
|
|
13
40
|
return /* @__PURE__ */ jsxs(TooltipTrigger, {
|
|
@@ -38,23 +65,88 @@ const variants = {
|
|
|
38
65
|
*/
|
|
39
66
|
danger: "bg-[var(--chat-danger-bg,color-mix(in_srgb,var(--chat-danger)_70%,black))] font-medium text-[var(--chat-danger-fg,#fff)] data-hovered:brightness-110 data-pressed:brightness-95"
|
|
40
67
|
};
|
|
41
|
-
const sizes = {
|
|
68
|
+
const sizes$1 = {
|
|
42
69
|
sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
|
|
43
70
|
md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
|
|
44
71
|
/** Dialog and empty-state actions. */
|
|
45
72
|
lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
|
|
46
73
|
};
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
/** Press handlers dropped from a button that is disabled with a reason; it only looks and announces as disabled. */
|
|
75
|
+
const pressHandlers = [
|
|
76
|
+
"onPress",
|
|
77
|
+
"onPressStart",
|
|
78
|
+
"onPressEnd",
|
|
79
|
+
"onPressChange",
|
|
80
|
+
"onPressUp",
|
|
81
|
+
"onClick"
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* A disabled button that still explains itself. React Aria's `isDisabled` makes the button unfocusable and
|
|
85
|
+
* hoverable-less, so there'd be nothing to hang a tooltip on. With a reason the button instead stays focusable
|
|
86
|
+
* with `aria-disabled`, loses its press handlers and never submits a form, and the reason is shown as a tooltip
|
|
87
|
+
* and read as its description.
|
|
88
|
+
*/
|
|
89
|
+
function useDisabledReason(props, disabledReason) {
|
|
90
|
+
const id = useId();
|
|
91
|
+
const active = Boolean(props.isDisabled && disabledReason);
|
|
92
|
+
if (!active) return {
|
|
93
|
+
active,
|
|
94
|
+
props,
|
|
95
|
+
reasonId: void 0
|
|
96
|
+
};
|
|
97
|
+
const rest = {
|
|
98
|
+
...props,
|
|
99
|
+
isDisabled: false,
|
|
100
|
+
type: "button"
|
|
101
|
+
};
|
|
102
|
+
for (const h of pressHandlers) delete rest[h];
|
|
103
|
+
rest["aria-disabled"] = true;
|
|
104
|
+
rest["data-disabled"] = true;
|
|
105
|
+
rest["aria-describedby"] = [props["aria-describedby"], id].filter(Boolean).join(" ");
|
|
106
|
+
return {
|
|
107
|
+
active,
|
|
108
|
+
props: rest,
|
|
109
|
+
reasonId: id
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Hover and press styles are dropped while a button is disabled with a reason, to look as disabled as one that is. */
|
|
113
|
+
const noInteraction = (classes) => classes.replace(/data-(hovered|pressed):\S+\s*/g, "");
|
|
114
|
+
function WithReason({ active, reasonId, reason, children }) {
|
|
115
|
+
if (!active) return children;
|
|
116
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
117
|
+
content: reason,
|
|
118
|
+
children
|
|
119
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
120
|
+
id: reasonId,
|
|
121
|
+
hidden: true,
|
|
122
|
+
children: reason
|
|
123
|
+
})] });
|
|
124
|
+
}
|
|
125
|
+
function Button({ variant = "ghost", size = "md", className, disabledReason, ...rest }) {
|
|
126
|
+
const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
|
|
127
|
+
const button = /* @__PURE__ */ jsx(Button$1, {
|
|
49
128
|
...props,
|
|
50
|
-
className:
|
|
129
|
+
className: cx("inline-flex cursor-pointer items-center justify-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none [&_svg]:size-4 [&_svg]:shrink-0", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, active ? noInteraction(variants[variant]) : variants[variant], sizes$1[size], className)
|
|
130
|
+
});
|
|
131
|
+
return /* @__PURE__ */ jsx(WithReason, {
|
|
132
|
+
active,
|
|
133
|
+
reasonId,
|
|
134
|
+
reason: disabledReason,
|
|
135
|
+
children: button
|
|
51
136
|
});
|
|
52
137
|
}
|
|
53
|
-
function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...
|
|
138
|
+
function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, disabledReason, ...rest }) {
|
|
139
|
+
const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
|
|
54
140
|
const button = /* @__PURE__ */ jsx(Button$1, {
|
|
55
141
|
...props,
|
|
56
142
|
"aria-label": label,
|
|
57
|
-
className:
|
|
143
|
+
className: cx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, active ? noInteraction(variants[variant]) : variants[variant], variant === "ghost" && (active ? "text-chat-muted" : "text-chat-muted data-hovered:text-chat-fg"), size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
|
|
144
|
+
});
|
|
145
|
+
if (active) return /* @__PURE__ */ jsx(WithReason, {
|
|
146
|
+
active: true,
|
|
147
|
+
reasonId,
|
|
148
|
+
reason: disabledReason,
|
|
149
|
+
children: button
|
|
58
150
|
});
|
|
59
151
|
return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
|
|
60
152
|
content: label,
|
|
@@ -70,7 +162,7 @@ const tones = {
|
|
|
70
162
|
};
|
|
71
163
|
function Badge({ children, tone = "outline", icon, className }) {
|
|
72
164
|
return /* @__PURE__ */ jsxs("span", {
|
|
73
|
-
className:
|
|
165
|
+
className: cx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 whitespace-nowrap [&_svg]:size-3", tones[tone], className),
|
|
74
166
|
children: [icon, children]
|
|
75
167
|
});
|
|
76
168
|
}
|
|
@@ -97,6 +189,9 @@ const defaultIcons = {
|
|
|
97
189
|
send: /* @__PURE__ */ jsx(ArrowUp, {}),
|
|
98
190
|
stop: /* @__PURE__ */ jsx(Square, {}),
|
|
99
191
|
attach: /* @__PURE__ */ jsx(Plus, {}),
|
|
192
|
+
mic: /* @__PURE__ */ jsx(Mic, {}),
|
|
193
|
+
dictationCancel: /* @__PURE__ */ jsx(X, {}),
|
|
194
|
+
dictationAccept: /* @__PURE__ */ jsx(Check, {}),
|
|
100
195
|
scrollToBottom: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
101
196
|
tokens: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
102
197
|
sidebar: /* @__PURE__ */ jsx(PanelLeft, {}),
|
|
@@ -167,6 +262,9 @@ const defaultLabels = {
|
|
|
167
262
|
duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
|
|
168
263
|
ranCommandLabel: "Ran a command",
|
|
169
264
|
ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
|
|
265
|
+
runCommandAsk: "Run this command?",
|
|
266
|
+
searchWebAsk: "Search the web?",
|
|
267
|
+
readPageAsk: "Read this page?",
|
|
170
268
|
searchedWebLabel: "Searched the web",
|
|
171
269
|
readPageLabel: "Read page",
|
|
172
270
|
readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
|
|
@@ -178,7 +276,7 @@ const defaultLabels = {
|
|
|
178
276
|
noResults: "No results",
|
|
179
277
|
reasoningHidden: "The model's reasoning is not shown.",
|
|
180
278
|
allowTool: (name) => `Allow ${name}?`,
|
|
181
|
-
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
279
|
+
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
182
280
|
"Allow ",
|
|
183
281
|
label,
|
|
184
282
|
"?"
|
|
@@ -190,9 +288,14 @@ const defaultLabels = {
|
|
|
190
288
|
openSidebar: "Open sidebar",
|
|
191
289
|
expandSidebar: "Expand sidebar",
|
|
192
290
|
collapseSidebar: "Collapse sidebar",
|
|
291
|
+
resizeSidebar: "Resize sidebar",
|
|
292
|
+
withShortcut: (label, keys) => `${label} (${keys})`,
|
|
293
|
+
ctrlKey: "Ctrl",
|
|
193
294
|
context: "Context",
|
|
194
295
|
contextUsed: "Context used",
|
|
195
296
|
percentUsed: (percent) => `${percent}% used`,
|
|
297
|
+
contextSummary: (used, limit, percent) => `Context: ${used} / ${limit} (${percent}%)`,
|
|
298
|
+
contextLoading: "Context: loading",
|
|
196
299
|
freeSpace: "Free space",
|
|
197
300
|
tokenCount: (count) => `${count} tokens`,
|
|
198
301
|
replyPlaceholder: "Reply…",
|
|
@@ -200,6 +303,11 @@ const defaultLabels = {
|
|
|
200
303
|
message: "Message",
|
|
201
304
|
stop: "Stop",
|
|
202
305
|
send: "Send",
|
|
306
|
+
listening: "Listening…",
|
|
307
|
+
transcribing: "Transcribing…",
|
|
308
|
+
dictate: "Dictate",
|
|
309
|
+
dictationCancel: "Cancel dictation",
|
|
310
|
+
dictationAccept: "Use dictation",
|
|
203
311
|
addFiles: "Add files and more",
|
|
204
312
|
attachments: "Attachments",
|
|
205
313
|
editPastedText: "Edit pasted text",
|
|
@@ -232,6 +340,7 @@ const defaultLabels = {
|
|
|
232
340
|
justNow: "just now",
|
|
233
341
|
copy: "Copy",
|
|
234
342
|
copied: "Copied",
|
|
343
|
+
copyFieldLabel: "Link",
|
|
235
344
|
retry: "Retry",
|
|
236
345
|
edit: "Edit",
|
|
237
346
|
readAloud: "Read aloud",
|
|
@@ -291,27 +400,40 @@ function CopyButton({ text, className }) {
|
|
|
291
400
|
});
|
|
292
401
|
}
|
|
293
402
|
// src/primitives/segmented-control.tsx
|
|
403
|
+
const sizes = {
|
|
404
|
+
sm: {
|
|
405
|
+
button: "h-6",
|
|
406
|
+
iconOnly: "w-7"
|
|
407
|
+
},
|
|
408
|
+
md: {
|
|
409
|
+
button: "h-8",
|
|
410
|
+
iconOnly: "w-9"
|
|
411
|
+
}
|
|
412
|
+
};
|
|
294
413
|
/**
|
|
295
|
-
* One of a few options, side by side on a track,
|
|
414
|
+
* One of a few options, side by side on a track, similar to claude.ai's Chat / Cowork switch. Themed with
|
|
296
415
|
* `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
|
|
297
416
|
* (default `--chat-raised`).
|
|
298
417
|
*/
|
|
299
|
-
function SegmentedControl({ label, options, value, onChange, className }) {
|
|
418
|
+
function SegmentedControl({ label, options, value, onChange, clearable, isDisabled, size = "sm", className }) {
|
|
300
419
|
return /* @__PURE__ */ jsx(ToggleButtonGroup, {
|
|
301
420
|
"aria-label": label,
|
|
302
421
|
selectionMode: "single",
|
|
303
|
-
disallowEmptySelection:
|
|
304
|
-
|
|
422
|
+
disallowEmptySelection: !clearable,
|
|
423
|
+
...isDisabled && { isDisabled },
|
|
424
|
+
selectedKeys: value === null ? [] : [value],
|
|
305
425
|
onSelectionChange: (keys) => {
|
|
306
426
|
const next = [...keys][0];
|
|
307
427
|
if (next !== void 0) onChange(next);
|
|
428
|
+
else if (clearable) onChange(null);
|
|
308
429
|
},
|
|
309
|
-
className:
|
|
430
|
+
className: cx("inline-flex shrink-0 items-center gap-0.5 rounded-chat-md p-0.5", "bg-[var(--chat-segment-track,color-mix(in_srgb,var(--chat-bg)_55%,transparent))]", className),
|
|
310
431
|
children: options.map((o) => {
|
|
311
432
|
const button = /* @__PURE__ */ jsxs(ToggleButton, {
|
|
312
433
|
id: o.id,
|
|
434
|
+
...o.isDisabled && { isDisabled: true },
|
|
313
435
|
...o.iconOnly && { "aria-label": o.label },
|
|
314
|
-
className:
|
|
436
|
+
className: cx("inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-[calc(var(--chat-radius-md)-2px)] font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none transition-colors select-none [&_svg]:size-4", sizes[size].button, o.iconOnly ? sizes[size].iconOnly : "px-2", "data-disabled:cursor-default data-disabled:opacity-40", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "data-selected:bg-[var(--chat-segment-selected,var(--chat-raised))] data-selected:text-chat-fg data-selected:shadow-[inset_0_0_0_1px_var(--chat-border-strong)]"),
|
|
315
437
|
children: [o.icon, !o.iconOnly && o.label]
|
|
316
438
|
}, o.id);
|
|
317
439
|
return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
|
|
@@ -332,7 +454,7 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
332
454
|
className: "m-0 font-normal",
|
|
333
455
|
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
334
456
|
slot: "trigger",
|
|
335
|
-
className:
|
|
457
|
+
className: cx("group/trigger flex max-w-full cursor-pointer items-center gap-1.5 rounded-chat-sm text-left text-chat-muted outline-none", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", summaryClassName),
|
|
336
458
|
children: [
|
|
337
459
|
indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
|
|
338
460
|
className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
|
|
@@ -359,6 +481,11 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
359
481
|
});
|
|
360
482
|
}
|
|
361
483
|
// src/primitives/menu.tsx
|
|
484
|
+
/**
|
|
485
|
+
* Wraps a trigger button and a kit `Popover` (or `Modal`) so that pressing the button toggles it, without
|
|
486
|
+
* importing react-aria. The same component as `DialogTrigger`; use whichever name reads better.
|
|
487
|
+
*/
|
|
488
|
+
const PopoverTrigger = DialogTrigger$1;
|
|
362
489
|
/** A search box above a menu that filters its items, like "Search or create a project". */
|
|
363
490
|
function MenuSearch({ placeholder, children, value, onChange }) {
|
|
364
491
|
const { contains } = useFilter({ sensitivity: "base" });
|
|
@@ -377,41 +504,71 @@ function MenuSearch({ placeholder, children, value, onChange }) {
|
|
|
377
504
|
}), children]
|
|
378
505
|
});
|
|
379
506
|
}
|
|
380
|
-
/**
|
|
381
|
-
function
|
|
507
|
+
/** Viewport narrower than `max` px. False on the server and where `matchMedia` is missing. */
|
|
508
|
+
function useNarrower(max) {
|
|
509
|
+
const query = max === false ? null : `(max-width: ${max - .02}px)`;
|
|
510
|
+
return useSyncExternalStore((notify) => {
|
|
511
|
+
if (!query || typeof window === "undefined" || !window.matchMedia) return () => {};
|
|
512
|
+
const list = window.matchMedia(query);
|
|
513
|
+
list.addEventListener("change", notify);
|
|
514
|
+
return () => list.removeEventListener("change", notify);
|
|
515
|
+
}, () => query && typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false, () => false);
|
|
516
|
+
}
|
|
517
|
+
/** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. On phones it's a bottom sheet. */
|
|
518
|
+
function Popover({ className, offset = 6, sheet = 768, width = "auto", fullWidthOnPhones, children, ...props }) {
|
|
519
|
+
if (useNarrower(sheet)) {
|
|
520
|
+
const { isOpen, onOpenChange } = props;
|
|
521
|
+
return /* @__PURE__ */ jsx(ModalOverlay, {
|
|
522
|
+
...isOpen !== void 0 && { isOpen },
|
|
523
|
+
...onOpenChange && { onOpenChange },
|
|
524
|
+
isDismissable: true,
|
|
525
|
+
className: "fixed inset-0 z-50 flex items-end bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
526
|
+
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
527
|
+
className: cx("flex max-h-[85dvh] w-full flex-col overflow-auto rounded-t-chat-lg border-t border-chat-popover-border bg-chat-popover pb-[env(safe-area-inset-bottom)] font-chat-sans text-chat-fg shadow-chat-popover outline-none", "data-entering:animate-chat-sheet-in data-exiting:animate-chat-sheet-out", className),
|
|
528
|
+
children: typeof children === "function" ? children({
|
|
529
|
+
placement: "bottom",
|
|
530
|
+
trigger: null,
|
|
531
|
+
isEntering: false,
|
|
532
|
+
isExiting: false,
|
|
533
|
+
defaultChildren: void 0
|
|
534
|
+
}) : children
|
|
535
|
+
})
|
|
536
|
+
});
|
|
537
|
+
}
|
|
382
538
|
return /* @__PURE__ */ jsx(Popover$1, {
|
|
383
539
|
offset,
|
|
384
540
|
...props,
|
|
385
|
-
className:
|
|
541
|
+
className: cx("rounded-chat-popover border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover outline-none", "motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out", width === "trigger" && "w-(--trigger-width) [&_[role=menu]]:min-w-0", fullWidthOnPhones && "max-sm:w-[calc(100vw-1.5rem)]", className),
|
|
542
|
+
children
|
|
386
543
|
});
|
|
387
544
|
}
|
|
388
545
|
function Menu({ className, ...props }) {
|
|
389
546
|
return /* @__PURE__ */ jsx(Menu$1, {
|
|
390
547
|
...props,
|
|
391
|
-
className:
|
|
548
|
+
className: cx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
|
|
392
549
|
});
|
|
393
550
|
}
|
|
394
|
-
function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
|
|
551
|
+
function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", indicator = "check", className, ...props }) {
|
|
395
552
|
const icons = useIcons();
|
|
396
553
|
const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
|
|
397
554
|
return /* @__PURE__ */ jsx(MenuItem$1, {
|
|
398
555
|
...props,
|
|
399
556
|
...textValue !== void 0 && { textValue },
|
|
400
|
-
className:
|
|
401
|
-
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
557
|
+
className: cx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none select-none", "data-focused:bg-chat-popover-hover data-open:bg-chat-popover-hover data-disabled:opacity-40", tone === "danger" && "text-chat-danger", className),
|
|
558
|
+
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
402
559
|
icon && /* @__PURE__ */ jsx("span", {
|
|
403
|
-
className:
|
|
560
|
+
className: cx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
|
|
404
561
|
children: icon
|
|
405
562
|
}),
|
|
406
563
|
/* @__PURE__ */ jsxs("span", {
|
|
407
|
-
className:
|
|
564
|
+
className: cx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
|
|
408
565
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
409
566
|
slot: "label",
|
|
410
|
-
className:
|
|
567
|
+
className: cx("truncate", descriptionPlacement === "beside" && "shrink-0"),
|
|
411
568
|
children
|
|
412
569
|
}), description && /* @__PURE__ */ jsx(Text, {
|
|
413
570
|
slot: "description",
|
|
414
|
-
className:
|
|
571
|
+
className: cx("text-chat-muted", descriptionPlacement === "beside" ? "truncate text-(length:--chat-ui-font-size) leading-5" : "text-(length:--chat-ui-font-size-sm) leading-4"),
|
|
415
572
|
children: description
|
|
416
573
|
})]
|
|
417
574
|
}),
|
|
@@ -423,7 +580,12 @@ function MenuItem({ children, icon, description, descriptionPlacement = "below",
|
|
|
423
580
|
className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
424
581
|
children: shortcut
|
|
425
582
|
}),
|
|
426
|
-
selectionMode !== "none" &&
|
|
583
|
+
selectionMode !== "none" && indicator === "switch" && /* @__PURE__ */ jsx("span", {
|
|
584
|
+
"aria-hidden": true,
|
|
585
|
+
"data-selected": isSelected || void 0,
|
|
586
|
+
className: "relative h-4 w-7 shrink-0 rounded-full bg-chat-border-strong transition-colors after:absolute after:top-0.5 after:left-0.5 after:size-3 after:rounded-full after:bg-chat-bg after:transition-transform data-selected:bg-chat-accent data-selected:after:translate-x-3 data-selected:after:bg-chat-accent-fg"
|
|
587
|
+
}),
|
|
588
|
+
selectionMode !== "none" && indicator === "check" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
|
|
427
589
|
className: "text-chat-check [&_svg]:size-4",
|
|
428
590
|
children: icons.check
|
|
429
591
|
}),
|
|
@@ -450,7 +612,7 @@ function MenuSeparator() {
|
|
|
450
612
|
/** Explanatory text inside a menu, above or below the items. Not focusable. */
|
|
451
613
|
function MenuNote({ children, className }) {
|
|
452
614
|
return /* @__PURE__ */ jsx("div", {
|
|
453
|
-
className:
|
|
615
|
+
className: cx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
|
|
454
616
|
children
|
|
455
617
|
});
|
|
456
618
|
}
|
|
@@ -484,7 +646,7 @@ function ContextMenu({ menu, children, className }) {
|
|
|
484
646
|
ref: root,
|
|
485
647
|
onContextMenu,
|
|
486
648
|
onKeyDown,
|
|
487
|
-
className:
|
|
649
|
+
className: cx("relative", className),
|
|
488
650
|
children: [children, /* @__PURE__ */ jsxs(MenuTrigger$1, {
|
|
489
651
|
isOpen: at !== void 0,
|
|
490
652
|
onOpenChange: (open) => !open && setAt(void 0),
|
|
@@ -505,15 +667,23 @@ function ContextMenu({ menu, children, className }) {
|
|
|
505
667
|
});
|
|
506
668
|
}
|
|
507
669
|
// src/primitives/overlay.tsx
|
|
670
|
+
const modalSizes = {
|
|
671
|
+
sm: "max-w-sm",
|
|
672
|
+
md: "max-w-lg",
|
|
673
|
+
lg: "max-w-2xl",
|
|
674
|
+
xl: "max-w-5xl",
|
|
675
|
+
/** Fills the page inside the overlay's padding. */
|
|
676
|
+
full: "h-full max-h-none max-w-none"
|
|
677
|
+
};
|
|
508
678
|
/** A centered dialog over a dimmed page. Escape and clicking outside close it; focus stays inside. */
|
|
509
|
-
function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
|
|
679
|
+
function Modal({ isOpen, onOpenChange, title, size = "lg", placement = "center", children, className, ...props }) {
|
|
510
680
|
return /* @__PURE__ */ jsx(ModalOverlay, {
|
|
511
681
|
isOpen,
|
|
512
682
|
onOpenChange,
|
|
513
683
|
isDismissable: true,
|
|
514
|
-
className: "fixed inset-0 z-50 flex
|
|
684
|
+
className: cx("fixed inset-0 z-50 flex justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out", placement === "top" ? "items-start pt-[10dvh]" : "items-center"),
|
|
515
685
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
516
|
-
className:
|
|
686
|
+
className: cx("flex max-h-[85dvh] w-full flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", modalSizes[size], "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
|
|
517
687
|
children: /* @__PURE__ */ jsxs(Dialog, {
|
|
518
688
|
...props,
|
|
519
689
|
className: "flex min-h-0 flex-1 flex-col outline-none",
|
|
@@ -534,7 +704,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
534
704
|
isDismissable: true,
|
|
535
705
|
className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
536
706
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
537
|
-
className:
|
|
707
|
+
className: cx("fixed inset-y-0 w-[min(20rem,85vw)] bg-chat-sidebar text-chat-fg shadow-chat-popover outline-none", side === "left" ? "left-0 data-entering:animate-chat-drawer-in data-exiting:animate-chat-drawer-out" : "right-0 data-entering:animate-chat-drawer-in-right data-exiting:animate-chat-drawer-out-right", className),
|
|
538
708
|
children: /* @__PURE__ */ jsx(Dialog, {
|
|
539
709
|
...props,
|
|
540
710
|
className: "h-full outline-none",
|
|
@@ -551,7 +721,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
551
721
|
function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
|
|
552
722
|
return /* @__PURE__ */ jsxs("section", {
|
|
553
723
|
"aria-label": props["aria-label"],
|
|
554
|
-
className:
|
|
724
|
+
className: cx("flex min-h-0 min-w-0 flex-col overflow-hidden bg-chat-bg font-chat-sans text-chat-fg", variant === "card" && "rounded-chat-md border border-chat-border", className),
|
|
555
725
|
children: [
|
|
556
726
|
(title || actions) && /* @__PURE__ */ jsxs("header", {
|
|
557
727
|
className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-(length:--chat-ui-font-size) leading-5",
|
|
@@ -718,7 +888,7 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
|
|
|
718
888
|
}), copy && /* @__PURE__ */ jsx(CopyButton, { text: code })]
|
|
719
889
|
}), /* @__PURE__ */ jsx("pre", {
|
|
720
890
|
className: clsx("overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-[1.75] whitespace-pre text-chat-fg", variant === "plain" && "max-h-56", "[scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]"),
|
|
721
|
-
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment
|
|
891
|
+
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
|
|
722
892
|
className: "chat-code-token",
|
|
723
893
|
style: token.htmlStyle,
|
|
724
894
|
children: token.content
|
|
@@ -873,7 +1043,7 @@ const DECLINED = "Declined by the user";
|
|
|
873
1043
|
* retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
|
|
874
1044
|
* wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
|
|
875
1045
|
*/
|
|
876
|
-
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12 }) {
|
|
1046
|
+
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
|
|
877
1047
|
const contextAgent = useAgentRegistry();
|
|
878
1048
|
const registry = agent === void 0 ? contextAgent : agent;
|
|
879
1049
|
const [messages, setMessages] = useState(initialMessages);
|
|
@@ -941,7 +1111,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
941
1111
|
if (ended) commit(ended);
|
|
942
1112
|
}, [commit]);
|
|
943
1113
|
const advanceRef = useRef(() => {});
|
|
944
|
-
const start = useCallback((history, options, continuation) => {
|
|
1114
|
+
const start = useCallback((history, options, continuation, joining) => {
|
|
945
1115
|
const message = history.at(-1);
|
|
946
1116
|
const controller = new AbortController();
|
|
947
1117
|
const current = {
|
|
@@ -957,7 +1127,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
957
1127
|
try {
|
|
958
1128
|
const { transport, conversationId, registry } = latest.current;
|
|
959
1129
|
const tools = registry?.specs();
|
|
960
|
-
|
|
1130
|
+
const events = joining && conversationId !== void 0 && transport.resume ? transport.resume(conversationId, controller.signal) : transport.send({
|
|
961
1131
|
message,
|
|
962
1132
|
history,
|
|
963
1133
|
signal: controller.signal,
|
|
@@ -967,7 +1137,8 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
967
1137
|
...conversationId !== void 0 && { conversationId },
|
|
968
1138
|
...options.model !== void 0 && { model: options.model },
|
|
969
1139
|
...options.effort !== void 0 && { effort: options.effort }
|
|
970
|
-
})
|
|
1140
|
+
});
|
|
1141
|
+
for await (const event of events) {
|
|
971
1142
|
if (controller.signal.aborted) break;
|
|
972
1143
|
if (event.type === "title") {
|
|
973
1144
|
latest.current.onTitle?.(event.title);
|
|
@@ -1139,7 +1310,30 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1139
1310
|
};
|
|
1140
1311
|
start(messages.slice(0, index), options);
|
|
1141
1312
|
}, [start]);
|
|
1142
|
-
|
|
1313
|
+
const joined = useRef(false);
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
if (!resume || joined.current || run.current) return;
|
|
1316
|
+
const { transport, conversationId, messages } = latest.current;
|
|
1317
|
+
if (!transport.resume || conversationId === void 0) return;
|
|
1318
|
+
joined.current = true;
|
|
1319
|
+
const last = messages.at(-1);
|
|
1320
|
+
const history = last?.role === "assistant" && last.status === "streaming" ? messages.slice(0, -1) : messages;
|
|
1321
|
+
loop.current = {
|
|
1322
|
+
options: {},
|
|
1323
|
+
step: 0
|
|
1324
|
+
};
|
|
1325
|
+
start(history, {}, void 0, true);
|
|
1326
|
+
}, [resume, start]);
|
|
1327
|
+
const mounted = useRef(false);
|
|
1328
|
+
useEffect(() => {
|
|
1329
|
+
mounted.current = true;
|
|
1330
|
+
return () => {
|
|
1331
|
+
mounted.current = false;
|
|
1332
|
+
queueMicrotask(() => {
|
|
1333
|
+
if (!mounted.current) run.current?.controller.abort();
|
|
1334
|
+
});
|
|
1335
|
+
};
|
|
1336
|
+
}, []);
|
|
1143
1337
|
return {
|
|
1144
1338
|
messages,
|
|
1145
1339
|
busy,
|
|
@@ -1152,7 +1346,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1152
1346
|
}
|
|
1153
1347
|
// src/hooks/sse-transport.ts
|
|
1154
1348
|
/** A `Transport` for backends that stream `StreamEvent`s over server-sent events (see `formatEvent`). */
|
|
1155
|
-
function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
|
|
1349
|
+
function sseTransport({ url, headers, stopUrl, resumeUrl, fetch: fetchImpl = fetch }) {
|
|
1156
1350
|
const headersFor = async () => ({
|
|
1157
1351
|
"content-type": "application/json",
|
|
1158
1352
|
...Object.fromEntries(new Headers(typeof headers === "function" ? await headers() : headers))
|
|
@@ -1168,6 +1362,20 @@ function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
|
|
|
1168
1362
|
if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status} ${response.statusText}`.trim());
|
|
1169
1363
|
yield* readEventStream(response.body);
|
|
1170
1364
|
},
|
|
1365
|
+
...resumeUrl && { async *resume(conversationId, signal) {
|
|
1366
|
+
const target = typeof resumeUrl === "function" ? resumeUrl(conversationId) : `${resumeUrl}${resumeUrl.includes("?") ? "&" : "?"}conversationId=${encodeURIComponent(conversationId)}`;
|
|
1367
|
+
const { "content-type": _, ...headers } = await headersFor();
|
|
1368
|
+
const response = await fetchImpl(target, {
|
|
1369
|
+
method: "GET",
|
|
1370
|
+
headers: {
|
|
1371
|
+
...headers,
|
|
1372
|
+
accept: "text/event-stream"
|
|
1373
|
+
},
|
|
1374
|
+
signal
|
|
1375
|
+
});
|
|
1376
|
+
if (!response.ok || !response.body) throw new Error(`${target} answered ${response.status} ${response.statusText}`.trim());
|
|
1377
|
+
yield* readEventStream(response.body);
|
|
1378
|
+
} },
|
|
1171
1379
|
...stopUrl && { async stop(runId) {
|
|
1172
1380
|
await fetchImpl(stopUrl, {
|
|
1173
1381
|
method: "POST",
|
|
@@ -1272,6 +1480,30 @@ const SidebarContext = createContext({
|
|
|
1272
1480
|
dismiss: () => {}
|
|
1273
1481
|
});
|
|
1274
1482
|
const useSidebar = () => useContext(SidebarContext);
|
|
1483
|
+
/** State that the parent may own: `value` makes it controlled, otherwise `initial` seeds it. */
|
|
1484
|
+
function useControllable(value, initial, onChange) {
|
|
1485
|
+
const [inner, setInner] = useState(initial);
|
|
1486
|
+
const current = value ?? inner;
|
|
1487
|
+
const latest = useRef({
|
|
1488
|
+
current,
|
|
1489
|
+
value,
|
|
1490
|
+
onChange
|
|
1491
|
+
});
|
|
1492
|
+
latest.current = {
|
|
1493
|
+
current,
|
|
1494
|
+
value,
|
|
1495
|
+
onChange
|
|
1496
|
+
};
|
|
1497
|
+
return [current, useCallback((next) => {
|
|
1498
|
+
const { current, value, onChange } = latest.current;
|
|
1499
|
+
const resolved = typeof next === "function" ? next(current) : next;
|
|
1500
|
+
if (resolved === current) return;
|
|
1501
|
+
if (value === void 0) setInner(resolved);
|
|
1502
|
+
onChange?.(resolved);
|
|
1503
|
+
}, [])];
|
|
1504
|
+
}
|
|
1505
|
+
/** True on Apple platforms, where the shortcut modifier is ⌘ instead of Ctrl. */
|
|
1506
|
+
const isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
|
|
1275
1507
|
// src/components/replacements.tsx
|
|
1276
1508
|
/**
|
|
1277
1509
|
* In-place updates of drawn visuals: a tool call whose input has `replaces: <id>` takes the place of that
|
|
@@ -2185,7 +2417,7 @@ function Code({ node, className, children }) {
|
|
|
2185
2417
|
});
|
|
2186
2418
|
}
|
|
2187
2419
|
const components = {
|
|
2188
|
-
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
2420
|
+
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
|
|
2189
2421
|
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
2190
2422
|
node,
|
|
2191
2423
|
className,
|
|
@@ -2193,12 +2425,39 @@ const components = {
|
|
|
2193
2425
|
})
|
|
2194
2426
|
};
|
|
2195
2427
|
const LinksContext = createContext(void 0);
|
|
2196
|
-
function MarkdownLinksProvider({ renderLink, children }) {
|
|
2428
|
+
function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
|
|
2429
|
+
const value = useMemo(() => ({
|
|
2430
|
+
renderLink,
|
|
2431
|
+
renderLinkList
|
|
2432
|
+
}), [renderLink, renderLinkList]);
|
|
2197
2433
|
return /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
2198
|
-
value
|
|
2434
|
+
value,
|
|
2199
2435
|
children
|
|
2200
2436
|
});
|
|
2201
2437
|
}
|
|
2438
|
+
/** The rendered link an item consists of: an `a` alone, or alone in its paragraph (a loose list). */
|
|
2439
|
+
function soleLink(children) {
|
|
2440
|
+
const nodes = Children.toArray(children).filter((c) => !(typeof c === "string" && !c.trim()));
|
|
2441
|
+
if (nodes.length !== 1 || !isValidElement(nodes[0])) return void 0;
|
|
2442
|
+
const props = nodes[0].props;
|
|
2443
|
+
if (typeof props.href === "string") return {
|
|
2444
|
+
href: props.href,
|
|
2445
|
+
children: props.children
|
|
2446
|
+
};
|
|
2447
|
+
return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
|
|
2448
|
+
}
|
|
2449
|
+
function listOf(ordered, props, renderLinkList) {
|
|
2450
|
+
const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
|
|
2451
|
+
const { children } = props;
|
|
2452
|
+
const links = Children.toArray(children).filter((c) => isValidElement(c)).map((item) => soleLink(item.props.children));
|
|
2453
|
+
const list = /* @__PURE__ */ jsx(Default, { ...props });
|
|
2454
|
+
if (links.length === 0 || links.some((l) => !l)) return list;
|
|
2455
|
+
return renderLinkList({
|
|
2456
|
+
ordered,
|
|
2457
|
+
links,
|
|
2458
|
+
children: list
|
|
2459
|
+
}) ?? list;
|
|
2460
|
+
}
|
|
2202
2461
|
/** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
|
|
2203
2462
|
function Markdown({ children, streaming = false, className, partId }) {
|
|
2204
2463
|
const text = useSmoothText(children, streaming);
|
|
@@ -2229,22 +2488,26 @@ function BlockScope({ id, streaming, children }) {
|
|
|
2229
2488
|
});
|
|
2230
2489
|
}
|
|
2231
2490
|
const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
2232
|
-
const renderLink = useContext(LinksContext);
|
|
2233
|
-
const withLinks = useMemo(() =>
|
|
2491
|
+
const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
|
|
2492
|
+
const withLinks = useMemo(() => ({
|
|
2234
2493
|
...components,
|
|
2235
|
-
a: ({ href, children }) => {
|
|
2494
|
+
...renderLink && { a: ({ href, children }) => {
|
|
2236
2495
|
const custom = href ? renderLink({
|
|
2237
2496
|
href,
|
|
2238
2497
|
children
|
|
2239
2498
|
}) : void 0;
|
|
2240
|
-
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2499
|
+
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment$1, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2241
2500
|
href,
|
|
2242
2501
|
target: "_blank",
|
|
2243
2502
|
rel: "noopener noreferrer",
|
|
2244
2503
|
children
|
|
2245
2504
|
});
|
|
2505
|
+
} },
|
|
2506
|
+
...renderLinkList && {
|
|
2507
|
+
ul: (props) => listOf(false, props, renderLinkList),
|
|
2508
|
+
ol: (props) => listOf(true, props, renderLinkList)
|
|
2246
2509
|
}
|
|
2247
|
-
}
|
|
2510
|
+
}), [renderLink, renderLinkList]);
|
|
2248
2511
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
2249
2512
|
prefix: PREFIX,
|
|
2250
2513
|
mode: animating || fade ? "streaming" : "static",
|
|
@@ -2304,7 +2567,7 @@ function activityLabel(part, labels, view) {
|
|
|
2304
2567
|
*/
|
|
2305
2568
|
function activityHeading(parts, views, labels) {
|
|
2306
2569
|
const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
|
|
2307
|
-
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
2570
|
+
if (waiting) return /* @__PURE__ */ jsxs(Fragment$1, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
2308
2571
|
if (parts.some(isWorking)) {
|
|
2309
2572
|
const last = parts.at(-1);
|
|
2310
2573
|
return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
|
|
@@ -2323,6 +2586,7 @@ function stringOf(value) {
|
|
|
2323
2586
|
}
|
|
2324
2587
|
/** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
|
|
2325
2588
|
const commandTool = {
|
|
2589
|
+
ask: (_, labels) => labels.runCommandAsk,
|
|
2326
2590
|
label: (_, labels) => labels.ranCommandLabel,
|
|
2327
2591
|
hint: (part) => stringOf(recordOf(part.input).description),
|
|
2328
2592
|
summary: (n, labels) => labels.ranCommands(n),
|
|
@@ -2355,6 +2619,7 @@ const commandTool = {
|
|
|
2355
2619
|
* part's display `data`, else its `result`.
|
|
2356
2620
|
*/
|
|
2357
2621
|
const webSearchTool = {
|
|
2622
|
+
ask: (_, labels) => labels.searchWebAsk,
|
|
2358
2623
|
label: (_, labels) => labels.searchedWebLabel,
|
|
2359
2624
|
subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
|
|
2360
2625
|
summary: (n, labels) => labels.searchedWeb(n),
|
|
@@ -2402,6 +2667,7 @@ const webSearchTool = {
|
|
|
2402
2667
|
};
|
|
2403
2668
|
/** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
|
|
2404
2669
|
const webFetchTool = {
|
|
2670
|
+
ask: (_, labels) => labels.readPageAsk,
|
|
2405
2671
|
label: (_, labels) => labels.readPageLabel,
|
|
2406
2672
|
subtitle: (part) => {
|
|
2407
2673
|
const url = stringOf(recordOf(part.input).url);
|
|
@@ -2542,7 +2808,7 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
|
2542
2808
|
}) : /* @__PURE__ */ jsx(Disclosure, {
|
|
2543
2809
|
isExpanded: expanded,
|
|
2544
2810
|
onExpandedChange: setExpanded,
|
|
2545
|
-
summary: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2811
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2546
2812
|
/* @__PURE__ */ jsx(Lead, { live }),
|
|
2547
2813
|
/* @__PURE__ */ jsx(Live, {
|
|
2548
2814
|
live,
|
|
@@ -2576,7 +2842,7 @@ const SHIMMER_MS = 2e3;
|
|
|
2576
2842
|
* shared clock, so every live label sweeps in step and a remount does not restart it.
|
|
2577
2843
|
*/
|
|
2578
2844
|
function Live({ live, children }) {
|
|
2579
|
-
return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2845
|
+
return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
2580
2846
|
}
|
|
2581
2847
|
function Shimmer({ children }) {
|
|
2582
2848
|
const [delay] = useState(() => `-${Math.round(performance.now() % SHIMMER_MS)}ms`);
|
|
@@ -2594,7 +2860,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
2594
2860
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2595
2861
|
isExpanded: expanded,
|
|
2596
2862
|
onExpandedChange,
|
|
2597
|
-
summary: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2863
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2598
2864
|
/* @__PURE__ */ jsx(Lead, { live }),
|
|
2599
2865
|
/* @__PURE__ */ jsx(Live, {
|
|
2600
2866
|
live,
|
|
@@ -2765,11 +3031,19 @@ function useAttachments({ upload } = {}) {
|
|
|
2765
3031
|
toParts
|
|
2766
3032
|
};
|
|
2767
3033
|
}
|
|
3034
|
+
const IMAGE_EXTENSION = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico)$/i;
|
|
3035
|
+
/** Whether the card draws a thumbnail: by mime when given, else by `kind`, else by the file extension of the url or name. */
|
|
3036
|
+
function isImage({ mime, kind, url, name }) {
|
|
3037
|
+
if (mime) return mime.startsWith("image/");
|
|
3038
|
+
if (kind) return kind === "image";
|
|
3039
|
+
const path = url && !url.startsWith("data:") ? url.split(/[?#]/)[0] : "";
|
|
3040
|
+
return url?.startsWith("data:image/") === true || IMAGE_EXTENSION.test(path) || IMAGE_EXTENSION.test(name);
|
|
3041
|
+
}
|
|
2768
3042
|
const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
|
|
2769
3043
|
/** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
|
|
2770
3044
|
function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
|
|
2771
3045
|
const icons = useIcons();
|
|
2772
|
-
const image = a.
|
|
3046
|
+
const image = a.url && isImage(a);
|
|
2773
3047
|
const pasted = a.text !== void 0;
|
|
2774
3048
|
const labels = useLabels();
|
|
2775
3049
|
const uploading = a.status === "uploading";
|
|
@@ -2834,7 +3108,7 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
|
2834
3108
|
const [editing, setEditing] = useState();
|
|
2835
3109
|
const labels = useLabels();
|
|
2836
3110
|
if (attachments.length === 0) return null;
|
|
2837
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
3111
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("ul", {
|
|
2838
3112
|
"aria-label": labels.attachments,
|
|
2839
3113
|
className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
|
|
2840
3114
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
@@ -3004,7 +3278,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
3004
3278
|
if (block.kind === "activity") {
|
|
3005
3279
|
const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
|
|
3006
3280
|
if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
|
|
3007
|
-
return /* @__PURE__ */ jsxs(Fragment
|
|
3281
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
|
|
3008
3282
|
if (p.type !== "tool_call") return null;
|
|
3009
3283
|
const Card = views[p.name].Card;
|
|
3010
3284
|
return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
|
|
@@ -3046,7 +3320,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
3046
3320
|
onPress: onReadAloud,
|
|
3047
3321
|
children: icons.speak
|
|
3048
3322
|
}),
|
|
3049
|
-
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
3323
|
+
onFeedback && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
3050
3324
|
label: labels.goodResponse,
|
|
3051
3325
|
size: "sm",
|
|
3052
3326
|
onPress: () => onFeedback("good"),
|
|
@@ -3100,7 +3374,7 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
3100
3374
|
children: icons.approval
|
|
3101
3375
|
}), /* @__PURE__ */ jsx("span", {
|
|
3102
3376
|
className: "min-w-0 flex-1 font-medium",
|
|
3103
|
-
children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
|
|
3377
|
+
children: view.ask?.(part, labels) ?? labels.allowAction(view.askLabel?.(part, labels) ?? activityLabel(part, labels, view))
|
|
3104
3378
|
})]
|
|
3105
3379
|
}),
|
|
3106
3380
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -3131,6 +3405,16 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
3131
3405
|
// src/components/context-panel.tsx
|
|
3132
3406
|
const FREE_COLOR = "var(--chat-raised)";
|
|
3133
3407
|
const DEFAULT_COLOR = "var(--chat-subtle)";
|
|
3408
|
+
/** Tokens used (reserved and deferred categories excluded) and tokens still free. */
|
|
3409
|
+
function contextTotals(usage) {
|
|
3410
|
+
const counted = usage.categories.filter((c) => !c.deferred);
|
|
3411
|
+
const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
|
|
3412
|
+
const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
|
|
3413
|
+
return {
|
|
3414
|
+
used,
|
|
3415
|
+
free: Math.max(usage.limit - used - reserved, 0)
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3134
3418
|
/**
|
|
3135
3419
|
* What fills the context window: a usage line, a segmented bar, one row per category with its share, and
|
|
3136
3420
|
* expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
|
|
@@ -3139,9 +3423,7 @@ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
|
|
|
3139
3423
|
const labels = useLabels();
|
|
3140
3424
|
const counted = usage.categories.filter((c) => !c.deferred);
|
|
3141
3425
|
const deferred = usage.categories.filter((c) => c.deferred);
|
|
3142
|
-
const used
|
|
3143
|
-
const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
|
|
3144
|
-
const free = Math.max(usage.limit - used - reserved, 0);
|
|
3426
|
+
const { used, free } = contextTotals(usage);
|
|
3145
3427
|
const share = (tokens) => formatShare(tokens / usage.limit);
|
|
3146
3428
|
return /* @__PURE__ */ jsxs(Disclosure, {
|
|
3147
3429
|
summary: title ?? labels.context,
|
|
@@ -3266,11 +3548,363 @@ function GroupList({ group }) {
|
|
|
3266
3548
|
function formatShare(fraction) {
|
|
3267
3549
|
return `${(fraction * 100).toFixed(1)}%`;
|
|
3268
3550
|
}
|
|
3551
|
+
// src/components/context-meter.tsx
|
|
3552
|
+
const SIZE = 16;
|
|
3553
|
+
const STROKE = 2;
|
|
3554
|
+
const RADIUS = 7;
|
|
3555
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
3556
|
+
/**
|
|
3557
|
+
* A small progress ring for the composer toolbar, filled to the share of the context window in use (accent,
|
|
3558
|
+
* then warning, then danger). Hover for the numbers; click or Enter opens the ContextPanel above it.
|
|
3559
|
+
*/
|
|
3560
|
+
function ContextMeter({ usage, tooltip, tooltipNote, isLoading = false, warnAt = .75, dangerAt = .9, placement = "top end", className }) {
|
|
3561
|
+
const labels = useLabels();
|
|
3562
|
+
const { used } = contextTotals(usage);
|
|
3563
|
+
const fraction = usage.limit > 0 ? Math.min(used / usage.limit, 1) : 0;
|
|
3564
|
+
const summary = isLoading ? labels.contextLoading : labels.contextSummary(formatTokens(used), formatTokens(usage.limit), Math.round(fraction * 100));
|
|
3565
|
+
const color = fraction >= dangerAt ? "text-chat-danger" : fraction >= warnAt ? "text-chat-warning" : "text-chat-accent";
|
|
3566
|
+
const button = /* @__PURE__ */ jsx(Button, {
|
|
3567
|
+
variant: "ghost",
|
|
3568
|
+
size: "sm",
|
|
3569
|
+
"aria-label": summary,
|
|
3570
|
+
isDisabled: isLoading,
|
|
3571
|
+
className: clsx("size-7 justify-center px-0 text-chat-muted", isLoading && "data-disabled:opacity-100", className),
|
|
3572
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
3573
|
+
"aria-hidden": true,
|
|
3574
|
+
viewBox: `0 0 ${SIZE} ${SIZE}`,
|
|
3575
|
+
className: clsx("size-4", isLoading && "motion-safe:animate-spin"),
|
|
3576
|
+
fill: "none",
|
|
3577
|
+
strokeWidth: STROKE,
|
|
3578
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
3579
|
+
cx: SIZE / 2,
|
|
3580
|
+
cy: SIZE / 2,
|
|
3581
|
+
r: RADIUS,
|
|
3582
|
+
stroke: "var(--chat-raised)"
|
|
3583
|
+
}), /* @__PURE__ */ jsx("circle", {
|
|
3584
|
+
cx: SIZE / 2,
|
|
3585
|
+
cy: SIZE / 2,
|
|
3586
|
+
r: RADIUS,
|
|
3587
|
+
stroke: "currentColor",
|
|
3588
|
+
strokeLinecap: "round",
|
|
3589
|
+
strokeDasharray: CIRCUMFERENCE,
|
|
3590
|
+
strokeDashoffset: CIRCUMFERENCE * (1 - (isLoading ? .25 : fraction)),
|
|
3591
|
+
transform: `rotate(-90 ${SIZE / 2} ${SIZE / 2})`,
|
|
3592
|
+
className: clsx(color, "transition-[stroke-dashoffset]")
|
|
3593
|
+
})]
|
|
3594
|
+
})
|
|
3595
|
+
});
|
|
3596
|
+
return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
3597
|
+
content: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: tooltip ?? summary }), tooltipNote && /* @__PURE__ */ jsx("div", {
|
|
3598
|
+
className: "text-chat-muted",
|
|
3599
|
+
children: tooltipNote
|
|
3600
|
+
})] }),
|
|
3601
|
+
children: button
|
|
3602
|
+
}), /* @__PURE__ */ jsx(Popover, {
|
|
3603
|
+
placement,
|
|
3604
|
+
className: "w-80 overflow-auto p-3",
|
|
3605
|
+
children: /* @__PURE__ */ jsx(Dialog, {
|
|
3606
|
+
"aria-label": labels.context,
|
|
3607
|
+
className: "outline-none",
|
|
3608
|
+
children: /* @__PURE__ */ jsx(ContextPanel, { usage })
|
|
3609
|
+
})
|
|
3610
|
+
})] });
|
|
3611
|
+
}
|
|
3612
|
+
// src/components/copy-field.tsx
|
|
3613
|
+
/** How long "Copied" stays before the button reads "Copy" again. */
|
|
3614
|
+
const COPIED_MS = 2e3;
|
|
3615
|
+
/**
|
|
3616
|
+
* A read-only, selectable field with a Copy button, for share links. The button says "Copied" for about two
|
|
3617
|
+
* seconds (and announces it to screen readers). Where the clipboard is unavailable or refuses, it selects the
|
|
3618
|
+
* text instead so the reader can copy it by hand.
|
|
3619
|
+
*/
|
|
3620
|
+
function CopyField({ value, "aria-label": ariaLabel, onCopy, className }) {
|
|
3621
|
+
const icons = useIcons();
|
|
3622
|
+
const labels = useLabels();
|
|
3623
|
+
const inputRef = useRef(null);
|
|
3624
|
+
const timer = useRef(void 0);
|
|
3625
|
+
const [copied, setCopied] = useState(false);
|
|
3626
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
3627
|
+
const copy = async () => {
|
|
3628
|
+
try {
|
|
3629
|
+
if (!navigator.clipboard) throw new Error("no clipboard");
|
|
3630
|
+
await navigator.clipboard.writeText(value);
|
|
3631
|
+
} catch {
|
|
3632
|
+
inputRef.current?.focus();
|
|
3633
|
+
inputRef.current?.select();
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
setCopied(true);
|
|
3637
|
+
onCopy?.();
|
|
3638
|
+
clearTimeout(timer.current);
|
|
3639
|
+
timer.current = setTimeout(() => setCopied(false), COPIED_MS);
|
|
3640
|
+
};
|
|
3641
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3642
|
+
className: clsx("flex items-center gap-2 font-chat-sans", className),
|
|
3643
|
+
children: [
|
|
3644
|
+
/* @__PURE__ */ jsx("input", {
|
|
3645
|
+
ref: inputRef,
|
|
3646
|
+
readOnly: true,
|
|
3647
|
+
value,
|
|
3648
|
+
"aria-label": ariaLabel ?? labels.copyFieldLabel,
|
|
3649
|
+
onFocus: (e) => e.currentTarget.select(),
|
|
3650
|
+
className: "h-9 min-w-0 flex-1 rounded-chat-md border border-chat-border bg-chat-bg px-3 text-(length:--chat-ui-font-size) text-chat-fg outline-none focus:border-chat-border-strong"
|
|
3651
|
+
}),
|
|
3652
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
3653
|
+
variant: "subtle",
|
|
3654
|
+
size: "lg",
|
|
3655
|
+
onPress: () => void copy(),
|
|
3656
|
+
children: [/* @__PURE__ */ jsx(IconSlot, { children: copied ? icons.copied : icons.copy }), copied ? labels.copied : labels.copy]
|
|
3657
|
+
}),
|
|
3658
|
+
/* @__PURE__ */ jsx("span", {
|
|
3659
|
+
role: "status",
|
|
3660
|
+
className: "sr-only",
|
|
3661
|
+
children: copied ? labels.copied : ""
|
|
3662
|
+
})
|
|
3663
|
+
]
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
// src/components/dictation.tsx
|
|
3667
|
+
/**
|
|
3668
|
+
* Voice dictation in the composer, similar to claude.ai's: while listening the words fill the box as they are
|
|
3669
|
+
* heard ("Listening…" before the first), a dotted meter follows the voice, and ✕ / ✓ take the send button's
|
|
3670
|
+
* place. The kit draws the state and records; the words come from a transcription model the host plugs in
|
|
3671
|
+
* (`useDictation({ transcribe })`), or from the browser's speech recognition without one.
|
|
3672
|
+
*/
|
|
3673
|
+
/** A row of dots that grow into bars with the voice; flat dots when it is quiet. */
|
|
3674
|
+
function VoiceMeter({ levels, count = 16, className }) {
|
|
3675
|
+
const recent = levels.slice(-count);
|
|
3676
|
+
const dots = [...Array(count - recent.length).fill(0), ...recent];
|
|
3677
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3678
|
+
"aria-hidden": true,
|
|
3679
|
+
className: clsx("flex h-6 items-center gap-[3px] px-1", className),
|
|
3680
|
+
children: dots.map((level, i) => /* @__PURE__ */ jsx("span", {
|
|
3681
|
+
className: "chat-voice-dot w-[3px] rounded-full bg-chat-subtle transition-[height] duration-75",
|
|
3682
|
+
style: { height: `${Math.max(3, Math.min(1, level) * 20)}px` }
|
|
3683
|
+
}, i))
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
/** The mic button that starts dictation; put it in the composer's `trailing`. */
|
|
3687
|
+
function DictationButton({ onPress, isDisabled, className }) {
|
|
3688
|
+
const icons = useIcons();
|
|
3689
|
+
const labels = useLabels();
|
|
3690
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
3691
|
+
label: labels.dictate,
|
|
3692
|
+
onPress,
|
|
3693
|
+
...isDisabled && { isDisabled },
|
|
3694
|
+
...className && { className },
|
|
3695
|
+
children: icons.mic
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
/** The meter, ✕ and ✓: what the composer shows in place of send while dictating. */
|
|
3699
|
+
function DictationControls({ dictation, onAccept, finishing }) {
|
|
3700
|
+
const icons = useIcons();
|
|
3701
|
+
const labels = useLabels();
|
|
3702
|
+
const square = "rounded-[var(--chat-send-radius,var(--chat-radius-md))]! bg-chat-raised border border-chat-border";
|
|
3703
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3704
|
+
className: "flex items-center gap-1",
|
|
3705
|
+
children: [
|
|
3706
|
+
/* @__PURE__ */ jsx(VoiceMeter, { levels: dictation.levels }),
|
|
3707
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
3708
|
+
label: labels.dictationCancel,
|
|
3709
|
+
onPress: dictation.onCancel,
|
|
3710
|
+
className: square,
|
|
3711
|
+
children: icons.dictationCancel
|
|
3712
|
+
}),
|
|
3713
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
3714
|
+
label: labels.dictationAccept,
|
|
3715
|
+
onPress: onAccept,
|
|
3716
|
+
isDisabled: finishing,
|
|
3717
|
+
className: square,
|
|
3718
|
+
children: icons.dictationAccept
|
|
3719
|
+
})
|
|
3720
|
+
]
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
const recognitionClass = () => {
|
|
3724
|
+
if (typeof window === "undefined") return void 0;
|
|
3725
|
+
const w = window;
|
|
3726
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
3727
|
+
};
|
|
3728
|
+
/** Encodes mono samples as 16-bit PCM WAV. */
|
|
3729
|
+
function encodeWav(samples, sampleRate) {
|
|
3730
|
+
const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(44 + samples.length * 2));
|
|
3731
|
+
const text = (at, s) => [...s].forEach((c, i) => view.setUint8(at + i, c.charCodeAt(0)));
|
|
3732
|
+
text(0, "RIFF");
|
|
3733
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
3734
|
+
text(8, "WAVE");
|
|
3735
|
+
text(12, "fmt ");
|
|
3736
|
+
view.setUint32(16, 16, true);
|
|
3737
|
+
view.setUint16(20, 1, true);
|
|
3738
|
+
view.setUint16(22, 1, true);
|
|
3739
|
+
view.setUint32(24, sampleRate, true);
|
|
3740
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
3741
|
+
view.setUint16(32, 2, true);
|
|
3742
|
+
view.setUint16(34, 16, true);
|
|
3743
|
+
text(36, "data");
|
|
3744
|
+
view.setUint32(40, samples.length * 2, true);
|
|
3745
|
+
samples.forEach((v, i) => view.setInt16(44 + i * 2, Math.max(-1, Math.min(1, v)) * 32767, true));
|
|
3746
|
+
return new Blob([view.buffer], { type: "audio/wav" });
|
|
3747
|
+
}
|
|
3748
|
+
/** A recording (whatever MediaRecorder made) as 16 kHz mono WAV, the format transcription APIs all take. */
|
|
3749
|
+
async function toWav(recording) {
|
|
3750
|
+
const decoder = new AudioContext();
|
|
3751
|
+
try {
|
|
3752
|
+
const decoded = await decoder.decodeAudioData(await recording.arrayBuffer());
|
|
3753
|
+
const rate = 16e3;
|
|
3754
|
+
const offline = new OfflineAudioContext(1, Math.max(1, Math.ceil(decoded.duration * rate)), rate);
|
|
3755
|
+
const source = offline.createBufferSource();
|
|
3756
|
+
source.buffer = decoded;
|
|
3757
|
+
source.connect(offline.destination);
|
|
3758
|
+
source.start();
|
|
3759
|
+
return encodeWav((await offline.startRendering()).getChannelData(0), rate);
|
|
3760
|
+
} finally {
|
|
3761
|
+
decoder.close();
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Dictation for the composer: records the mic, draws its level, and fills the box with the words as they
|
|
3766
|
+
* are heard. Plug in your transcription model with `transcribe`; a streaming model can instead drive
|
|
3767
|
+
* `DictationState` directly.
|
|
3768
|
+
*/
|
|
3769
|
+
function useDictation({ transcribe, format = "wav", partialEvery = 1500, lang, onError } = {}) {
|
|
3770
|
+
const [active, setActive] = useState(false);
|
|
3771
|
+
const [transcript, setTranscript] = useState("");
|
|
3772
|
+
const [levels, setLevels] = useState([]);
|
|
3773
|
+
const session = useRef(void 0);
|
|
3774
|
+
const latest = useRef({
|
|
3775
|
+
transcribe,
|
|
3776
|
+
format,
|
|
3777
|
+
partialEvery,
|
|
3778
|
+
lang,
|
|
3779
|
+
onError
|
|
3780
|
+
});
|
|
3781
|
+
latest.current = {
|
|
3782
|
+
transcribe,
|
|
3783
|
+
format,
|
|
3784
|
+
partialEvery,
|
|
3785
|
+
lang,
|
|
3786
|
+
onError
|
|
3787
|
+
};
|
|
3788
|
+
const reset = useCallback(() => {
|
|
3789
|
+
session.current?.stop();
|
|
3790
|
+
session.current = void 0;
|
|
3791
|
+
setActive(false);
|
|
3792
|
+
setTranscript("");
|
|
3793
|
+
setLevels([]);
|
|
3794
|
+
}, []);
|
|
3795
|
+
useEffect(() => reset, [reset]);
|
|
3796
|
+
const start = useCallback(() => {
|
|
3797
|
+
if (session.current || typeof navigator === "undefined" || !navigator.mediaDevices) return;
|
|
3798
|
+
const { transcribe, format, partialEvery, lang, onError } = latest.current;
|
|
3799
|
+
const audioOf = (blob) => format === "wav" ? toWav(blob) : Promise.resolve(blob);
|
|
3800
|
+
const Recognition = transcribe ? void 0 : recognitionClass();
|
|
3801
|
+
if (!transcribe && !Recognition) return;
|
|
3802
|
+
const abort = new AbortController();
|
|
3803
|
+
const cleanups = [() => abort.abort()];
|
|
3804
|
+
const stop = () => cleanups.splice(0).forEach((f) => f());
|
|
3805
|
+
let heard = "";
|
|
3806
|
+
const fail = (error) => {
|
|
3807
|
+
if (abort.signal.aborted) return;
|
|
3808
|
+
onError?.(error);
|
|
3809
|
+
reset();
|
|
3810
|
+
};
|
|
3811
|
+
let recognition;
|
|
3812
|
+
if (Recognition) {
|
|
3813
|
+
recognition = new Recognition();
|
|
3814
|
+
const r = recognition;
|
|
3815
|
+
r.continuous = true;
|
|
3816
|
+
r.interimResults = true;
|
|
3817
|
+
r.lang = lang ?? document.documentElement.lang;
|
|
3818
|
+
r.onresult = (e) => setTranscript(heard = Array.from(e.results, (x) => x[0]?.transcript ?? "").join("").trim());
|
|
3819
|
+
r.onerror = (e) => e.error !== "no-speech" && e.error !== "aborted" && fail(new Error(e.error));
|
|
3820
|
+
r.onend = () => !abort.signal.aborted && r.start();
|
|
3821
|
+
r.start();
|
|
3822
|
+
cleanups.push(() => (r.onend = null, r.abort()));
|
|
3823
|
+
}
|
|
3824
|
+
const chunks = [];
|
|
3825
|
+
let recorder;
|
|
3826
|
+
const ready = navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
|
3827
|
+
if (abort.signal.aborted) return stream.getTracks().forEach((t) => t.stop());
|
|
3828
|
+
cleanups.push(() => stream.getTracks().forEach((t) => t.stop()));
|
|
3829
|
+
const audio = new AudioContext();
|
|
3830
|
+
cleanups.push(() => void audio.close());
|
|
3831
|
+
const analyser = audio.createAnalyser();
|
|
3832
|
+
analyser.fftSize = 512;
|
|
3833
|
+
audio.createMediaStreamSource(stream).connect(analyser);
|
|
3834
|
+
const samples = new Float32Array(analyser.fftSize);
|
|
3835
|
+
let last = 0;
|
|
3836
|
+
let frame = requestAnimationFrame(function tick(now) {
|
|
3837
|
+
frame = requestAnimationFrame(tick);
|
|
3838
|
+
if (now - last < 70) return;
|
|
3839
|
+
last = now;
|
|
3840
|
+
analyser.getFloatTimeDomainData(samples);
|
|
3841
|
+
let sum = 0;
|
|
3842
|
+
for (const v of samples) sum += v * v;
|
|
3843
|
+
setLevels((l) => [...l.slice(-31), Math.min(1, Math.sqrt(sum / samples.length) * 6)]);
|
|
3844
|
+
});
|
|
3845
|
+
cleanups.push(() => cancelAnimationFrame(frame));
|
|
3846
|
+
if (!transcribe) return;
|
|
3847
|
+
const r = new MediaRecorder(stream);
|
|
3848
|
+
recorder = r;
|
|
3849
|
+
r.ondataavailable = (e) => e.data.size && chunks.push(e.data);
|
|
3850
|
+
r.start(250);
|
|
3851
|
+
cleanups.push(() => r.state !== "inactive" && r.stop());
|
|
3852
|
+
if (partialEvery > 0) {
|
|
3853
|
+
let busy = false;
|
|
3854
|
+
const timer = setInterval(() => {
|
|
3855
|
+
if (busy || !chunks.length) return;
|
|
3856
|
+
busy = true;
|
|
3857
|
+
audioOf(new Blob(chunks, { type: r.mimeType })).then((wav) => transcribe(wav, abort.signal)).then((text) => !abort.signal.aborted && setTranscript(heard = text.trim())).catch(() => {}).finally(() => busy = false);
|
|
3858
|
+
}, partialEvery);
|
|
3859
|
+
cleanups.push(() => clearInterval(timer));
|
|
3860
|
+
}
|
|
3861
|
+
}, fail);
|
|
3862
|
+
session.current = {
|
|
3863
|
+
stop,
|
|
3864
|
+
finish: async () => {
|
|
3865
|
+
if (!transcribe) return stop(), heard;
|
|
3866
|
+
await ready;
|
|
3867
|
+
const r = recorder;
|
|
3868
|
+
if (r && r.state !== "inactive") await new Promise((done) => {
|
|
3869
|
+
r.addEventListener("stop", () => done(), { once: true });
|
|
3870
|
+
r.stop();
|
|
3871
|
+
});
|
|
3872
|
+
if (!chunks.length) return stop(), heard;
|
|
3873
|
+
const text = await transcribe(await audioOf(new Blob(chunks, { type: r?.mimeType })), abort.signal);
|
|
3874
|
+
stop();
|
|
3875
|
+
return text.trim();
|
|
3876
|
+
}
|
|
3877
|
+
};
|
|
3878
|
+
setActive(true);
|
|
3879
|
+
}, [reset]);
|
|
3880
|
+
const accept = useCallback(async () => {
|
|
3881
|
+
const s = session.current;
|
|
3882
|
+
if (!s) return;
|
|
3883
|
+
try {
|
|
3884
|
+
return await s.finish();
|
|
3885
|
+
} catch (error) {
|
|
3886
|
+
latest.current.onError?.(error);
|
|
3887
|
+
return;
|
|
3888
|
+
} finally {
|
|
3889
|
+
if (session.current === s) reset();
|
|
3890
|
+
}
|
|
3891
|
+
}, [reset]);
|
|
3892
|
+
return {
|
|
3893
|
+
supported: typeof navigator !== "undefined" && !!navigator.mediaDevices && (!!transcribe || recognitionClass() !== void 0),
|
|
3894
|
+
start,
|
|
3895
|
+
state: active ? {
|
|
3896
|
+
levels,
|
|
3897
|
+
transcript,
|
|
3898
|
+
onCancel: reset,
|
|
3899
|
+
onAccept: accept
|
|
3900
|
+
} : void 0
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3269
3903
|
// src/components/composer.tsx
|
|
3270
3904
|
/** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
|
|
3271
3905
|
const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
|
|
3272
3906
|
/** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
|
|
3273
|
-
function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder, variant = "docked", leading, trailing, above, onFiles, onLongPaste, longPasteChars = 2e3, allowEmpty = false, submitDisabled = false, isDisabled = false, autoFocus, className }) {
|
|
3907
|
+
function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder, variant = "docked", leading, trailing, above, onFiles, onLongPaste, longPasteChars = 2e3, allowEmpty = false, submitDisabled = false, isDisabled = false, dictation, autoFocus, className }) {
|
|
3274
3908
|
const icons = useIcons();
|
|
3275
3909
|
const labels = useLabels();
|
|
3276
3910
|
const [own, setOwn] = useState("");
|
|
@@ -3302,8 +3936,22 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
3302
3936
|
observer.observe(el);
|
|
3303
3937
|
return () => observer.disconnect();
|
|
3304
3938
|
}, [fit]);
|
|
3939
|
+
const [finishing, setFinishing] = useState(false);
|
|
3940
|
+
const joined = (words) => words ? text.trim() ? `${text.replace(/\s+$/, "")} ${words}` : words : text;
|
|
3941
|
+
const acceptDictation = async () => {
|
|
3942
|
+
if (!dictation || finishing) return;
|
|
3943
|
+
const heard = dictation.transcript;
|
|
3944
|
+
setFinishing(true);
|
|
3945
|
+
try {
|
|
3946
|
+
const words = await dictation.onAccept();
|
|
3947
|
+
setText(joined(typeof words === "string" ? words : heard));
|
|
3948
|
+
} finally {
|
|
3949
|
+
setFinishing(false);
|
|
3950
|
+
area.current?.focus();
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3305
3953
|
const [dragging, setDragging] = useState(false);
|
|
3306
|
-
const canSend = !busy && !isDisabled && !submitDisabled && (allowEmpty || text.trim().length > 0);
|
|
3954
|
+
const canSend = !dictation && !busy && !isDisabled && !submitDisabled && (allowEmpty || text.trim().length > 0);
|
|
3307
3955
|
const submit = () => {
|
|
3308
3956
|
if (!canSend) return;
|
|
3309
3957
|
onSubmit(text);
|
|
@@ -3344,7 +3992,11 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
3344
3992
|
}
|
|
3345
3993
|
};
|
|
3346
3994
|
const compact = variant === "compact";
|
|
3347
|
-
const action =
|
|
3995
|
+
const action = dictation ? /* @__PURE__ */ jsx(DictationControls, {
|
|
3996
|
+
dictation,
|
|
3997
|
+
onAccept: () => void acceptDictation(),
|
|
3998
|
+
finishing
|
|
3999
|
+
}) : busy ? /* @__PURE__ */ jsx(IconButton, {
|
|
3348
4000
|
label: labels.stop,
|
|
3349
4001
|
variant: "primary",
|
|
3350
4002
|
onPress: onStop,
|
|
@@ -3374,11 +4026,12 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
3374
4026
|
/* @__PURE__ */ jsx("textarea", {
|
|
3375
4027
|
ref: area,
|
|
3376
4028
|
rows: 1,
|
|
3377
|
-
value: text,
|
|
4029
|
+
value: dictation ? joined(dictation.transcript) : text,
|
|
4030
|
+
readOnly: !!dictation,
|
|
3378
4031
|
onChange: (e) => setText(e.target.value),
|
|
3379
4032
|
onKeyDown,
|
|
3380
4033
|
onPaste,
|
|
3381
|
-
placeholder: placeholder ?? labels.replyPlaceholder,
|
|
4034
|
+
placeholder: dictation ? finishing ? labels.transcribing : labels.listening : placeholder ?? labels.replyPlaceholder,
|
|
3382
4035
|
"aria-label": labels.message,
|
|
3383
4036
|
disabled: isDisabled,
|
|
3384
4037
|
autoFocus,
|
|
@@ -3386,7 +4039,7 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
3386
4039
|
}),
|
|
3387
4040
|
compact ? /* @__PURE__ */ jsxs("div", {
|
|
3388
4041
|
className: "flex shrink-0 items-center gap-1",
|
|
3389
|
-
children: [trailing, action]
|
|
4042
|
+
children: [!dictation && trailing, action]
|
|
3390
4043
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
3391
4044
|
className: "flex items-center gap-1",
|
|
3392
4045
|
children: [
|
|
@@ -3394,7 +4047,7 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
3394
4047
|
className: "flex min-w-0 flex-1 items-center gap-1",
|
|
3395
4048
|
children: leading
|
|
3396
4049
|
}),
|
|
3397
|
-
trailing,
|
|
4050
|
+
!dictation && trailing,
|
|
3398
4051
|
action
|
|
3399
4052
|
]
|
|
3400
4053
|
})
|
|
@@ -3505,11 +4158,11 @@ function EffortSlider({ efforts, value, onChange, label, className }) {
|
|
|
3505
4158
|
]
|
|
3506
4159
|
});
|
|
3507
4160
|
}
|
|
3508
|
-
/** A composer pill ("◎ Medium") that opens the effort slider above it,
|
|
4161
|
+
/** A composer pill ("◎ Medium") that opens the effort slider above it, similar to chatgpt.com's Think button. */
|
|
3509
4162
|
function EffortButton({ icon, children, className, ...slider }) {
|
|
3510
4163
|
const current = slider.efforts.find((e) => e.id === slider.value) ?? slider.efforts[0];
|
|
3511
4164
|
const labels = useLabels();
|
|
3512
|
-
return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
4165
|
+
return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
3513
4166
|
variant: "ghost",
|
|
3514
4167
|
size: "sm",
|
|
3515
4168
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
@@ -3537,6 +4190,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
3537
4190
|
const more = models.filter((m) => m.secondary);
|
|
3538
4191
|
const item = (m) => /* @__PURE__ */ jsx(MenuItem, {
|
|
3539
4192
|
id: m.id,
|
|
4193
|
+
...m.icon && { icon: m.icon },
|
|
3540
4194
|
description: m.disabledReason ?? m.description,
|
|
3541
4195
|
isDisabled: m.disabled ?? false,
|
|
3542
4196
|
children: m.label
|
|
@@ -3551,6 +4205,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
3551
4205
|
"aria-label": labels.modelButton(current?.label ?? model),
|
|
3552
4206
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
3553
4207
|
children: [
|
|
4208
|
+
current?.icon && /* @__PURE__ */ jsx(IconSlot, {
|
|
4209
|
+
className: "text-chat-fg [&_svg]:size-3.5",
|
|
4210
|
+
children: current.icon
|
|
4211
|
+
}),
|
|
3554
4212
|
/* @__PURE__ */ jsx("span", {
|
|
3555
4213
|
className: "min-w-0 truncate text-chat-fg",
|
|
3556
4214
|
children: current?.label ?? model
|
|
@@ -3654,10 +4312,12 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
|
|
|
3654
4312
|
behavior
|
|
3655
4313
|
});
|
|
3656
4314
|
}, []);
|
|
3657
|
-
const
|
|
4315
|
+
const nearBottom = () => {
|
|
3658
4316
|
const el = scroller.current;
|
|
3659
|
-
|
|
3660
|
-
|
|
4317
|
+
return !el || el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
|
|
4318
|
+
};
|
|
4319
|
+
const onScroll = () => {
|
|
4320
|
+
const bottom = nearBottom();
|
|
3661
4321
|
stuck.current = bottom;
|
|
3662
4322
|
setAtBottom(bottom);
|
|
3663
4323
|
};
|
|
@@ -3674,7 +4334,7 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
|
|
|
3674
4334
|
const jumpedFor = useRef(start === "top" ? lastUser : null);
|
|
3675
4335
|
useLayoutEffect(() => {
|
|
3676
4336
|
if (lastUser === jumpedFor.current) {
|
|
3677
|
-
|
|
4337
|
+
setAtBottom(nearBottom());
|
|
3678
4338
|
return;
|
|
3679
4339
|
}
|
|
3680
4340
|
jumpedFor.current = lastUser;
|
|
@@ -3820,7 +4480,7 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
3820
4480
|
children: labels.shareChat
|
|
3821
4481
|
}),
|
|
3822
4482
|
children,
|
|
3823
|
-
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
4483
|
+
onDelete && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
3824
4484
|
id: "delete",
|
|
3825
4485
|
icon: icons.delete,
|
|
3826
4486
|
shortcut: "D",
|
|
@@ -3922,15 +4582,22 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
3922
4582
|
role: "img",
|
|
3923
4583
|
"aria-label": labels.running,
|
|
3924
4584
|
className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
|
|
3925
|
-
}) : chat.unread ? /* @__PURE__ */ jsx("span", {
|
|
4585
|
+
}) : chat.unread && !chat.muted ? /* @__PURE__ */ jsx("span", {
|
|
3926
4586
|
role: "img",
|
|
3927
4587
|
"aria-label": labels.unread,
|
|
3928
4588
|
className: clsx(markerClass, "bg-chat-check")
|
|
3929
4589
|
}) : null;
|
|
3930
|
-
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4590
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4591
|
+
/* @__PURE__ */ jsx("span", {
|
|
4592
|
+
className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && !chat.muted && "font-medium", chat.muted && "text-chat-muted"),
|
|
4593
|
+
children: chat.title
|
|
4594
|
+
}),
|
|
4595
|
+
chat.detail != null && /* @__PURE__ */ jsx("span", {
|
|
4596
|
+
className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums",
|
|
4597
|
+
children: chat.detail
|
|
4598
|
+
}),
|
|
4599
|
+
marker
|
|
4600
|
+
] });
|
|
3934
4601
|
const common = {
|
|
3935
4602
|
className: clsx(rowClass, nested && "pl-8", active && "bg-chat-selected", menu && (menuOpen || active ? "pr-8" : "group-hover/item:pr-8 group-focus-within/item:pr-8"), menu && "[@media(hover:none)]:pr-10"),
|
|
3936
4603
|
"aria-current": active ? "page" : void 0,
|
|
@@ -3967,7 +4634,7 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
3967
4634
|
const holdsActive = project.chats.some((c) => c.id === activeId);
|
|
3968
4635
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
|
|
3969
4636
|
defaultExpanded: project.defaultExpanded ?? holdsActive,
|
|
3970
|
-
children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
4637
|
+
children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
3971
4638
|
className: "m-0 font-normal",
|
|
3972
4639
|
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
3973
4640
|
slot: "trigger",
|
|
@@ -4026,6 +4693,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
4026
4693
|
const labels = useLabels();
|
|
4027
4694
|
const pending = onApprove && approvals?.[0];
|
|
4028
4695
|
const box = (variant) => {
|
|
4696
|
+
if (composer === false) return null;
|
|
4029
4697
|
const element = /* @__PURE__ */ jsx(Composer, {
|
|
4030
4698
|
variant: density === "inline" || short && !empty ? "compact" : variant,
|
|
4031
4699
|
busy,
|
|
@@ -4159,41 +4827,84 @@ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer,
|
|
|
4159
4827
|
});
|
|
4160
4828
|
}
|
|
4161
4829
|
// src/views/app-shell.tsx
|
|
4830
|
+
const DEFAULT_WIDTH = 288;
|
|
4831
|
+
const KEY_STEP = 16;
|
|
4832
|
+
const clampWidth = (px, min, max) => Math.min(max, Math.max(min, Math.round(px)));
|
|
4833
|
+
function readStoredWidth(key) {
|
|
4834
|
+
if (!key) return void 0;
|
|
4835
|
+
try {
|
|
4836
|
+
const n = Number(window.localStorage.getItem(key));
|
|
4837
|
+
return n > 0 ? n : void 0;
|
|
4838
|
+
} catch {
|
|
4839
|
+
return;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4162
4842
|
/**
|
|
4163
4843
|
* A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
|
|
4164
4844
|
* content on phones. Children get `useSidebar()` and `<SidebarToggle>`.
|
|
4165
4845
|
*/
|
|
4166
|
-
function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
4846
|
+
function AppShell({ sidebar, children, defaultCollapsed, collapsed: collapsedProp, onCollapsedChange, scroll = "inner", resizable, width: widthProp, defaultWidth, onWidthChange, persistKey, shortcut = true, className }) {
|
|
4167
4847
|
const drawer = useMediaQuery(DRAWER_QUERY);
|
|
4168
|
-
const
|
|
4848
|
+
const labels = useLabels();
|
|
4849
|
+
const [collapsed, setCollapsed] = useControllable(collapsedProp, () => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches), onCollapsedChange);
|
|
4169
4850
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
4170
|
-
const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer]);
|
|
4851
|
+
const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer, setCollapsed]);
|
|
4171
4852
|
const dismiss = useCallback(() => setDrawerOpen(false), []);
|
|
4853
|
+
const min = resizable?.min ?? 0;
|
|
4854
|
+
const max = resizable?.max ?? Infinity;
|
|
4855
|
+
const [rawWidth, setWidth] = useControllable(widthProp, () => readStoredWidth(persistKey) ?? defaultWidth ?? DEFAULT_WIDTH, (px) => {
|
|
4856
|
+
if (persistKey) try {
|
|
4857
|
+
window.localStorage.setItem(persistKey, String(px));
|
|
4858
|
+
} catch {}
|
|
4859
|
+
onWidthChange?.(px);
|
|
4860
|
+
});
|
|
4861
|
+
const width = resizable ? clampWidth(rawWidth, min, max) : void 0;
|
|
4862
|
+
const shortcutKeys = shortcut ? `${isMacPlatform() ? "⌘" : labels.ctrlKey} B` : void 0;
|
|
4863
|
+
useEffect(() => {
|
|
4864
|
+
if (!shortcut) return;
|
|
4865
|
+
const mac = isMacPlatform();
|
|
4866
|
+
const onKey = (e) => {
|
|
4867
|
+
if (e.defaultPrevented || e.altKey || e.shiftKey || e.key.toLowerCase() !== "b") return;
|
|
4868
|
+
if (mac ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return;
|
|
4869
|
+
e.preventDefault();
|
|
4870
|
+
toggle();
|
|
4871
|
+
};
|
|
4872
|
+
window.addEventListener("keydown", onKey);
|
|
4873
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
4874
|
+
}, [shortcut, toggle]);
|
|
4172
4875
|
const pinned = useMemo(() => ({
|
|
4173
4876
|
collapsed,
|
|
4174
4877
|
drawer,
|
|
4175
4878
|
drawerOpen,
|
|
4176
4879
|
toggle,
|
|
4177
|
-
dismiss
|
|
4880
|
+
dismiss,
|
|
4881
|
+
...shortcutKeys && { shortcut: shortcutKeys }
|
|
4178
4882
|
}), [
|
|
4179
4883
|
collapsed,
|
|
4180
4884
|
drawer,
|
|
4181
4885
|
drawerOpen,
|
|
4182
4886
|
toggle,
|
|
4183
|
-
dismiss
|
|
4887
|
+
dismiss,
|
|
4888
|
+
shortcutKeys
|
|
4184
4889
|
]);
|
|
4185
|
-
const labels = useLabels();
|
|
4186
4890
|
const inDrawer = useMemo(() => ({
|
|
4187
4891
|
...pinned,
|
|
4188
4892
|
collapsed: false
|
|
4189
4893
|
}), [pinned]);
|
|
4894
|
+
const windowScroll = scroll === "window";
|
|
4190
4895
|
return /* @__PURE__ */ jsxs(SidebarContext.Provider, {
|
|
4191
4896
|
value: pinned,
|
|
4192
4897
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
4193
|
-
className: clsx("flex
|
|
4194
|
-
children: [!drawer && /* @__PURE__ */
|
|
4195
|
-
|
|
4196
|
-
|
|
4898
|
+
className: clsx("flex bg-chat-bg text-chat-fg", windowScroll ? "min-h-dvh" : "h-dvh overflow-hidden", className),
|
|
4899
|
+
children: [!drawer && /* @__PURE__ */ jsxs("aside", {
|
|
4900
|
+
style: width ? { "--chat-sidebar-width": `${width}px` } : void 0,
|
|
4901
|
+
className: clsx("relative shrink-0 border-r border-chat-divider bg-chat-sidebar transition-[width] duration-200 ease-out motion-reduce:transition-none data-resizing:transition-none", windowScroll ? "sticky top-0 h-dvh self-start overflow-clip" : "overflow-hidden", collapsed ? "w-13" : "w-(--chat-sidebar-width,18rem)"),
|
|
4902
|
+
children: [sidebar, resizable && !collapsed && width && /* @__PURE__ */ jsx(ResizeHandle, {
|
|
4903
|
+
width,
|
|
4904
|
+
min,
|
|
4905
|
+
max,
|
|
4906
|
+
onCommit: setWidth
|
|
4907
|
+
})]
|
|
4197
4908
|
}), /* @__PURE__ */ jsx("main", {
|
|
4198
4909
|
className: "flex min-w-0 flex-1 flex-col",
|
|
4199
4910
|
children
|
|
@@ -4209,15 +4920,58 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
4209
4920
|
})]
|
|
4210
4921
|
});
|
|
4211
4922
|
}
|
|
4212
|
-
/**
|
|
4923
|
+
/** The draggable right edge of the sidebar. A drag moves the aside's width variable alone and commits on release. */
|
|
4924
|
+
function ResizeHandle({ width, min, max, onCommit }) {
|
|
4925
|
+
const labels = useLabels();
|
|
4926
|
+
const live = useRef(null);
|
|
4927
|
+
const aside = (el) => el.closest("aside");
|
|
4928
|
+
const move = (el, px) => {
|
|
4929
|
+
live.current = clampWidth(px, min, max);
|
|
4930
|
+
aside(el).style.setProperty("--chat-sidebar-width", `${live.current}px`);
|
|
4931
|
+
};
|
|
4932
|
+
const end = (el) => {
|
|
4933
|
+
delete aside(el).dataset.resizing;
|
|
4934
|
+
if (live.current != null) onCommit(live.current);
|
|
4935
|
+
live.current = null;
|
|
4936
|
+
};
|
|
4937
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4938
|
+
role: "separator",
|
|
4939
|
+
"aria-orientation": "vertical",
|
|
4940
|
+
"aria-label": labels.resizeSidebar,
|
|
4941
|
+
"aria-valuenow": width,
|
|
4942
|
+
"aria-valuemin": min,
|
|
4943
|
+
"aria-valuemax": max,
|
|
4944
|
+
tabIndex: 0,
|
|
4945
|
+
className: "group/handle absolute inset-y-0 right-0 z-10 w-1.5 cursor-col-resize touch-none outline-none",
|
|
4946
|
+
onPointerDown: (e) => {
|
|
4947
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
4948
|
+
aside(e.currentTarget).dataset.resizing = "";
|
|
4949
|
+
live.current = null;
|
|
4950
|
+
},
|
|
4951
|
+
onPointerMove: (e) => {
|
|
4952
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) move(e.currentTarget, e.clientX - aside(e.currentTarget).getBoundingClientRect().left);
|
|
4953
|
+
},
|
|
4954
|
+
onPointerUp: (e) => end(e.currentTarget),
|
|
4955
|
+
onPointerCancel: (e) => end(e.currentTarget),
|
|
4956
|
+
onKeyDown: (e) => {
|
|
4957
|
+
const step = e.shiftKey ? 64 : KEY_STEP;
|
|
4958
|
+
const next = e.key === "ArrowLeft" ? width - step : e.key === "ArrowRight" ? width + step : e.key === "Home" ? min : e.key === "End" ? max : null;
|
|
4959
|
+
if (next == null) return;
|
|
4960
|
+
e.preventDefault();
|
|
4961
|
+
onCommit(clampWidth(next, min, max));
|
|
4962
|
+
},
|
|
4963
|
+
children: /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 right-0 w-px bg-transparent transition-colors group-hover/handle:bg-chat-border-strong group-focus-visible/handle:w-0.5 group-focus-visible/handle:bg-chat-focus in-data-resizing:bg-chat-focus" })
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
/** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. Its label names the shortcut, e.g. "(⌘ B)". */
|
|
4213
4967
|
function SidebarToggle({ drawerOnly = false, className }) {
|
|
4214
4968
|
const icons = useIcons();
|
|
4215
|
-
const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
|
|
4969
|
+
const { drawer, drawerOpen, collapsed, toggle, shortcut } = useSidebar();
|
|
4216
4970
|
const labels = useLabels();
|
|
4217
4971
|
if (drawerOnly && !drawer) return null;
|
|
4218
4972
|
const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
|
|
4219
4973
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
4220
|
-
label,
|
|
4974
|
+
label: shortcut ? labels.withShortcut(label, shortcut) : label,
|
|
4221
4975
|
onPress: toggle,
|
|
4222
4976
|
className,
|
|
4223
4977
|
children: icons.sidebar
|
|
@@ -4225,7 +4979,7 @@ function SidebarToggle({ drawerOnly = false, className }) {
|
|
|
4225
4979
|
}
|
|
4226
4980
|
// src/views/sidebar.tsx
|
|
4227
4981
|
/** The claude.ai sidebar frame. Collapses to an icon rail inside `AppShell`; a drawer on phones. */
|
|
4228
|
-
function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, className }) {
|
|
4982
|
+
function Sidebar({ brand, brandIcon, headerActions, railActions, nav, children, footer, className }) {
|
|
4229
4983
|
const { collapsed } = useSidebar();
|
|
4230
4984
|
return /* @__PURE__ */ jsxs("div", {
|
|
4231
4985
|
className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
|
|
@@ -4251,8 +5005,12 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
|
|
|
4251
5005
|
/* @__PURE__ */ jsx(SidebarToggle, {})
|
|
4252
5006
|
]
|
|
4253
5007
|
}),
|
|
4254
|
-
|
|
4255
|
-
className:
|
|
5008
|
+
collapsed && railActions && /* @__PURE__ */ jsx("div", {
|
|
5009
|
+
className: "flex shrink-0 flex-col items-center gap-1 pb-2",
|
|
5010
|
+
children: railActions
|
|
5011
|
+
}),
|
|
5012
|
+
nav && /* @__PURE__ */ jsx(SidebarList, {
|
|
5013
|
+
className: clsx("shrink-0 px-2 pb-2", collapsed && "items-center"),
|
|
4256
5014
|
children: nav
|
|
4257
5015
|
}),
|
|
4258
5016
|
!collapsed && /* @__PURE__ */ jsx("div", {
|
|
@@ -4267,32 +5025,65 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
|
|
|
4267
5025
|
]
|
|
4268
5026
|
});
|
|
4269
5027
|
}
|
|
4270
|
-
/**
|
|
4271
|
-
function
|
|
5028
|
+
/** The `<ul>` for `SidebarItem`s outside `Sidebar`'s `nav` slot (e.g. inside a section): no bullets, no padding. */
|
|
5029
|
+
function SidebarList({ children, className }) {
|
|
5030
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
5031
|
+
className: clsx("m-0 flex list-none flex-col gap-px p-0", className),
|
|
5032
|
+
children
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
const iconSize = "[&_svg]:size-(--chat-sidebar-icon-size,1rem) [&_svg]:shrink-0";
|
|
5036
|
+
/**
|
|
5037
|
+
* A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked.
|
|
5038
|
+
* Icons are 1rem, or `--chat-sidebar-icon-size`.
|
|
5039
|
+
*/
|
|
5040
|
+
function SidebarItem({ icon, label, onPress, href, active, shortcut, badge, description, trailing, title, className: extra, ...rest }) {
|
|
4272
5041
|
const { collapsed, dismiss } = useSidebar();
|
|
4273
|
-
const
|
|
5042
|
+
const data = Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
|
|
5043
|
+
const hasTrailing = !collapsed && trailing != null;
|
|
5044
|
+
const className = clsx("group flex min-h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none [@media(pointer:coarse)]:min-h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", iconSize, collapsed ? "relative w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", description && !collapsed && "py-1", hasTrailing && "group-hover/item:pr-9 group-focus-within/item:pr-9 [@media(hover:none)]:pr-10", active && "bg-chat-selected");
|
|
4274
5045
|
const press = () => {
|
|
4275
5046
|
onPress?.();
|
|
4276
5047
|
dismiss();
|
|
4277
5048
|
};
|
|
4278
|
-
const
|
|
5049
|
+
const showIfCut = (e) => {
|
|
5050
|
+
const el = e.currentTarget;
|
|
5051
|
+
el.title = el.scrollWidth > el.clientWidth ? label : "";
|
|
5052
|
+
};
|
|
5053
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4279
5054
|
icon,
|
|
4280
|
-
collapsed ? /* @__PURE__ */
|
|
5055
|
+
collapsed ? /* @__PURE__ */ jsxs("span", {
|
|
4281
5056
|
className: "sr-only",
|
|
4282
|
-
children: label
|
|
4283
|
-
}) : /* @__PURE__ */
|
|
4284
|
-
className: "min-w-0 flex-1
|
|
4285
|
-
children:
|
|
5057
|
+
children: [label, (typeof badge === "string" || typeof badge === "number") && ` (${badge})`]
|
|
5058
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
5059
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
5060
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5061
|
+
className: "truncate",
|
|
5062
|
+
onPointerEnter: title ? void 0 : showIfCut,
|
|
5063
|
+
children: label
|
|
5064
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
5065
|
+
className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
5066
|
+
children: description
|
|
5067
|
+
})]
|
|
4286
5068
|
}),
|
|
4287
5069
|
!collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
|
|
4288
5070
|
className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
|
|
4289
5071
|
children: shortcut
|
|
5072
|
+
}),
|
|
5073
|
+
!collapsed && badge != null && /* @__PURE__ */ jsx("span", {
|
|
5074
|
+
className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted tabular-nums",
|
|
5075
|
+
children: badge
|
|
5076
|
+
}),
|
|
5077
|
+
collapsed && badge != null && /* @__PURE__ */ jsx("span", {
|
|
5078
|
+
"aria-hidden": true,
|
|
5079
|
+
className: "absolute top-1 right-1 size-2 rounded-full bg-chat-accent"
|
|
4290
5080
|
})
|
|
4291
5081
|
] });
|
|
4292
5082
|
const common = {
|
|
4293
5083
|
className,
|
|
4294
5084
|
onPress: press,
|
|
4295
|
-
"aria-current": active ? "page" : void 0
|
|
5085
|
+
"aria-current": active ? "page" : void 0,
|
|
5086
|
+
...title && { title }
|
|
4296
5087
|
};
|
|
4297
5088
|
const control = href ? /* @__PURE__ */ jsx(Link, {
|
|
4298
5089
|
...common,
|
|
@@ -4302,11 +5093,18 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
4302
5093
|
...common,
|
|
4303
5094
|
children: content
|
|
4304
5095
|
});
|
|
4305
|
-
return /* @__PURE__ */
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
children:
|
|
4309
|
-
|
|
5096
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
5097
|
+
...data,
|
|
5098
|
+
className: clsx("group/item relative list-none", extra),
|
|
5099
|
+
children: [collapsed ? /* @__PURE__ */ jsx(Tooltip, {
|
|
5100
|
+
content: label,
|
|
5101
|
+
placement: "right",
|
|
5102
|
+
children: control
|
|
5103
|
+
}) : control, hasTrailing && /* @__PURE__ */ jsx("div", {
|
|
5104
|
+
className: "absolute top-1/2 right-1 flex -translate-y-1/2 items-center opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100",
|
|
5105
|
+
children: trailing
|
|
5106
|
+
})]
|
|
5107
|
+
});
|
|
4310
5108
|
}
|
|
4311
5109
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
4312
5110
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
@@ -4325,7 +5123,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
4325
5123
|
const button = /* @__PURE__ */ jsxs(Button$1, {
|
|
4326
5124
|
"aria-label": collapsed ? name : void 0,
|
|
4327
5125
|
className: clsx("flex h-12 w-full cursor-pointer items-center gap-2.5 rounded-chat-md text-left outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", collapsed ? "justify-center" : "px-1.5"),
|
|
4328
|
-
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
5126
|
+
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
|
|
4329
5127
|
className: "flex min-w-0 flex-1 flex-col",
|
|
4330
5128
|
children: [/* @__PURE__ */ jsx("span", {
|
|
4331
5129
|
className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
@@ -4345,4 +5143,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
4345
5143
|
children: menu
|
|
4346
5144
|
})] });
|
|
4347
5145
|
}
|
|
4348
|
-
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, CopyButton, DRAWER_QUERY, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, SegmentedControl, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|
|
5146
|
+
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|