agentic-ui-kit 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -2
- package/dist/core.js +96 -1
- package/dist/index.js +1482 -304
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +33 -3
- package/dist/types/core/src/index.d.ts +8 -1
- package/dist/types/core/src/partial-json.d.ts +8 -0
- package/dist/types/core/src/types.d.ts +5 -0
- package/dist/types/core/src/ui-markup.d.ts +92 -0
- package/dist/types/core/src/ui-tree.d.ts +110 -0
- package/dist/types/core/src/visualize.d.ts +60 -0
- package/dist/types/react/src/components/chat-notice.d.ts +11 -0
- package/dist/types/react/src/components/composer.d.ts +7 -1
- package/dist/types/react/src/components/markdown.d.ts +19 -1
- package/dist/types/react/src/components/message-list.d.ts +10 -2
- package/dist/types/react/src/components/messages.d.ts +25 -4
- package/dist/types/react/src/components/tools.d.ts +20 -7
- package/dist/types/react/src/components/ui-tree.d.ts +87 -0
- package/dist/types/react/src/components/widget-frame.d.ts +97 -0
- package/dist/types/react/src/index.d.ts +23 -4
- package/dist/types/react/src/primitives/button.d.ts +10 -1
- package/dist/types/react/src/primitives/chat-theme.d.ts +13 -1
- package/dist/types/react/src/primitives/icons.d.ts +95 -0
- package/dist/types/react/src/primitives/labels.d.ts +193 -0
- package/dist/types/react/src/primitives/menu.d.ts +3 -1
- package/dist/types/react/src/primitives/segmented-control.d.ts +22 -0
- package/dist/types/react/src/views/chat-transcript.d.ts +27 -0
- package/dist/types/react/src/views/chat-view.d.ts +20 -3
- package/dist/ui-markup-DtMYer7I.js +1082 -0
- package/package.json +1 -1
- package/dist/sse-BA_ebMcH.js +0 -408
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { A as salvageJson, C as awaitingToolCalls, O as settleToolCall, S as applyEvent, b as toBlocks, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, v as readEventStream, x as activityTokens } from "./ui-markup-DtMYer7I.js";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
|
-
import { Autocomplete, Button as Button$1, Dialog, 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, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
|
|
4
|
+
import { Autocomplete, Button as Button$1, Dialog, 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";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
7
|
-
import { Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
7
|
+
import { Component, Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
8
8
|
import { UNSAFE_PortalProvider } from "react-aria";
|
|
9
9
|
import { createMathPlugin } from "@streamdown/math";
|
|
10
10
|
import { Streamdown, defaultRehypePlugins } from "streamdown";
|
|
@@ -16,7 +16,7 @@ function Tooltip({ content, children, placement = "top", delay = 600 }) {
|
|
|
16
16
|
children: [children, /* @__PURE__ */ jsx(Tooltip$1, {
|
|
17
17
|
placement,
|
|
18
18
|
offset: 6,
|
|
19
|
-
className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-
|
|
19
|
+
className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-fg shadow-chat-popover motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out",
|
|
20
20
|
children: content
|
|
21
21
|
})]
|
|
22
22
|
});
|
|
@@ -27,25 +27,34 @@ const variants = {
|
|
|
27
27
|
/** Plain text button, highlights on hover. Sidebar rows, header buttons. */
|
|
28
28
|
ghost: "text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
|
|
29
29
|
/** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
|
|
30
|
-
subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
|
|
31
|
-
/**
|
|
32
|
-
|
|
30
|
+
subtle: "bg-chat-surface font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
|
|
31
|
+
/** A thin border, no fill: secondary actions next to a primary one, e.g. Cancel or Share. */
|
|
32
|
+
outline: "border border-chat-border-strong font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
|
|
33
|
+
/** The send button and the one main action of a dialog. */
|
|
34
|
+
primary: "bg-chat-accent font-medium text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95",
|
|
35
|
+
/**
|
|
36
|
+
* Destructive confirmations, e.g. Delete chat. Fill --chat-danger-bg (default: --chat-danger, darkened so it
|
|
37
|
+
* doesn't read as the accent), text --chat-danger-fg (default white).
|
|
38
|
+
*/
|
|
39
|
+
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"
|
|
33
40
|
};
|
|
34
41
|
const sizes = {
|
|
35
|
-
sm: "h-7 gap-1.5 px-2 text-
|
|
36
|
-
md: "h-8 gap-2 px-2.5 text-
|
|
42
|
+
sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
|
|
43
|
+
md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
|
|
44
|
+
/** Dialog and empty-state actions. */
|
|
45
|
+
lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
|
|
37
46
|
};
|
|
38
47
|
function Button({ variant = "ghost", size = "md", className, ...props }) {
|
|
39
48
|
return /* @__PURE__ */ jsx(Button$1, {
|
|
40
49
|
...props,
|
|
41
|
-
className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
|
|
50
|
+
className: clsx("inline-flex cursor-pointer items-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, variants[variant], sizes[size], className)
|
|
42
51
|
});
|
|
43
52
|
}
|
|
44
53
|
function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...props }) {
|
|
45
54
|
const button = /* @__PURE__ */ jsx(Button$1, {
|
|
46
55
|
...props,
|
|
47
56
|
"aria-label": label,
|
|
48
|
-
className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm
|
|
57
|
+
className: clsx("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, variants[variant], variant === "ghost" && "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)
|
|
49
58
|
});
|
|
50
59
|
return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
|
|
51
60
|
content: label,
|
|
@@ -61,10 +70,41 @@ const tones = {
|
|
|
61
70
|
};
|
|
62
71
|
function Badge({ children, tone = "outline", icon, className }) {
|
|
63
72
|
return /* @__PURE__ */ jsxs("span", {
|
|
64
|
-
className: clsx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-
|
|
73
|
+
className: clsx("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),
|
|
65
74
|
children: [icon, children]
|
|
66
75
|
});
|
|
67
76
|
}
|
|
77
|
+
// src/primitives/segmented-control.tsx
|
|
78
|
+
/**
|
|
79
|
+
* One of a few options, side by side on a track, like claude.ai's Chat / Cowork switch. Themed with
|
|
80
|
+
* `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
|
|
81
|
+
* (default `--chat-raised`).
|
|
82
|
+
*/
|
|
83
|
+
function SegmentedControl({ label, options, value, onChange, className }) {
|
|
84
|
+
return /* @__PURE__ */ jsx(ToggleButtonGroup, {
|
|
85
|
+
"aria-label": label,
|
|
86
|
+
selectionMode: "single",
|
|
87
|
+
disallowEmptySelection: true,
|
|
88
|
+
selectedKeys: [value],
|
|
89
|
+
onSelectionChange: (keys) => {
|
|
90
|
+
const next = [...keys][0];
|
|
91
|
+
if (next !== void 0) onChange(next);
|
|
92
|
+
},
|
|
93
|
+
className: clsx("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),
|
|
94
|
+
children: options.map((o) => {
|
|
95
|
+
const button = /* @__PURE__ */ jsxs(ToggleButton, {
|
|
96
|
+
id: o.id,
|
|
97
|
+
...o.iconOnly && { "aria-label": o.label },
|
|
98
|
+
className: clsx("inline-flex h-6 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", o.iconOnly ? "w-7" : "px-2", "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)]"),
|
|
99
|
+
children: [o.icon, !o.iconOnly && o.label]
|
|
100
|
+
}, o.id);
|
|
101
|
+
return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
|
|
102
|
+
content: o.label,
|
|
103
|
+
children: button
|
|
104
|
+
}, o.id) : button;
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
}
|
|
68
108
|
// src/primitives/caret.tsx
|
|
69
109
|
/** The small filled triangle claude.ai uses after expandable labels. Points right; rotate to point down. */
|
|
70
110
|
function Caret({ className }) {
|
|
@@ -75,9 +115,77 @@ function Caret({ className }) {
|
|
|
75
115
|
children: /* @__PURE__ */ jsx("path", { d: "M2 1.2 6.2 4 2 6.8z" })
|
|
76
116
|
});
|
|
77
117
|
}
|
|
118
|
+
// src/primitives/icons.tsx
|
|
119
|
+
/** Lucide icons, plus the claude.ai caret. Unsized: the places that show them size them with CSS. */
|
|
120
|
+
const defaultIcons = {
|
|
121
|
+
copy: /* @__PURE__ */ jsx(Copy, {}),
|
|
122
|
+
copied: /* @__PURE__ */ jsx(Check, {}),
|
|
123
|
+
retry: /* @__PURE__ */ jsx(RefreshCw, {}),
|
|
124
|
+
edit: /* @__PURE__ */ jsx(Pencil, {}),
|
|
125
|
+
speak: /* @__PURE__ */ jsx(Volume2, {}),
|
|
126
|
+
thumbsUp: /* @__PURE__ */ jsx(ThumbsUp, {}),
|
|
127
|
+
thumbsDown: /* @__PURE__ */ jsx(ThumbsDown, {}),
|
|
128
|
+
send: /* @__PURE__ */ jsx(ArrowUp, {}),
|
|
129
|
+
stop: /* @__PURE__ */ jsx(Square, {}),
|
|
130
|
+
attach: /* @__PURE__ */ jsx(Plus, {}),
|
|
131
|
+
scrollToBottom: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
132
|
+
tokens: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
133
|
+
sidebar: /* @__PURE__ */ jsx(PanelLeft, {}),
|
|
134
|
+
switcher: /* @__PURE__ */ jsx(ChevronsUpDown, {}),
|
|
135
|
+
dropdown: /* @__PURE__ */ jsx(ChevronDown, {}),
|
|
136
|
+
expand: /* @__PURE__ */ jsx(ChevronRight, {}),
|
|
137
|
+
caret: /* @__PURE__ */ jsx(Caret, {}),
|
|
138
|
+
submenu: /* @__PURE__ */ jsx(ChevronRight, {}),
|
|
139
|
+
check: /* @__PURE__ */ jsx(Check, {}),
|
|
140
|
+
info: /* @__PURE__ */ jsx(Info, {}),
|
|
141
|
+
warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
|
|
142
|
+
approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
|
|
143
|
+
web: /* @__PURE__ */ jsx(Globe, {}),
|
|
144
|
+
pin: /* @__PURE__ */ jsx(Pin, {}),
|
|
145
|
+
unpin: /* @__PURE__ */ jsx(PinOff, {}),
|
|
146
|
+
rename: /* @__PURE__ */ jsx(Pencil, {}),
|
|
147
|
+
addToProject: /* @__PURE__ */ jsx(FolderPlus, {}),
|
|
148
|
+
add: /* @__PURE__ */ jsx(Plus, {}),
|
|
149
|
+
share: /* @__PURE__ */ jsx(Share, {}),
|
|
150
|
+
delete: /* @__PURE__ */ jsx(Trash2, {}),
|
|
151
|
+
more: /* @__PURE__ */ jsx(MoreHorizontal, {}),
|
|
152
|
+
folder: /* @__PURE__ */ jsx(Folder, {}),
|
|
153
|
+
folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
|
|
154
|
+
file: /* @__PURE__ */ jsx(FileText, {}),
|
|
155
|
+
remove: /* @__PURE__ */ jsx(X, {})
|
|
156
|
+
};
|
|
157
|
+
const IconsContext = createContext(defaultIcons);
|
|
158
|
+
/** Merges `icons` over the icons of the enclosing provider (or the defaults). */
|
|
159
|
+
function IconsProvider({ icons, children }) {
|
|
160
|
+
const parent = useIcons();
|
|
161
|
+
const merged = useMemo(() => icons ? {
|
|
162
|
+
...parent,
|
|
163
|
+
...icons
|
|
164
|
+
} : parent, [parent, icons]);
|
|
165
|
+
return /* @__PURE__ */ jsx(IconsContext.Provider, {
|
|
166
|
+
value: merged,
|
|
167
|
+
children
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** The active icons: the enclosing `ChatTheme`'s `icons` over the defaults. */
|
|
171
|
+
function useIcons() {
|
|
172
|
+
return useContext(IconsContext);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sizes and colors an icon where no button does it already. Pass the size as `[&_svg]:size-4` so it
|
|
176
|
+
* reaches whatever SVG the host supplies.
|
|
177
|
+
*/
|
|
178
|
+
function IconSlot({ children, className }) {
|
|
179
|
+
return /* @__PURE__ */ jsx("span", {
|
|
180
|
+
"aria-hidden": true,
|
|
181
|
+
className: clsx("inline-flex shrink-0 items-center justify-center", className),
|
|
182
|
+
children
|
|
183
|
+
});
|
|
184
|
+
}
|
|
78
185
|
// src/primitives/disclosure.tsx
|
|
79
186
|
/** A row that expands to show more, like "Recalled memory ▸". */
|
|
80
187
|
function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
|
|
188
|
+
const icons = useIcons();
|
|
81
189
|
return /* @__PURE__ */ jsxs(Disclosure$1, {
|
|
82
190
|
...props,
|
|
83
191
|
...className !== void 0 && { className },
|
|
@@ -87,15 +195,18 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
87
195
|
slot: "trigger",
|
|
88
196
|
className: clsx("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),
|
|
89
197
|
children: [
|
|
90
|
-
indicator === "chevron" && /* @__PURE__ */ jsx(
|
|
91
|
-
"aria-
|
|
92
|
-
|
|
198
|
+
indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
|
|
199
|
+
className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
|
|
200
|
+
children: icons.expand
|
|
93
201
|
}),
|
|
94
202
|
/* @__PURE__ */ jsx("span", {
|
|
95
203
|
className: "min-w-0 truncate",
|
|
96
204
|
children: summary
|
|
97
205
|
}),
|
|
98
|
-
indicator === "caret" && /* @__PURE__ */ jsx(
|
|
206
|
+
indicator === "caret" && /* @__PURE__ */ jsx(IconSlot, {
|
|
207
|
+
className: "transition-transform duration-150 group-aria-expanded/trigger:rotate-90",
|
|
208
|
+
children: icons.caret
|
|
209
|
+
}),
|
|
99
210
|
trailing && /* @__PURE__ */ jsx("span", {
|
|
100
211
|
className: "ml-auto shrink-0 pl-2",
|
|
101
212
|
children: trailing
|
|
@@ -122,7 +233,7 @@ function MenuSearch({ placeholder, children, value, onChange }) {
|
|
|
122
233
|
className: "border-b border-chat-popover-border",
|
|
123
234
|
children: /* @__PURE__ */ jsx(Input, {
|
|
124
235
|
placeholder,
|
|
125
|
-
className: "w-full bg-transparent px-3.5 py-2.5 text-
|
|
236
|
+
className: "w-full bg-transparent px-3.5 py-2.5 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none placeholder:text-chat-subtle"
|
|
126
237
|
})
|
|
127
238
|
}), children]
|
|
128
239
|
});
|
|
@@ -141,44 +252,45 @@ function Menu({ className, ...props }) {
|
|
|
141
252
|
className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
|
|
142
253
|
});
|
|
143
254
|
}
|
|
144
|
-
function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
|
|
255
|
+
function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
|
|
256
|
+
const icons = useIcons();
|
|
145
257
|
const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
|
|
146
258
|
return /* @__PURE__ */ jsx(MenuItem$1, {
|
|
147
259
|
...props,
|
|
148
260
|
...textValue !== void 0 && { textValue },
|
|
149
|
-
className: clsx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-
|
|
261
|
+
className: clsx("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),
|
|
150
262
|
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
151
263
|
icon && /* @__PURE__ */ jsx("span", {
|
|
152
264
|
className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
|
|
153
265
|
children: icon
|
|
154
266
|
}),
|
|
155
267
|
/* @__PURE__ */ jsxs("span", {
|
|
156
|
-
className: "flex min-w-0 flex-1 flex-col",
|
|
268
|
+
className: clsx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
|
|
157
269
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
158
270
|
slot: "label",
|
|
159
|
-
className: "truncate",
|
|
271
|
+
className: clsx("truncate", descriptionPlacement === "beside" && "shrink-0"),
|
|
160
272
|
children
|
|
161
273
|
}), description && /* @__PURE__ */ jsx(Text, {
|
|
162
274
|
slot: "description",
|
|
163
|
-
className: "text-
|
|
275
|
+
className: clsx("text-chat-muted", descriptionPlacement === "beside" ? "truncate text-(length:--chat-ui-font-size) leading-5" : "text-(length:--chat-ui-font-size-sm) leading-4"),
|
|
164
276
|
children: description
|
|
165
277
|
})]
|
|
166
278
|
}),
|
|
167
279
|
detail && /* @__PURE__ */ jsx("span", {
|
|
168
|
-
className: "shrink-0 text-
|
|
280
|
+
className: "shrink-0 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
169
281
|
children: detail
|
|
170
282
|
}),
|
|
171
283
|
shortcut && /* @__PURE__ */ jsx(Keyboard, {
|
|
172
|
-
className: "shrink-0 font-chat-sans text-
|
|
284
|
+
className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
173
285
|
children: shortcut
|
|
174
286
|
}),
|
|
175
|
-
selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(
|
|
176
|
-
"
|
|
177
|
-
|
|
287
|
+
selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
|
|
288
|
+
className: "text-chat-check [&_svg]:size-4",
|
|
289
|
+
children: icons.check
|
|
178
290
|
}),
|
|
179
|
-
hasSubmenu && /* @__PURE__ */ jsx(
|
|
180
|
-
"
|
|
181
|
-
|
|
291
|
+
hasSubmenu && /* @__PURE__ */ jsx(IconSlot, {
|
|
292
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
293
|
+
children: icons.submenu
|
|
182
294
|
})
|
|
183
295
|
] })
|
|
184
296
|
});
|
|
@@ -188,7 +300,7 @@ function MenuSection({ title, children, ...selection }) {
|
|
|
188
300
|
...selection,
|
|
189
301
|
className: "[&:not(:first-child)]:mt-1",
|
|
190
302
|
children: [title && /* @__PURE__ */ jsx(Header, {
|
|
191
|
-
className: "px-2 pt-1.5 pb-1 text-
|
|
303
|
+
className: "px-2 pt-1.5 pb-1 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
192
304
|
children: title
|
|
193
305
|
}), children]
|
|
194
306
|
});
|
|
@@ -199,7 +311,7 @@ function MenuSeparator() {
|
|
|
199
311
|
/** Explanatory text inside a menu, above or below the items. Not focusable. */
|
|
200
312
|
function MenuNote({ children, className }) {
|
|
201
313
|
return /* @__PURE__ */ jsx("div", {
|
|
202
|
-
className: clsx("px-2 py-1.5 text-
|
|
314
|
+
className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
|
|
203
315
|
children
|
|
204
316
|
});
|
|
205
317
|
}
|
|
@@ -260,7 +372,7 @@ function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
|
|
|
260
372
|
isOpen,
|
|
261
373
|
onOpenChange,
|
|
262
374
|
isDismissable: true,
|
|
263
|
-
className: "fixed inset-0 z-50 flex items-center justify-center bg-
|
|
375
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
264
376
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
265
377
|
className: clsx("flex max-h-[85dvh] w-full max-w-2xl flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
|
|
266
378
|
children: /* @__PURE__ */ jsxs(Dialog, {
|
|
@@ -281,7 +393,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
281
393
|
isOpen,
|
|
282
394
|
onOpenChange,
|
|
283
395
|
isDismissable: true,
|
|
284
|
-
className: "fixed inset-0 z-40 bg-
|
|
396
|
+
className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
285
397
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
286
398
|
className: clsx("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),
|
|
287
399
|
children: /* @__PURE__ */ jsx(Dialog, {
|
|
@@ -303,7 +415,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
|
|
|
303
415
|
className: clsx("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),
|
|
304
416
|
children: [
|
|
305
417
|
(title || actions) && /* @__PURE__ */ jsxs("header", {
|
|
306
|
-
className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-
|
|
418
|
+
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",
|
|
307
419
|
children: [/* @__PURE__ */ jsx("div", {
|
|
308
420
|
className: "min-w-0 flex-1 truncate font-medium",
|
|
309
421
|
children: title
|
|
@@ -460,10 +572,10 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
|
|
|
460
572
|
style: themeBackground ? result.vars : void 0,
|
|
461
573
|
className: clsx(variant === "boxed" && "my-4 rounded-chat-md border border-chat-code-border bg-chat-code-bg px-4 py-3.5", className),
|
|
462
574
|
children: [label && /* @__PURE__ */ jsx("div", {
|
|
463
|
-
className: "mb-3 font-chat-sans text-
|
|
575
|
+
className: "mb-3 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
464
576
|
children: label
|
|
465
577
|
}), /* @__PURE__ */ jsx("pre", {
|
|
466
|
-
className: clsx("overflow-auto font-chat-mono text-
|
|
578
|
+
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]"),
|
|
467
579
|
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
|
|
468
580
|
className: "chat-code-token",
|
|
469
581
|
style: token.htmlStyle,
|
|
@@ -472,13 +584,130 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
|
|
|
472
584
|
})]
|
|
473
585
|
});
|
|
474
586
|
}
|
|
587
|
+
// src/primitives/labels.tsx
|
|
588
|
+
const plural = (count, one, many) => count === 1 ? one : many(count);
|
|
589
|
+
/** The English strings. */
|
|
590
|
+
const defaultLabels = {
|
|
591
|
+
thinking: "Thinking…",
|
|
592
|
+
thought: "Thought",
|
|
593
|
+
thoughtFor: (duration) => `Thought for ${duration}`,
|
|
594
|
+
waitingForYou: " · waiting for you",
|
|
595
|
+
usedTool: (name) => `used ${name}`,
|
|
596
|
+
joinSummaries: (phrases) => {
|
|
597
|
+
const text = phrases.join(", ");
|
|
598
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
599
|
+
},
|
|
600
|
+
duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
|
|
601
|
+
ranCommandLabel: "Ran a command",
|
|
602
|
+
ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
|
|
603
|
+
searchedWebLabel: "Searched the web",
|
|
604
|
+
readPageLabel: "Read page",
|
|
605
|
+
readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
|
|
606
|
+
searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
|
|
607
|
+
recalledMemoryLabel: "Recalled memory",
|
|
608
|
+
recalledMemory: () => "recalled memory",
|
|
609
|
+
output: "Output",
|
|
610
|
+
noOutput: "(no output)",
|
|
611
|
+
noResults: "No results",
|
|
612
|
+
reasoningHidden: "The model's reasoning is not shown.",
|
|
613
|
+
allowTool: (name) => `Allow ${name}?`,
|
|
614
|
+
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
615
|
+
"Allow ",
|
|
616
|
+
label,
|
|
617
|
+
"?"
|
|
618
|
+
] }),
|
|
619
|
+
allow: "Allow",
|
|
620
|
+
deny: "Deny",
|
|
621
|
+
sidebar: "Sidebar",
|
|
622
|
+
closeSidebar: "Close sidebar",
|
|
623
|
+
openSidebar: "Open sidebar",
|
|
624
|
+
expandSidebar: "Expand sidebar",
|
|
625
|
+
collapseSidebar: "Collapse sidebar",
|
|
626
|
+
context: "Context",
|
|
627
|
+
contextUsed: "Context used",
|
|
628
|
+
percentUsed: (percent) => `${percent}% used`,
|
|
629
|
+
freeSpace: "Free space",
|
|
630
|
+
tokenCount: (count) => `${count} tokens`,
|
|
631
|
+
replyPlaceholder: "Reply…",
|
|
632
|
+
replyDirectly: "Or reply directly…",
|
|
633
|
+
message: "Message",
|
|
634
|
+
stop: "Stop",
|
|
635
|
+
send: "Send",
|
|
636
|
+
addFiles: "Add files and more",
|
|
637
|
+
attachments: "Attachments",
|
|
638
|
+
editPastedText: "Edit pasted text",
|
|
639
|
+
viewPastedText: "View pasted text",
|
|
640
|
+
removeAttachment: (name) => `Remove ${name}`,
|
|
641
|
+
pastedText: "Pasted text",
|
|
642
|
+
pasted: "Pasted",
|
|
643
|
+
failed: "Failed",
|
|
644
|
+
file: "File",
|
|
645
|
+
uploadFailed: "Upload failed",
|
|
646
|
+
cancel: "Cancel",
|
|
647
|
+
close: "Close",
|
|
648
|
+
save: "Save",
|
|
649
|
+
chats: "Chats",
|
|
650
|
+
running: "Running",
|
|
651
|
+
unread: "Unread",
|
|
652
|
+
moreOptionsFor: (title) => `More options for ${title}`,
|
|
653
|
+
new: "New",
|
|
654
|
+
viewAll: "View all",
|
|
655
|
+
chatActions: "Chat actions",
|
|
656
|
+
pin: "Pin",
|
|
657
|
+
unpin: "Unpin",
|
|
658
|
+
rename: "Rename",
|
|
659
|
+
addToProject: "Add to project",
|
|
660
|
+
searchProjects: "Search or create a project",
|
|
661
|
+
projects: "Projects",
|
|
662
|
+
newProject: "New project",
|
|
663
|
+
shareChat: "Share chat",
|
|
664
|
+
delete: "Delete",
|
|
665
|
+
justNow: "just now",
|
|
666
|
+
copy: "Copy",
|
|
667
|
+
copied: "Copied",
|
|
668
|
+
retry: "Retry",
|
|
669
|
+
edit: "Edit",
|
|
670
|
+
readAloud: "Read aloud",
|
|
671
|
+
goodResponse: "Good response",
|
|
672
|
+
badResponse: "Bad response",
|
|
673
|
+
somethingWentWrong: "Something went wrong.",
|
|
674
|
+
conversation: "Conversation",
|
|
675
|
+
scrollToBottom: "Scroll to bottom",
|
|
676
|
+
showMore: "Show more",
|
|
677
|
+
showLess: "Show less",
|
|
678
|
+
model: "Model",
|
|
679
|
+
modelButton: (label) => `Model: ${label}`,
|
|
680
|
+
effort: "Effort",
|
|
681
|
+
effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
|
|
682
|
+
recommended: "Recommended",
|
|
683
|
+
moreModels: "More models",
|
|
684
|
+
widget: "Widget",
|
|
685
|
+
drawingWidget: "Drawing"
|
|
686
|
+
};
|
|
687
|
+
const LabelsContext = createContext(defaultLabels);
|
|
688
|
+
/** Merges `labels` over the labels of the enclosing provider (or the defaults). */
|
|
689
|
+
function LabelsProvider({ labels, children }) {
|
|
690
|
+
const parent = useLabels();
|
|
691
|
+
const merged = useMemo(() => labels ? {
|
|
692
|
+
...parent,
|
|
693
|
+
...labels
|
|
694
|
+
} : parent, [parent, labels]);
|
|
695
|
+
return /* @__PURE__ */ jsx(LabelsContext.Provider, {
|
|
696
|
+
value: merged,
|
|
697
|
+
children
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
|
|
701
|
+
function useLabels() {
|
|
702
|
+
return useContext(LabelsContext);
|
|
703
|
+
}
|
|
475
704
|
// src/primitives/chat-theme.tsx
|
|
476
705
|
/**
|
|
477
706
|
* Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
|
|
478
707
|
* element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
|
|
479
708
|
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
|
|
480
709
|
*/
|
|
481
|
-
function ChatTheme({ theme, codeThemes, motion, children, className }) {
|
|
710
|
+
function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
|
|
482
711
|
const portal = useRef(null);
|
|
483
712
|
const getContainer = useCallback(() => portal.current, []);
|
|
484
713
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -488,9 +717,15 @@ function ChatTheme({ theme, codeThemes, motion, children, className }) {
|
|
|
488
717
|
children: [/* @__PURE__ */ jsx(AppearanceProvider, {
|
|
489
718
|
codeThemes,
|
|
490
719
|
motion,
|
|
491
|
-
children: /* @__PURE__ */ jsx(
|
|
492
|
-
|
|
493
|
-
children
|
|
720
|
+
children: /* @__PURE__ */ jsx(LabelsProvider, {
|
|
721
|
+
labels,
|
|
722
|
+
children: /* @__PURE__ */ jsx(IconsProvider, {
|
|
723
|
+
icons,
|
|
724
|
+
children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
|
|
725
|
+
getContainer,
|
|
726
|
+
children
|
|
727
|
+
})
|
|
728
|
+
})
|
|
494
729
|
})
|
|
495
730
|
}), /* @__PURE__ */ jsx("div", { ref: portal })]
|
|
496
731
|
});
|
|
@@ -1012,6 +1247,700 @@ const SidebarContext = createContext({
|
|
|
1012
1247
|
dismiss: () => {}
|
|
1013
1248
|
});
|
|
1014
1249
|
const useSidebar = () => useContext(SidebarContext);
|
|
1250
|
+
// src/components/widget-frame.tsx
|
|
1251
|
+
const WidgetHostContext = createContext({});
|
|
1252
|
+
/** Sets what widgets below may do. ChatView and ChatTranscript set it from their `widgets` prop. */
|
|
1253
|
+
function WidgetHostProvider({ host, children }) {
|
|
1254
|
+
return /* @__PURE__ */ jsx(WidgetHostContext.Provider, {
|
|
1255
|
+
value: host,
|
|
1256
|
+
children
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
function useWidgetHost() {
|
|
1260
|
+
return useContext(WidgetHostContext);
|
|
1261
|
+
}
|
|
1262
|
+
/** The tallest a widget may grow, in px. */
|
|
1263
|
+
const WIDGET_MAX_HEIGHT = 2e3;
|
|
1264
|
+
const PROTOCOL_VERSION = "2026-01-26";
|
|
1265
|
+
const THEME_VARS = [
|
|
1266
|
+
"bg",
|
|
1267
|
+
"fg",
|
|
1268
|
+
"muted",
|
|
1269
|
+
"subtle",
|
|
1270
|
+
"surface",
|
|
1271
|
+
"raised",
|
|
1272
|
+
"hover",
|
|
1273
|
+
"selected",
|
|
1274
|
+
"border",
|
|
1275
|
+
"border-strong",
|
|
1276
|
+
"divider",
|
|
1277
|
+
"accent",
|
|
1278
|
+
"accent-fg",
|
|
1279
|
+
"check",
|
|
1280
|
+
"warning",
|
|
1281
|
+
"danger",
|
|
1282
|
+
"code-bg",
|
|
1283
|
+
"font-sans",
|
|
1284
|
+
"font-serif",
|
|
1285
|
+
"font-mono",
|
|
1286
|
+
"radius-sm",
|
|
1287
|
+
"radius-md",
|
|
1288
|
+
"radius-lg",
|
|
1289
|
+
"ui-font-size",
|
|
1290
|
+
"ui-font-size-sm",
|
|
1291
|
+
"message-font-size"
|
|
1292
|
+
];
|
|
1293
|
+
const ORIGIN = /^(https?|wss?):\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*(:\d{1,5})?$/;
|
|
1294
|
+
function origins(list) {
|
|
1295
|
+
return (list ?? []).map((d) => d.trim().toLowerCase().replace(/\/$/, "")).filter((d) => ORIGIN.test(d));
|
|
1296
|
+
}
|
|
1297
|
+
/** Whether `origin` is covered by `allowed`, which may be a wildcard like "https://*.example.com". */
|
|
1298
|
+
function covers(allowed, origin) {
|
|
1299
|
+
if (allowed === origin) return true;
|
|
1300
|
+
const wild = /^([a-z]+:\/\/)\*\.(.+)$/.exec(allowed);
|
|
1301
|
+
return !!wild && origin.startsWith(wild[1]) && origin.slice(wild[1].length).endsWith("." + wild[2]) && !origin.includes("*");
|
|
1302
|
+
}
|
|
1303
|
+
/** The part of a widget's request that the host allows. */
|
|
1304
|
+
function intersectAllow(request, allow = {}) {
|
|
1305
|
+
const keep = (asked, max) => origins(asked).filter((o) => origins(max).some((m) => covers(m, o)));
|
|
1306
|
+
return {
|
|
1307
|
+
resourceDomains: keep(request.resourceDomains, allow.resourceDomains),
|
|
1308
|
+
connectDomains: keep(request.connectDomains, allow.connectDomains)
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
/** The Content-Security-Policy of a widget's document. */
|
|
1312
|
+
function widgetCsp(allow = {}) {
|
|
1313
|
+
const res = origins(allow.resourceDomains).join(" ");
|
|
1314
|
+
const conn = origins(allow.connectDomains).join(" ");
|
|
1315
|
+
const with_ = (base) => res ? `${base} ${res}` : base;
|
|
1316
|
+
return [
|
|
1317
|
+
"default-src 'none'",
|
|
1318
|
+
with_("script-src 'unsafe-inline'"),
|
|
1319
|
+
with_("style-src 'unsafe-inline'"),
|
|
1320
|
+
with_("img-src data: blob:"),
|
|
1321
|
+
with_("font-src data:"),
|
|
1322
|
+
with_("media-src data: blob:"),
|
|
1323
|
+
`connect-src ${conn || "'none'"}`,
|
|
1324
|
+
"frame-src 'none'",
|
|
1325
|
+
"base-uri 'none'",
|
|
1326
|
+
"form-action 'none'"
|
|
1327
|
+
].join("; ");
|
|
1328
|
+
}
|
|
1329
|
+
/** Runs inside a fragment's frame: renders the markup, reports its height, offers sendPrompt and openLink. */
|
|
1330
|
+
const BOOT = `(() => {
|
|
1331
|
+
const root = document.getElementById("widget");
|
|
1332
|
+
let id = 0;
|
|
1333
|
+
const post = (method, params, request) => parent.postMessage(request ? { jsonrpc: "2.0", id: ++id, method, params } : { jsonrpc: "2.0", method, params }, "*");
|
|
1334
|
+
window.sendPrompt = (text) => post("ui/message", { role: "user", content: [{ type: "text", text: String(text) }] }, true);
|
|
1335
|
+
window.openLink = (url) => post("ui/open-link", { url: String(url) }, true);
|
|
1336
|
+
let done = false;
|
|
1337
|
+
const runScripts = async () => {
|
|
1338
|
+
for (const old of [...root.querySelectorAll("script")]) {
|
|
1339
|
+
const s = document.createElement("script");
|
|
1340
|
+
for (const a of old.attributes) s.setAttribute(a.name, a.value);
|
|
1341
|
+
s.textContent = old.textContent;
|
|
1342
|
+
const loaded = s.src ? new Promise((ok) => { s.onload = s.onerror = ok; }) : null;
|
|
1343
|
+
old.replaceWith(s);
|
|
1344
|
+
if (loaded) await loaded;
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
const theme = (ctx) => {
|
|
1348
|
+
const vars = (ctx && ctx.styles && ctx.styles.variables) || {};
|
|
1349
|
+
for (const k in vars) if (k.startsWith("--chat-")) document.documentElement.style.setProperty(k, vars[k]);
|
|
1350
|
+
if (ctx && ctx.theme) document.documentElement.style.colorScheme = ctx.theme;
|
|
1351
|
+
};
|
|
1352
|
+
addEventListener("message", (e) => {
|
|
1353
|
+
if (e.source !== parent) return;
|
|
1354
|
+
const m = e.data || {};
|
|
1355
|
+
if (m.method === "ui/notifications/host-context-changed") return theme(m.params);
|
|
1356
|
+
const code = m.params && m.params.arguments && m.params.arguments.widget_code;
|
|
1357
|
+
if (typeof code !== "string" || done) return;
|
|
1358
|
+
if (m.method === "ui/notifications/tool-input-partial") root.innerHTML = code;
|
|
1359
|
+
if (m.method === "ui/notifications/tool-input") { done = true; root.innerHTML = code; runScripts(); }
|
|
1360
|
+
});
|
|
1361
|
+
let last = -1;
|
|
1362
|
+
new ResizeObserver(() => {
|
|
1363
|
+
const h = Math.ceil(root.getBoundingClientRect().height);
|
|
1364
|
+
if (h !== last) { last = h; post("ui/notifications/size-changed", { height: h }); }
|
|
1365
|
+
}).observe(root);
|
|
1366
|
+
const seen = new Set();
|
|
1367
|
+
const report = (text) => { if (!seen.has(text) && seen.size < 10) { seen.add(text); post("notifications/message", { level: "error", logger: "widget", data: text }); } };
|
|
1368
|
+
addEventListener("error", (e) => report(String(e.message) + (e.lineno ? " (line " + e.lineno + ")" : "")));
|
|
1369
|
+
addEventListener("unhandledrejection", (e) => report("Unhandled promise rejection: " + String(e.reason && e.reason.message || e.reason)));
|
|
1370
|
+
post("ui/notifications/initialized", {});
|
|
1371
|
+
})();`;
|
|
1372
|
+
function fragmentDoc(csp, theme) {
|
|
1373
|
+
return `<!doctype html><html><head><meta charset="utf-8">
|
|
1374
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
1375
|
+
<style>
|
|
1376
|
+
:root { ${Object.entries(theme.vars).map(([k, v]) => `${k}: ${v};`).join(" ")} color-scheme: ${theme.scheme}; }
|
|
1377
|
+
html, body { margin: 0; background: transparent; color: var(--chat-fg); font-family: var(--chat-font-sans);
|
|
1378
|
+
font-size: var(--chat-ui-font-size); line-height: 1.5; }
|
|
1379
|
+
#widget { display: flow-root; }
|
|
1380
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
1381
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1382
|
+
</style></head><body><div id="widget"></div><script>${BOOT}<\/script></body></html>`;
|
|
1383
|
+
}
|
|
1384
|
+
/** An app's own document with the host's CSP put first in its head (a second policy can only narrow it). */
|
|
1385
|
+
function appDoc(csp, html) {
|
|
1386
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
|
|
1387
|
+
const head = /<head(\s[^>]*)?>/i.exec(html);
|
|
1388
|
+
if (head) return html.slice(0, head.index + head[0].length) + meta + html.slice(head.index + head[0].length);
|
|
1389
|
+
const root = /<html(\s[^>]*)?>/i.exec(html);
|
|
1390
|
+
if (root) return html.slice(0, root.index + root[0].length) + `<head>${meta}</head>` + html.slice(root.index + root[0].length);
|
|
1391
|
+
return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;
|
|
1392
|
+
}
|
|
1393
|
+
function readTheme(el) {
|
|
1394
|
+
const style = getComputedStyle(el);
|
|
1395
|
+
return {
|
|
1396
|
+
vars: Object.fromEntries(THEME_VARS.map((v) => [`--chat-${v}`, style.getPropertyValue(`--chat-${v}`).trim()])),
|
|
1397
|
+
scheme: /light/.test(style.colorScheme) && !/dark/.test(style.colorScheme) ? "light" : "dark"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
/** Calls `onChange` when anything above `el` changes attributes (a theme switch) or the OS scheme flips. */
|
|
1401
|
+
function watchTheme(el, onChange) {
|
|
1402
|
+
const observer = new MutationObserver(onChange);
|
|
1403
|
+
for (let node = el; node; node = node.parentElement) observer.observe(node, {
|
|
1404
|
+
attributes: true,
|
|
1405
|
+
attributeFilter: [
|
|
1406
|
+
"class",
|
|
1407
|
+
"style",
|
|
1408
|
+
"data-chat-theme"
|
|
1409
|
+
]
|
|
1410
|
+
});
|
|
1411
|
+
const media = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : void 0;
|
|
1412
|
+
media?.addEventListener?.("change", onChange);
|
|
1413
|
+
return () => {
|
|
1414
|
+
observer.disconnect();
|
|
1415
|
+
media?.removeEventListener?.("change", onChange);
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function WidgetFrame({ html, document: isDocument = false, streaming = false, title, loadingMessages, allow, csp: request, onPrompt, onOpenLink, onCallTool, onError, toolInput, toolResult, cancelled, className }) {
|
|
1419
|
+
const labels = useLabels();
|
|
1420
|
+
const frame = useRef(null);
|
|
1421
|
+
const host = useRef(null);
|
|
1422
|
+
const [ready, setReady] = useState(false);
|
|
1423
|
+
const [height, setHeight] = useState(0);
|
|
1424
|
+
const [initial, setInitial] = useState();
|
|
1425
|
+
const theme = useRef(void 0);
|
|
1426
|
+
const [tick, setTick] = useState(0);
|
|
1427
|
+
const effective = request ? intersectAllow(request, allow) : isDocument ? {} : allow ?? {};
|
|
1428
|
+
const policy = widgetCsp(effective);
|
|
1429
|
+
const doc = useMemo(() => initial && (isDocument ? appDoc(policy, html) : fragmentDoc(policy, initial)), [
|
|
1430
|
+
initial,
|
|
1431
|
+
policy,
|
|
1432
|
+
isDocument,
|
|
1433
|
+
isDocument && html
|
|
1434
|
+
]);
|
|
1435
|
+
const latest = useRef({
|
|
1436
|
+
onPrompt,
|
|
1437
|
+
onOpenLink,
|
|
1438
|
+
onCallTool,
|
|
1439
|
+
onError,
|
|
1440
|
+
policy,
|
|
1441
|
+
effective
|
|
1442
|
+
});
|
|
1443
|
+
latest.current = {
|
|
1444
|
+
onPrompt,
|
|
1445
|
+
onOpenLink,
|
|
1446
|
+
onCallTool,
|
|
1447
|
+
onError,
|
|
1448
|
+
policy,
|
|
1449
|
+
effective
|
|
1450
|
+
};
|
|
1451
|
+
const send = (message) => frame.current?.contentWindow?.postMessage({
|
|
1452
|
+
jsonrpc: "2.0",
|
|
1453
|
+
...message
|
|
1454
|
+
}, "*");
|
|
1455
|
+
const context = () => ({
|
|
1456
|
+
theme: theme.current?.scheme,
|
|
1457
|
+
styles: { variables: theme.current?.vars },
|
|
1458
|
+
displayMode: "inline",
|
|
1459
|
+
availableDisplayModes: ["inline"],
|
|
1460
|
+
containerDimensions: {
|
|
1461
|
+
maxHeight: WIDGET_MAX_HEIGHT,
|
|
1462
|
+
...host.current && { width: host.current.clientWidth }
|
|
1463
|
+
},
|
|
1464
|
+
platform: "web",
|
|
1465
|
+
...labels.locale && { locale: labels.locale }
|
|
1466
|
+
});
|
|
1467
|
+
useEffect(() => {
|
|
1468
|
+
const el = host.current;
|
|
1469
|
+
theme.current = readTheme(el);
|
|
1470
|
+
setInitial(theme.current);
|
|
1471
|
+
return watchTheme(el, () => {
|
|
1472
|
+
const next = readTheme(el);
|
|
1473
|
+
if (JSON.stringify(next) === JSON.stringify(theme.current)) return;
|
|
1474
|
+
theme.current = next;
|
|
1475
|
+
send({
|
|
1476
|
+
method: "ui/notifications/host-context-changed",
|
|
1477
|
+
params: {
|
|
1478
|
+
theme: next.scheme,
|
|
1479
|
+
styles: { variables: next.vars }
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
}, []);
|
|
1484
|
+
useEffect(() => setReady(false), [doc]);
|
|
1485
|
+
useEffect(() => {
|
|
1486
|
+
const reply = (id, result) => id !== void 0 && send({
|
|
1487
|
+
id,
|
|
1488
|
+
result
|
|
1489
|
+
});
|
|
1490
|
+
const fail = (id, code, message) => id !== void 0 && send({
|
|
1491
|
+
id,
|
|
1492
|
+
error: {
|
|
1493
|
+
code,
|
|
1494
|
+
message
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
const onMessage = async (e) => {
|
|
1498
|
+
if (!frame.current || e.source !== frame.current.contentWindow) return;
|
|
1499
|
+
const m = e.data ?? {};
|
|
1500
|
+
if (typeof m !== "object" || typeof m.method !== "string") return;
|
|
1501
|
+
const { onPrompt, onOpenLink, onCallTool, effective } = latest.current;
|
|
1502
|
+
switch (m.method) {
|
|
1503
|
+
case "ui/initialize": return reply(m.id, {
|
|
1504
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1505
|
+
hostInfo: {
|
|
1506
|
+
name: "agentic-ui-kit",
|
|
1507
|
+
version: "1"
|
|
1508
|
+
},
|
|
1509
|
+
hostCapabilities: {
|
|
1510
|
+
...onOpenLink && { openLinks: {} },
|
|
1511
|
+
...onCallTool && { serverTools: {} },
|
|
1512
|
+
logging: {},
|
|
1513
|
+
sandbox: { csp: effective }
|
|
1514
|
+
},
|
|
1515
|
+
hostContext: context()
|
|
1516
|
+
});
|
|
1517
|
+
case "ui/notifications/initialized": return setReady(true);
|
|
1518
|
+
case "ui/notifications/size-changed": return setHeight(Math.min(Math.max(0, Number(m.params?.height) || 0), WIDGET_MAX_HEIGHT));
|
|
1519
|
+
case "ui/message": {
|
|
1520
|
+
const text = (Array.isArray(m.params?.content) ? m.params.content : []).find((c) => c?.type === "text")?.text;
|
|
1521
|
+
if (!onPrompt) return fail(m.id, -32e3, "Sending messages is off");
|
|
1522
|
+
if (typeof text !== "string" || !text.trim()) return fail(m.id, -32602, "Empty message");
|
|
1523
|
+
onPrompt(text.slice(0, 4e3));
|
|
1524
|
+
return reply(m.id, {});
|
|
1525
|
+
}
|
|
1526
|
+
case "ui/open-link": {
|
|
1527
|
+
const url = String(m.params?.url ?? "");
|
|
1528
|
+
if (!onOpenLink) return fail(m.id, -32e3, "Opening links is off");
|
|
1529
|
+
if (!/^https?:\/\//i.test(url)) return fail(m.id, -32602, "Only http(s) links");
|
|
1530
|
+
onOpenLink(url);
|
|
1531
|
+
return reply(m.id, {});
|
|
1532
|
+
}
|
|
1533
|
+
case "tools/call": {
|
|
1534
|
+
if (!onCallTool) return fail(m.id, -32e3, "Calling tools is off");
|
|
1535
|
+
const name = String(m.params?.name ?? "");
|
|
1536
|
+
const args = m.params?.arguments ?? {};
|
|
1537
|
+
try {
|
|
1538
|
+
return reply(m.id, await onCallTool(name, args));
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
return fail(m.id, -32e3, error instanceof Error ? error.message : String(error));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
case "ping": return reply(m.id, {});
|
|
1544
|
+
case "notifications/message": {
|
|
1545
|
+
const data = typeof m.params?.data === "string" ? m.params.data : JSON.stringify(m.params?.data);
|
|
1546
|
+
if (m.params?.level === "error" && data) latest.current.onError?.(data.slice(0, 1e3));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
default: return fail(m.id, -32601, "Method not found");
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
window.addEventListener("message", onMessage);
|
|
1553
|
+
return () => window.removeEventListener("message", onMessage);
|
|
1554
|
+
}, []);
|
|
1555
|
+
useEffect(() => {
|
|
1556
|
+
if (!ready || isDocument) return;
|
|
1557
|
+
send({
|
|
1558
|
+
method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
|
|
1559
|
+
params: { arguments: { widget_code: html } }
|
|
1560
|
+
});
|
|
1561
|
+
}, [
|
|
1562
|
+
ready,
|
|
1563
|
+
isDocument,
|
|
1564
|
+
html,
|
|
1565
|
+
streaming
|
|
1566
|
+
]);
|
|
1567
|
+
const inputKey = isDocument && toolInput !== void 0 ? JSON.stringify(toolInput) : void 0;
|
|
1568
|
+
useEffect(() => {
|
|
1569
|
+
if (!ready || inputKey === void 0) return;
|
|
1570
|
+
send({
|
|
1571
|
+
method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
|
|
1572
|
+
params: { arguments: JSON.parse(inputKey) }
|
|
1573
|
+
});
|
|
1574
|
+
}, [
|
|
1575
|
+
ready,
|
|
1576
|
+
inputKey,
|
|
1577
|
+
streaming
|
|
1578
|
+
]);
|
|
1579
|
+
useEffect(() => {
|
|
1580
|
+
if (ready && isDocument && toolResult !== void 0) send({
|
|
1581
|
+
method: "ui/notifications/tool-result",
|
|
1582
|
+
params: toolResult
|
|
1583
|
+
});
|
|
1584
|
+
}, [
|
|
1585
|
+
ready,
|
|
1586
|
+
isDocument,
|
|
1587
|
+
toolResult
|
|
1588
|
+
]);
|
|
1589
|
+
useEffect(() => {
|
|
1590
|
+
if (ready && isDocument && cancelled !== void 0) send({
|
|
1591
|
+
method: "ui/notifications/tool-cancelled",
|
|
1592
|
+
params: { reason: cancelled }
|
|
1593
|
+
});
|
|
1594
|
+
}, [
|
|
1595
|
+
ready,
|
|
1596
|
+
isDocument,
|
|
1597
|
+
cancelled
|
|
1598
|
+
]);
|
|
1599
|
+
const lines = loadingMessages?.length ? loadingMessages : [labels.drawingWidget];
|
|
1600
|
+
useEffect(() => {
|
|
1601
|
+
if (!streaming || lines.length < 2) return;
|
|
1602
|
+
const id = setInterval(() => setTick((t) => t + 1), 1800);
|
|
1603
|
+
return () => clearInterval(id);
|
|
1604
|
+
}, [streaming, lines.length]);
|
|
1605
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
1606
|
+
ref: host,
|
|
1607
|
+
className: className ?? "my-2",
|
|
1608
|
+
"aria-label": title ?? labels.widget,
|
|
1609
|
+
"aria-busy": streaming || void 0,
|
|
1610
|
+
children: [streaming && /* @__PURE__ */ jsx("div", {
|
|
1611
|
+
className: "mb-2 animate-pulse font-chat-sans text-(length:--chat-ui-font-size) text-chat-muted motion-reduce:animate-none",
|
|
1612
|
+
children: lines[tick % lines.length]
|
|
1613
|
+
}), doc && /* @__PURE__ */ jsx("iframe", {
|
|
1614
|
+
ref: frame,
|
|
1615
|
+
title: title ?? labels.widget,
|
|
1616
|
+
sandbox: "allow-scripts",
|
|
1617
|
+
srcDoc: doc,
|
|
1618
|
+
style: {
|
|
1619
|
+
height: height || (streaming ? 120 : 0),
|
|
1620
|
+
transition: "height 150ms ease-out"
|
|
1621
|
+
},
|
|
1622
|
+
className: "block w-full border-0 bg-transparent"
|
|
1623
|
+
})]
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
/** Pulls the host's abilities from context into frame props. */
|
|
1627
|
+
function useHostProps(partId) {
|
|
1628
|
+
const { allow, onPrompt, onOpenLink, onCallTool, onWidgetError } = useWidgetHost();
|
|
1629
|
+
return {
|
|
1630
|
+
...allow && { allow },
|
|
1631
|
+
...onPrompt && { onPrompt },
|
|
1632
|
+
...onOpenLink && { onOpenLink },
|
|
1633
|
+
...onCallTool && { onCallTool },
|
|
1634
|
+
...onWidgetError && { onError: (message) => onWidgetError({
|
|
1635
|
+
partId,
|
|
1636
|
+
message
|
|
1637
|
+
}) }
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/** The default look of a `widget` part: inline html, or an app UI the host reads from `resourceUri`. */
|
|
1641
|
+
function WidgetPartView({ part }) {
|
|
1642
|
+
const host = useWidgetHost();
|
|
1643
|
+
const props = useHostProps(part.id);
|
|
1644
|
+
const [resource, setResource] = useState();
|
|
1645
|
+
const uri = part.html === void 0 ? part.resourceUri : void 0;
|
|
1646
|
+
useEffect(() => {
|
|
1647
|
+
if (!uri || !host.readResource) return;
|
|
1648
|
+
let live = true;
|
|
1649
|
+
host.readResource(uri).then((r) => live && setResource(r), (e) => console.warn("widget:", e));
|
|
1650
|
+
return () => {
|
|
1651
|
+
live = false;
|
|
1652
|
+
};
|
|
1653
|
+
}, [uri, host.readResource]);
|
|
1654
|
+
if (part.html !== void 0) return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1655
|
+
html: part.html,
|
|
1656
|
+
streaming: part.status === "streaming",
|
|
1657
|
+
...part.title && { title: part.title },
|
|
1658
|
+
...props
|
|
1659
|
+
});
|
|
1660
|
+
if (!resource) return null;
|
|
1661
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1662
|
+
document: true,
|
|
1663
|
+
html: resource.html,
|
|
1664
|
+
...resource.csp && { csp: resource.csp },
|
|
1665
|
+
...part.title && { title: part.title },
|
|
1666
|
+
...props
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
1670
|
+
/** Reads the fields of the suggested `visualizeTool` (widget_code, title, loading_messages). */
|
|
1671
|
+
function readVisualizeInput(input) {
|
|
1672
|
+
const i = input ?? {};
|
|
1673
|
+
const loading = Array.isArray(i.loading_messages) ? i.loading_messages.filter((l) => typeof l === "string" && l !== "") : [];
|
|
1674
|
+
return {
|
|
1675
|
+
...str(i.widget_code) !== void 0 && { html: str(i.widget_code) },
|
|
1676
|
+
...str(i.title) && { title: str(i.title) },
|
|
1677
|
+
...loading.length && { loadingMessages: loading.slice(0, 4) }
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* A tool view that draws a call's (streaming) input in a WidgetFrame, for whatever tool your harness uses
|
|
1682
|
+
* to write HTML. Register it under your tool's name: `{ show_widget: widgetToolView() }`. `read` maps your
|
|
1683
|
+
* tool's input (possibly cut short while it streams) to the frame's fields; the default reads the suggested
|
|
1684
|
+
* `visualizeTool`'s fields.
|
|
1685
|
+
*/
|
|
1686
|
+
function widgetToolView(read = readVisualizeInput) {
|
|
1687
|
+
function WidgetToolBlock({ part }) {
|
|
1688
|
+
const props = useHostProps(part.id);
|
|
1689
|
+
const { html = "", title, loadingMessages } = read(part.input ?? part.partialInput);
|
|
1690
|
+
if (part.status === "error" && !html) return null;
|
|
1691
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1692
|
+
html,
|
|
1693
|
+
streaming: part.status === "streaming" || part.status === "input",
|
|
1694
|
+
...title && { title },
|
|
1695
|
+
...loadingMessages?.length && { loadingMessages },
|
|
1696
|
+
...props
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
return { Block: WidgetToolBlock };
|
|
1700
|
+
}
|
|
1701
|
+
/** The default `onOpenLink` of ChatView and ChatTranscript: a new tab with no opener. */
|
|
1702
|
+
function openInNewTab(url) {
|
|
1703
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1704
|
+
}
|
|
1705
|
+
// src/components/ui-tree.tsx
|
|
1706
|
+
const RegistryContext = createContext(void 0);
|
|
1707
|
+
/**
|
|
1708
|
+
* The components the model may use: in ```ui blocks of its replies (see `uiMarkupGuide` in
|
|
1709
|
+
* agentic-ui-kit/core) and in tool calls drawn with `uiTreeToolView`. `persist` keeps each widget's state.
|
|
1710
|
+
*/
|
|
1711
|
+
function UiComponentsProvider({ components, persist, markup = "ui", children }) {
|
|
1712
|
+
const value = useMemo(() => ({
|
|
1713
|
+
components: new Map(components.map((c) => [c.name, c])),
|
|
1714
|
+
persist,
|
|
1715
|
+
markup
|
|
1716
|
+
}), [
|
|
1717
|
+
components,
|
|
1718
|
+
persist,
|
|
1719
|
+
markup
|
|
1720
|
+
]);
|
|
1721
|
+
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
1722
|
+
value,
|
|
1723
|
+
children
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
const TreeContext = createContext({
|
|
1727
|
+
state: {},
|
|
1728
|
+
run: () => {},
|
|
1729
|
+
streaming: false
|
|
1730
|
+
});
|
|
1731
|
+
/** Inside a registered component: runs an action from its props, e.g. `run(props.onPress)`. Bad actions do nothing. */
|
|
1732
|
+
function useUiAction() {
|
|
1733
|
+
const { run } = useContext(TreeContext);
|
|
1734
|
+
return useCallback((action) => matchesSchema(uiActionSchema, action) && run(action), [run]);
|
|
1735
|
+
}
|
|
1736
|
+
/** Inside a registered component: the widget's state and a setter, for inputs that write to it. */
|
|
1737
|
+
function useUiState() {
|
|
1738
|
+
const { state, run } = useContext(TreeContext);
|
|
1739
|
+
return [state, useCallback((key, value) => run({
|
|
1740
|
+
action: "set",
|
|
1741
|
+
key,
|
|
1742
|
+
value
|
|
1743
|
+
}), [run])];
|
|
1744
|
+
}
|
|
1745
|
+
/** Replaces `{ "$state": key }` anywhere in the props with the state's value. */
|
|
1746
|
+
function resolve(value, state) {
|
|
1747
|
+
if (isStateRef(value)) return state[value.$state];
|
|
1748
|
+
if (Array.isArray(value)) return value.map((v) => resolve(v, state));
|
|
1749
|
+
if (typeof value === "object" && value !== null) {
|
|
1750
|
+
const out = {};
|
|
1751
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1752
|
+
const r = resolve(v, state);
|
|
1753
|
+
if (r !== void 0) out[k] = r;
|
|
1754
|
+
}
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
return value;
|
|
1758
|
+
}
|
|
1759
|
+
/** A component that throws renders nothing instead of taking the message down. */
|
|
1760
|
+
var Contain = class extends Component {
|
|
1761
|
+
state = { failed: false };
|
|
1762
|
+
static getDerivedStateFromError() {
|
|
1763
|
+
return { failed: true };
|
|
1764
|
+
}
|
|
1765
|
+
componentDidCatch(error) {
|
|
1766
|
+
console.warn("render_ui:", error);
|
|
1767
|
+
}
|
|
1768
|
+
render() {
|
|
1769
|
+
return this.state.failed ? null : this.props.children;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
function Node({ node, registry }) {
|
|
1773
|
+
const { state } = useContext(TreeContext);
|
|
1774
|
+
if (Array.isArray(node)) return node.map((child, i) => /* @__PURE__ */ jsx(Node, {
|
|
1775
|
+
node: child,
|
|
1776
|
+
registry
|
|
1777
|
+
}, i));
|
|
1778
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1779
|
+
const { component, props = {}, children } = node;
|
|
1780
|
+
const spec = typeof component === "string" ? registry.components.get(component) : void 0;
|
|
1781
|
+
if (!spec || typeof props !== "object") return null;
|
|
1782
|
+
const resolved = resolve(props, state);
|
|
1783
|
+
if (!matchesSchema(spec.props ?? { type: "object" }, resolved)) return null;
|
|
1784
|
+
const Render = spec.render;
|
|
1785
|
+
const kids = spec.children && Array.isArray(children) ? children.map((child, i) => /* @__PURE__ */ jsx(Node, {
|
|
1786
|
+
node: child,
|
|
1787
|
+
registry
|
|
1788
|
+
}, i)) : void 0;
|
|
1789
|
+
return /* @__PURE__ */ jsx(Contain, { children: /* @__PURE__ */ jsx(Render, {
|
|
1790
|
+
...resolved,
|
|
1791
|
+
children: kids
|
|
1792
|
+
}) });
|
|
1793
|
+
}
|
|
1794
|
+
/** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
|
|
1795
|
+
function UiTree({ id, root, state: initial, streaming = false }) {
|
|
1796
|
+
const registry = useContext(RegistryContext);
|
|
1797
|
+
const host = useWidgetHost();
|
|
1798
|
+
const [local, setLocal] = useState(() => registry?.persist?.load(id) ?? {});
|
|
1799
|
+
const changed = useRef(false);
|
|
1800
|
+
const latest = useRef(host);
|
|
1801
|
+
latest.current = host;
|
|
1802
|
+
useEffect(() => {
|
|
1803
|
+
if (changed.current) registry?.persist?.save(id, local);
|
|
1804
|
+
}, [
|
|
1805
|
+
id,
|
|
1806
|
+
local,
|
|
1807
|
+
registry
|
|
1808
|
+
]);
|
|
1809
|
+
const run = useCallback((action) => {
|
|
1810
|
+
switch (action.action) {
|
|
1811
|
+
case "set":
|
|
1812
|
+
changed.current = true;
|
|
1813
|
+
return setLocal((s) => ({
|
|
1814
|
+
...s,
|
|
1815
|
+
[action.key]: action.value
|
|
1816
|
+
}));
|
|
1817
|
+
case "send": return latest.current.onPrompt?.(action.text);
|
|
1818
|
+
case "open": return /^https?:\/\//i.test(action.url) && latest.current.onOpenLink?.(action.url);
|
|
1819
|
+
case "copy":
|
|
1820
|
+
navigator.clipboard?.writeText(action.text).catch(() => {});
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
}, []);
|
|
1824
|
+
const state = useMemo(() => ({
|
|
1825
|
+
...typeof initial === "object" && initial ? initial : {},
|
|
1826
|
+
...local
|
|
1827
|
+
}), [initial, local]);
|
|
1828
|
+
const context = useMemo(() => ({
|
|
1829
|
+
state,
|
|
1830
|
+
run,
|
|
1831
|
+
streaming
|
|
1832
|
+
}), [
|
|
1833
|
+
state,
|
|
1834
|
+
run,
|
|
1835
|
+
streaming
|
|
1836
|
+
]);
|
|
1837
|
+
if (!registry) return null;
|
|
1838
|
+
return /* @__PURE__ */ jsx(TreeContext.Provider, {
|
|
1839
|
+
value: context,
|
|
1840
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1841
|
+
className: "chat-ui-tree my-2",
|
|
1842
|
+
"aria-busy": streaming || void 0,
|
|
1843
|
+
children: /* @__PURE__ */ jsx(Node, {
|
|
1844
|
+
node: root,
|
|
1845
|
+
registry
|
|
1846
|
+
})
|
|
1847
|
+
})
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
/** The fence language drawn as UI here, or undefined outside a `UiComponentsProvider`. */
|
|
1851
|
+
function useUiMarkupLanguage() {
|
|
1852
|
+
const registry = useContext(RegistryContext);
|
|
1853
|
+
return registry && registry.markup !== false ? registry.markup : void 0;
|
|
1854
|
+
}
|
|
1855
|
+
/** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
|
|
1856
|
+
function UiMarkupBlock({ id, source, streaming = false }) {
|
|
1857
|
+
const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
|
|
1858
|
+
return /* @__PURE__ */ jsx(UiTree, {
|
|
1859
|
+
id,
|
|
1860
|
+
root: nodes,
|
|
1861
|
+
state,
|
|
1862
|
+
streaming
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
|
|
1867
|
+
* your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
|
|
1868
|
+
* `{ show_ui: uiMarkupToolView() }`. `read` maps your tool's input to the markup; the default reads `markup`.
|
|
1869
|
+
*/
|
|
1870
|
+
function uiMarkupToolView(read = (input) => {
|
|
1871
|
+
const markup = input?.markup;
|
|
1872
|
+
return typeof markup === "string" ? markup : void 0;
|
|
1873
|
+
}) {
|
|
1874
|
+
function UiMarkupToolBlock({ part }) {
|
|
1875
|
+
const markup = read(part.input ?? part.partialInput);
|
|
1876
|
+
if (!markup) return null;
|
|
1877
|
+
return /* @__PURE__ */ jsx(UiMarkupBlock, {
|
|
1878
|
+
id: part.id,
|
|
1879
|
+
source: markup,
|
|
1880
|
+
streaming: part.status === "streaming" || part.status === "input"
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
return { Block: UiMarkupToolBlock };
|
|
1884
|
+
}
|
|
1885
|
+
/** Reads `{ root, state }`. Models sometimes send `root` or `state` as a JSON string; those are parsed too. */
|
|
1886
|
+
function readTreeInput(input) {
|
|
1887
|
+
const i = input ?? {};
|
|
1888
|
+
const unwrap = (v) => typeof v === "string" ? salvageJson(v).value : v;
|
|
1889
|
+
const root = unwrap(i.root);
|
|
1890
|
+
const state = unwrap(i.state);
|
|
1891
|
+
return {
|
|
1892
|
+
...root !== void 0 && { root },
|
|
1893
|
+
...typeof state === "object" && state !== null && { state }
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* A tool view that draws a call's (streaming) input as a tree of your registered components, for whatever
|
|
1898
|
+
* tool your harness uses. Register it under your tool's name: `{ render_ui: uiTreeToolView() }`. `read` maps
|
|
1899
|
+
* your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
|
|
1900
|
+
* the suggested `renderUiTool`.
|
|
1901
|
+
*/
|
|
1902
|
+
function uiTreeToolView(read = readTreeInput) {
|
|
1903
|
+
function UiTreeToolBlock({ part }) {
|
|
1904
|
+
const { root, state } = read(part.input ?? part.partialInput);
|
|
1905
|
+
if (!root) return null;
|
|
1906
|
+
const ok = typeof state === "object" && state !== null;
|
|
1907
|
+
return /* @__PURE__ */ jsx(UiTree, {
|
|
1908
|
+
id: part.id,
|
|
1909
|
+
root,
|
|
1910
|
+
...ok && { state },
|
|
1911
|
+
streaming: part.status === "streaming" || part.status === "input"
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
return { Block: UiTreeToolBlock };
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* The escape hatch: raw HTML or SVG in a sandboxed WidgetFrame, as a node of the tree. Register it only if
|
|
1918
|
+
* the model may write HTML; it gets what the chat's `widgets` host allows.
|
|
1919
|
+
*/
|
|
1920
|
+
const htmlComponent = {
|
|
1921
|
+
name: "Html",
|
|
1922
|
+
description: "Raw HTML or <svg> in a sandboxed frame, only for what the other components cannot show. Follow the visual design guide.",
|
|
1923
|
+
props: {
|
|
1924
|
+
type: "object",
|
|
1925
|
+
properties: {
|
|
1926
|
+
html: { type: "string" },
|
|
1927
|
+
title: { type: "string" }
|
|
1928
|
+
},
|
|
1929
|
+
required: ["html"]
|
|
1930
|
+
},
|
|
1931
|
+
render: function Html({ html, title }) {
|
|
1932
|
+
const { streaming } = useContext(TreeContext);
|
|
1933
|
+
const { allow, onPrompt, onOpenLink } = useWidgetHost();
|
|
1934
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1935
|
+
html,
|
|
1936
|
+
streaming,
|
|
1937
|
+
...title && { title },
|
|
1938
|
+
...allow && { allow },
|
|
1939
|
+
...onPrompt && { onPrompt },
|
|
1940
|
+
...onOpenLink && { onOpenLink }
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1015
1944
|
// src/components/word-fade.ts
|
|
1016
1945
|
const OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
1017
1946
|
"code",
|
|
@@ -1078,45 +2007,96 @@ function textOf(node) {
|
|
|
1078
2007
|
if (node.type === "text") return node.value ?? "";
|
|
1079
2008
|
return (node.children ?? []).map(textOf).join("");
|
|
1080
2009
|
}
|
|
2010
|
+
const BlockContext = createContext(void 0);
|
|
2011
|
+
function Code({ node, className, children }) {
|
|
2012
|
+
const hast = node;
|
|
2013
|
+
const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
|
|
2014
|
+
const ui = useUiMarkupLanguage();
|
|
2015
|
+
const block = useContext(BlockContext);
|
|
2016
|
+
const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
|
|
2017
|
+
if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
|
|
2018
|
+
"data-chat-inline-code": true,
|
|
2019
|
+
children
|
|
2020
|
+
});
|
|
2021
|
+
const code = textOf(hast).replace(/\n$/, "");
|
|
2022
|
+
if (block && ui && language === ui) return /* @__PURE__ */ jsx(UiMarkupBlock, {
|
|
2023
|
+
id: `${block.id}:${hast?.position?.start.line ?? 0}`,
|
|
2024
|
+
source: code,
|
|
2025
|
+
streaming: block.streaming
|
|
2026
|
+
});
|
|
2027
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
2028
|
+
variant: "boxed",
|
|
2029
|
+
code,
|
|
2030
|
+
...language && { language }
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
1081
2033
|
const components = {
|
|
1082
2034
|
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
1083
|
-
code: ({ node, className, children }) => {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
"data-chat-inline-code": true,
|
|
1089
|
-
children
|
|
1090
|
-
});
|
|
1091
|
-
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
1092
|
-
variant: "boxed",
|
|
1093
|
-
code: textOf(hast).replace(/\n$/, ""),
|
|
1094
|
-
...language && { language }
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
2035
|
+
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
2036
|
+
node,
|
|
2037
|
+
className,
|
|
2038
|
+
children
|
|
2039
|
+
})
|
|
1097
2040
|
};
|
|
2041
|
+
const LinksContext = createContext(void 0);
|
|
2042
|
+
function MarkdownLinksProvider({ renderLink, children }) {
|
|
2043
|
+
return /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
2044
|
+
value: renderLink,
|
|
2045
|
+
children
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
1098
2048
|
/** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
|
|
1099
|
-
function Markdown({ children, streaming = false, className }) {
|
|
2049
|
+
function Markdown({ children, streaming = false, className, partId }) {
|
|
1100
2050
|
const text = useSmoothText(children, streaming);
|
|
1101
2051
|
const animating = streaming || text.length < children.length;
|
|
1102
2052
|
const [streamed, setStreamed] = useState(streaming);
|
|
1103
2053
|
if (streaming && !streamed) setStreamed(true);
|
|
1104
2054
|
return /* @__PURE__ */ jsx("div", {
|
|
1105
2055
|
className: clsx("chat-markdown", streamed && "chat-fade", className),
|
|
1106
|
-
children: /* @__PURE__ */ jsx(
|
|
1107
|
-
|
|
1108
|
-
animating,
|
|
1109
|
-
|
|
2056
|
+
children: /* @__PURE__ */ jsx(BlockScope, {
|
|
2057
|
+
id: partId,
|
|
2058
|
+
streaming: animating,
|
|
2059
|
+
children: /* @__PURE__ */ jsx(MarkdownBody, {
|
|
2060
|
+
text,
|
|
2061
|
+
animating,
|
|
2062
|
+
fade: streamed
|
|
2063
|
+
})
|
|
1110
2064
|
})
|
|
1111
2065
|
});
|
|
1112
2066
|
}
|
|
2067
|
+
function BlockScope({ id, streaming, children }) {
|
|
2068
|
+
const value = useMemo(() => id ? {
|
|
2069
|
+
id,
|
|
2070
|
+
streaming
|
|
2071
|
+
} : void 0, [id, streaming]);
|
|
2072
|
+
return /* @__PURE__ */ jsx(BlockContext.Provider, {
|
|
2073
|
+
value,
|
|
2074
|
+
children
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
1113
2077
|
const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
2078
|
+
const renderLink = useContext(LinksContext);
|
|
2079
|
+
const withLinks = useMemo(() => renderLink ? {
|
|
2080
|
+
...components,
|
|
2081
|
+
a: ({ href, children }) => {
|
|
2082
|
+
const custom = href ? renderLink({
|
|
2083
|
+
href,
|
|
2084
|
+
children
|
|
2085
|
+
}) : void 0;
|
|
2086
|
+
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2087
|
+
href,
|
|
2088
|
+
target: "_blank",
|
|
2089
|
+
rel: "noopener noreferrer",
|
|
2090
|
+
children
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
} : components, [renderLink]);
|
|
1114
2094
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
1115
2095
|
prefix: PREFIX,
|
|
1116
2096
|
mode: animating || fade ? "streaming" : "static",
|
|
1117
2097
|
...fade && { rehypePlugins: fadePlugins },
|
|
1118
2098
|
plugins: { math },
|
|
1119
|
-
components,
|
|
2099
|
+
components: withLinks,
|
|
1120
2100
|
controls: false,
|
|
1121
2101
|
lineNumbers: false,
|
|
1122
2102
|
linkSafety: { enabled: false },
|
|
@@ -1156,32 +2136,31 @@ const useChatTool = useAgentTool;
|
|
|
1156
2136
|
/** Streaming or running: the model or a tool is busy with it. */
|
|
1157
2137
|
const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
|
|
1158
2138
|
/** Row label for any activity part. */
|
|
1159
|
-
function activityLabel(part, view) {
|
|
2139
|
+
function activityLabel(part, labels, view) {
|
|
1160
2140
|
if (part.type === "thinking") {
|
|
1161
2141
|
if (part.title) return part.title;
|
|
1162
|
-
if (part.status !== "done") return
|
|
1163
|
-
return part.durationMs ?
|
|
2142
|
+
if (part.status !== "done") return labels.thinking;
|
|
2143
|
+
return part.durationMs ? labels.thoughtFor(formatDuration(part.durationMs, labels)) : labels.thought;
|
|
1164
2144
|
}
|
|
1165
|
-
return view?.label?.(part) ?? part.title ?? part.name;
|
|
2145
|
+
return view?.label?.(part, labels) ?? part.title ?? part.name;
|
|
1166
2146
|
}
|
|
1167
2147
|
/**
|
|
1168
2148
|
* Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
|
|
1169
2149
|
* did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
|
|
1170
2150
|
*/
|
|
1171
|
-
function activityHeading(parts, views) {
|
|
2151
|
+
function activityHeading(parts, views, labels) {
|
|
1172
2152
|
const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
|
|
1173
|
-
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, views[waiting.name]),
|
|
2153
|
+
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
1174
2154
|
if (parts.some(isWorking)) {
|
|
1175
2155
|
const last = parts.at(-1);
|
|
1176
|
-
return activityLabel(last, last.type === "tool_call" ? views[last.name] : void 0);
|
|
2156
|
+
return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
|
|
1177
2157
|
}
|
|
1178
2158
|
const counts = /* @__PURE__ */ new Map();
|
|
1179
2159
|
for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
|
|
1180
|
-
if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
|
|
1181
|
-
const
|
|
1182
|
-
return
|
|
2160
|
+
if (counts.size === 0) return parts[0] ? activityLabel(parts[0], labels) : "";
|
|
2161
|
+
const phrases = [...counts].map(([name, count]) => views[name]?.summary?.(count, labels) ?? labels.usedTool(name));
|
|
2162
|
+
return labels.joinSummaries(phrases);
|
|
1183
2163
|
}
|
|
1184
|
-
const plural = (count, one, many) => count === 1 ? one : many(count);
|
|
1185
2164
|
function recordOf(value) {
|
|
1186
2165
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1187
2166
|
}
|
|
@@ -1190,10 +2169,11 @@ function stringOf(value) {
|
|
|
1190
2169
|
}
|
|
1191
2170
|
/** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
|
|
1192
2171
|
const commandTool = {
|
|
1193
|
-
label: () =>
|
|
2172
|
+
label: (_, labels) => labels.ranCommandLabel,
|
|
1194
2173
|
hint: (part) => stringOf(recordOf(part.input).description),
|
|
1195
|
-
summary: (n
|
|
2174
|
+
summary: (n, labels) => labels.ranCommands(n),
|
|
1196
2175
|
Detail: function CommandDetail({ part }) {
|
|
2176
|
+
const labels = useLabels();
|
|
1197
2177
|
const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
|
|
1198
2178
|
const result = recordOf(part.result);
|
|
1199
2179
|
const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
|
|
@@ -1205,31 +2185,37 @@ const commandTool = {
|
|
|
1205
2185
|
code: command
|
|
1206
2186
|
}),
|
|
1207
2187
|
(output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
|
|
1208
|
-
label:
|
|
1209
|
-
code: output ||
|
|
2188
|
+
label: labels.output,
|
|
2189
|
+
code: output || labels.noOutput
|
|
1210
2190
|
}),
|
|
1211
2191
|
part.error && /* @__PURE__ */ jsx("p", {
|
|
1212
|
-
className: "text-
|
|
2192
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1213
2193
|
children: part.error
|
|
1214
2194
|
})
|
|
1215
2195
|
]
|
|
1216
2196
|
});
|
|
1217
2197
|
}
|
|
1218
2198
|
};
|
|
1219
|
-
/**
|
|
2199
|
+
/**
|
|
2200
|
+
* A web search. Input `{ query }`; results (`{ results: SearchResult[] }` or `SearchResult[]`) from the
|
|
2201
|
+
* part's display `data`, else its `result`.
|
|
2202
|
+
*/
|
|
1220
2203
|
const webSearchTool = {
|
|
1221
|
-
label: () =>
|
|
2204
|
+
label: (_, labels) => labels.searchedWebLabel,
|
|
1222
2205
|
subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
|
|
1223
|
-
summary: (n
|
|
2206
|
+
summary: (n, labels) => labels.searchedWeb(n),
|
|
1224
2207
|
Detail: function WebSearchDetail({ part }) {
|
|
1225
|
-
const
|
|
2208
|
+
const labels = useLabels();
|
|
2209
|
+
const icons = useIcons();
|
|
2210
|
+
const shown = part.data ?? part.result;
|
|
2211
|
+
const results = Array.isArray(shown) ? shown : recordOf(shown).results;
|
|
1226
2212
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1227
|
-
className: "text-
|
|
2213
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1228
2214
|
children: part.error
|
|
1229
2215
|
});
|
|
1230
2216
|
if (!results?.length) return /* @__PURE__ */ jsx("p", {
|
|
1231
|
-
className: "text-
|
|
1232
|
-
children:
|
|
2217
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
2218
|
+
children: labels.noResults
|
|
1233
2219
|
});
|
|
1234
2220
|
return /* @__PURE__ */ jsx("ul", {
|
|
1235
2221
|
className: "-mx-1",
|
|
@@ -1237,22 +2223,22 @@ const webSearchTool = {
|
|
|
1237
2223
|
href: result.url,
|
|
1238
2224
|
target: "_blank",
|
|
1239
2225
|
rel: "noreferrer",
|
|
1240
|
-
className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-
|
|
2226
|
+
className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-(length:--chat-activity-font-size) text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
|
|
1241
2227
|
children: [
|
|
1242
2228
|
result.favicon ? /* @__PURE__ */ jsx("img", {
|
|
1243
2229
|
src: result.favicon,
|
|
1244
2230
|
alt: "",
|
|
1245
2231
|
className: "size-4 shrink-0 rounded-sm"
|
|
1246
|
-
}) : /* @__PURE__ */ jsx(
|
|
1247
|
-
"
|
|
1248
|
-
|
|
2232
|
+
}) : /* @__PURE__ */ jsx(IconSlot, {
|
|
2233
|
+
className: "[&_svg]:size-4",
|
|
2234
|
+
children: icons.web
|
|
1249
2235
|
}),
|
|
1250
2236
|
/* @__PURE__ */ jsx("span", {
|
|
1251
2237
|
className: "min-w-0 flex-1 truncate",
|
|
1252
2238
|
children: result.title
|
|
1253
2239
|
}),
|
|
1254
2240
|
/* @__PURE__ */ jsx("span", {
|
|
1255
|
-
className: "shrink-0 text-
|
|
2241
|
+
className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
|
|
1256
2242
|
children: hostOf(result.url)
|
|
1257
2243
|
})
|
|
1258
2244
|
]
|
|
@@ -1260,18 +2246,44 @@ const webSearchTool = {
|
|
|
1260
2246
|
});
|
|
1261
2247
|
}
|
|
1262
2248
|
};
|
|
2249
|
+
/** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
|
|
2250
|
+
const webFetchTool = {
|
|
2251
|
+
label: (_, labels) => labels.readPageLabel,
|
|
2252
|
+
subtitle: (part) => {
|
|
2253
|
+
const url = stringOf(recordOf(part.input).url);
|
|
2254
|
+
return url ? hostOf(url) : part.subtitle;
|
|
2255
|
+
},
|
|
2256
|
+
summary: (n, labels) => labels.readPages(n),
|
|
2257
|
+
Detail: function WebFetchDetail({ part }) {
|
|
2258
|
+
const url = stringOf(recordOf(part.input).url);
|
|
2259
|
+
const title = stringOf(recordOf(part.data).title);
|
|
2260
|
+
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
2261
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
2262
|
+
children: part.error
|
|
2263
|
+
});
|
|
2264
|
+
if (!url) return null;
|
|
2265
|
+
return /* @__PURE__ */ jsx("a", {
|
|
2266
|
+
href: url,
|
|
2267
|
+
target: "_blank",
|
|
2268
|
+
rel: "noopener noreferrer",
|
|
2269
|
+
className: "block truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg hover:underline",
|
|
2270
|
+
children: title ?? url
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
1263
2274
|
/** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
|
|
1264
2275
|
const memoryTool = {
|
|
1265
|
-
label: () =>
|
|
2276
|
+
label: (_, labels) => labels.recalledMemoryLabel,
|
|
1266
2277
|
subtitle: (part) => {
|
|
1267
2278
|
return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
|
|
1268
2279
|
},
|
|
1269
|
-
summary: () =>
|
|
2280
|
+
summary: (n, labels) => labels.recalledMemory(n)
|
|
1270
2281
|
};
|
|
1271
2282
|
const defaultToolViews = {
|
|
1272
2283
|
bash: commandTool,
|
|
1273
2284
|
shell: commandTool,
|
|
1274
2285
|
web_search: webSearchTool,
|
|
2286
|
+
web_fetch: webFetchTool,
|
|
1275
2287
|
memory: memoryTool,
|
|
1276
2288
|
memory_view: memoryTool
|
|
1277
2289
|
};
|
|
@@ -1283,9 +2295,8 @@ function hostOf(url) {
|
|
|
1283
2295
|
}
|
|
1284
2296
|
}
|
|
1285
2297
|
/** "9s", "1m 5s". */
|
|
1286
|
-
function formatDuration(ms) {
|
|
1287
|
-
|
|
1288
|
-
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
2298
|
+
function formatDuration(ms, labels = defaultLabels) {
|
|
2299
|
+
return labels.duration(Math.max(1, Math.round(ms / 1e3)));
|
|
1289
2300
|
}
|
|
1290
2301
|
// src/components/token-counter.tsx
|
|
1291
2302
|
/** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
|
|
@@ -1330,25 +2341,23 @@ function useCountUp(target) {
|
|
|
1330
2341
|
}
|
|
1331
2342
|
/** A token count that counts up as `tokens` grows and holds still when it stops growing. */
|
|
1332
2343
|
function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
2344
|
+
const icons = useIcons();
|
|
1333
2345
|
const shown = useCountUp(tokens);
|
|
2346
|
+
const labels = useLabels();
|
|
1334
2347
|
if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
|
|
1335
2348
|
className: clsx("text-chat-subtle tabular-nums", className),
|
|
1336
2349
|
children: [
|
|
1337
2350
|
"(",
|
|
1338
|
-
formatTokens(shown),
|
|
1339
|
-
"
|
|
2351
|
+
labels.tokenCount(formatTokens(shown)),
|
|
2352
|
+
")"
|
|
1340
2353
|
]
|
|
1341
2354
|
});
|
|
1342
2355
|
return /* @__PURE__ */ jsxs("span", {
|
|
1343
|
-
className: clsx("inline-flex items-center gap-1 font-chat-sans text-
|
|
1344
|
-
children: [
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}),
|
|
1349
|
-
formatTokens(shown),
|
|
1350
|
-
" tokens"
|
|
1351
|
-
]
|
|
2356
|
+
className: clsx("inline-flex items-center gap-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums", className),
|
|
2357
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2358
|
+
className: "[&_svg]:size-3",
|
|
2359
|
+
children: icons.tokens
|
|
2360
|
+
}), labels.tokenCount(formatTokens(shown))]
|
|
1352
2361
|
});
|
|
1353
2362
|
}
|
|
1354
2363
|
// src/components/activity-group.tsx
|
|
@@ -1359,6 +2368,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
|
1359
2368
|
*/
|
|
1360
2369
|
function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
1361
2370
|
const views = useToolViews();
|
|
2371
|
+
const labels = useLabels();
|
|
1362
2372
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
1363
2373
|
const live = parts.some(isWorking);
|
|
1364
2374
|
const tokens = activityTokens(parts);
|
|
@@ -1380,9 +2390,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
|
1380
2390
|
onExpandedChange: setExpanded,
|
|
1381
2391
|
summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
|
|
1382
2392
|
live,
|
|
1383
|
-
children: activityHeading(parts, views)
|
|
2393
|
+
children: activityHeading(parts, views, labels)
|
|
1384
2394
|
}), after] }),
|
|
1385
|
-
summaryClassName: "text-
|
|
2395
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1386
2396
|
panelClassName: "pt-3",
|
|
1387
2397
|
children: /* @__PURE__ */ jsx("div", {
|
|
1388
2398
|
className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
|
|
@@ -1419,7 +2429,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
1419
2429
|
live,
|
|
1420
2430
|
children: summary
|
|
1421
2431
|
}), after] }),
|
|
1422
|
-
summaryClassName: "text-
|
|
2432
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1423
2433
|
panelClassName: "pt-3",
|
|
1424
2434
|
children: /* @__PURE__ */ jsx("div", {
|
|
1425
2435
|
className: "rounded-chat-md border border-chat-divider px-3 py-2",
|
|
@@ -1432,7 +2442,7 @@ function Step({ part }) {
|
|
|
1432
2442
|
const live = isWorking(part);
|
|
1433
2443
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
1434
2444
|
className: "px-3 py-2",
|
|
1435
|
-
summaryClassName: "text-
|
|
2445
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1436
2446
|
panelClassName: "pt-2 pb-1",
|
|
1437
2447
|
summary: /* @__PURE__ */ jsx(Live, {
|
|
1438
2448
|
live,
|
|
@@ -1443,12 +2453,13 @@ function Step({ part }) {
|
|
|
1443
2453
|
}
|
|
1444
2454
|
/** The label line and expanded body of one step. Tool calls go through their registered view. */
|
|
1445
2455
|
function useStepContent(part) {
|
|
2456
|
+
const labels = useLabels();
|
|
1446
2457
|
const view = useToolView(part.type === "tool_call" ? part.name : "");
|
|
1447
2458
|
if (part.type === "thinking") return {
|
|
1448
|
-
summary: activityLabel(part),
|
|
2459
|
+
summary: activityLabel(part, labels),
|
|
1449
2460
|
detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
|
|
1450
|
-
className: "text-
|
|
1451
|
-
children:
|
|
2461
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle",
|
|
2462
|
+
children: labels.reasoningHidden
|
|
1452
2463
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1453
2464
|
className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
|
|
1454
2465
|
children: /* @__PURE__ */ jsx(Markdown, {
|
|
@@ -1463,7 +2474,7 @@ function useStepContent(part) {
|
|
|
1463
2474
|
return {
|
|
1464
2475
|
summary: /* @__PURE__ */ jsxs("span", {
|
|
1465
2476
|
title: view.hint?.(part),
|
|
1466
|
-
children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
2477
|
+
children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
1467
2478
|
className: "ml-1.5 text-chat-fg",
|
|
1468
2479
|
children: subtitle
|
|
1469
2480
|
})]
|
|
@@ -1473,16 +2484,16 @@ function useStepContent(part) {
|
|
|
1473
2484
|
}
|
|
1474
2485
|
function DefaultDetail({ part }) {
|
|
1475
2486
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1476
|
-
className: "text-
|
|
2487
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1477
2488
|
children: part.error
|
|
1478
2489
|
});
|
|
1479
2490
|
const body = part.result ?? part.input ?? part.inputJson;
|
|
1480
2491
|
if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
|
|
1481
|
-
className: "text-
|
|
2492
|
+
className: "text-(length:--chat-activity-font-size) leading-relaxed whitespace-pre-wrap text-chat-muted",
|
|
1482
2493
|
children: body
|
|
1483
2494
|
});
|
|
1484
2495
|
return /* @__PURE__ */ jsx("pre", {
|
|
1485
|
-
className: "max-h-56 overflow-auto font-chat-mono text-
|
|
2496
|
+
className: "max-h-56 overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-relaxed text-chat-muted",
|
|
1486
2497
|
children: JSON.stringify(body, null, 2)
|
|
1487
2498
|
});
|
|
1488
2499
|
}
|
|
@@ -1493,6 +2504,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
|
|
|
1493
2504
|
function useAttachments({ upload } = {}) {
|
|
1494
2505
|
const [attachments, setAttachments] = useState([]);
|
|
1495
2506
|
const uploads = useRef(/* @__PURE__ */ new Map());
|
|
2507
|
+
const labels = useLabels();
|
|
2508
|
+
const labelsRef = useRef(labels);
|
|
2509
|
+
labelsRef.current = labels;
|
|
1496
2510
|
const uploadRef = useRef(upload);
|
|
1497
2511
|
uploadRef.current = upload;
|
|
1498
2512
|
const patch = useCallback((id, fields) => {
|
|
@@ -1526,7 +2540,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1526
2540
|
})).catch((error) => {
|
|
1527
2541
|
if (!controller.signal.aborted) patch(id, {
|
|
1528
2542
|
status: "error",
|
|
1529
|
-
error: error instanceof Error ? error.message :
|
|
2543
|
+
error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
|
|
1530
2544
|
});
|
|
1531
2545
|
}).finally(() => uploads.current.delete(id));
|
|
1532
2546
|
}
|
|
@@ -1535,7 +2549,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1535
2549
|
setAttachments((list) => [...list, {
|
|
1536
2550
|
id: draftId(),
|
|
1537
2551
|
kind: "text",
|
|
1538
|
-
name:
|
|
2552
|
+
name: labelsRef.current.pastedText,
|
|
1539
2553
|
mime: "text/plain",
|
|
1540
2554
|
size: new Blob([text]).size,
|
|
1541
2555
|
text,
|
|
@@ -1581,13 +2595,15 @@ function useAttachments({ upload } = {}) {
|
|
|
1581
2595
|
const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
|
|
1582
2596
|
/** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
|
|
1583
2597
|
function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
|
|
2598
|
+
const icons = useIcons();
|
|
1584
2599
|
const image = a.mime.startsWith("image/") && a.url;
|
|
1585
2600
|
const pasted = a.text !== void 0;
|
|
2601
|
+
const labels = useLabels();
|
|
1586
2602
|
const uploading = a.status === "uploading";
|
|
1587
2603
|
const failed = a.status === "error";
|
|
1588
2604
|
return /* @__PURE__ */ jsxs("div", {
|
|
1589
2605
|
className: clsx("group/card relative shrink-0 rounded-chat-md border bg-chat-bg font-chat-sans", failed ? "border-chat-danger" : "border-chat-border", size === "md" ? "size-30" : "size-24", className),
|
|
1590
|
-
title: failed ? a.error ??
|
|
2606
|
+
title: failed ? a.error ?? labels.uploadFailed : a.name,
|
|
1591
2607
|
children: [
|
|
1592
2608
|
/* @__PURE__ */ jsx("div", {
|
|
1593
2609
|
className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
|
|
@@ -1600,11 +2616,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1600
2616
|
children: a.text
|
|
1601
2617
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
1602
2618
|
className: "flex h-full flex-col gap-1.5 p-2.5",
|
|
1603
|
-
children: [/* @__PURE__ */ jsx(
|
|
1604
|
-
"
|
|
1605
|
-
|
|
2619
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2620
|
+
className: "text-chat-muted [&_svg]:size-5",
|
|
2621
|
+
children: icons.file
|
|
1606
2622
|
}), /* @__PURE__ */ jsx("span", {
|
|
1607
|
-
className: "line-clamp-3 text-
|
|
2623
|
+
className: "line-clamp-3 text-(length:--chat-ui-font-size-sm) leading-4 break-words text-chat-fg",
|
|
1608
2624
|
children: a.name
|
|
1609
2625
|
})]
|
|
1610
2626
|
})
|
|
@@ -1613,29 +2629,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1613
2629
|
className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
|
|
1614
2630
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1615
2631
|
className: clsx("rounded-[0.3rem] border bg-chat-bg px-1.5 py-0.5 text-[0.625rem] leading-none tracking-wide uppercase", failed ? "border-chat-danger text-chat-danger" : "border-chat-border-strong text-chat-muted"),
|
|
1616
|
-
children: failed ?
|
|
2632
|
+
children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
|
|
1617
2633
|
}), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
|
|
1618
|
-
label:
|
|
2634
|
+
label: labels.editPastedText,
|
|
1619
2635
|
size: "sm",
|
|
1620
2636
|
onPress: onOpen,
|
|
1621
2637
|
className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
|
|
1622
|
-
children:
|
|
2638
|
+
children: icons.edit
|
|
1623
2639
|
})]
|
|
1624
2640
|
}),
|
|
1625
2641
|
pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
|
|
1626
2642
|
type: "button",
|
|
1627
|
-
"aria-label":
|
|
2643
|
+
"aria-label": labels.viewPastedText,
|
|
1628
2644
|
onClick: onOpen,
|
|
1629
2645
|
className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
|
|
1630
2646
|
}),
|
|
1631
2647
|
onRemove && /* @__PURE__ */ jsx(IconButton, {
|
|
1632
|
-
label:
|
|
2648
|
+
label: labels.removeAttachment(a.name),
|
|
1633
2649
|
variant: "subtle",
|
|
1634
2650
|
size: "sm",
|
|
1635
2651
|
tooltip: false,
|
|
1636
2652
|
onPress: onRemove,
|
|
1637
2653
|
className: clsx("absolute -top-2 -right-2 size-6 rounded-full border border-chat-border-strong [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!", "opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100 data-focus-visible:opacity-100 [@media(hover:none)]:opacity-100"),
|
|
1638
|
-
children:
|
|
2654
|
+
children: icons.remove
|
|
1639
2655
|
})
|
|
1640
2656
|
]
|
|
1641
2657
|
});
|
|
@@ -1643,9 +2659,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1643
2659
|
/** The composer's row of cards above the text. Renders nothing when empty. */
|
|
1644
2660
|
function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
1645
2661
|
const [editing, setEditing] = useState();
|
|
2662
|
+
const labels = useLabels();
|
|
1646
2663
|
if (attachments.length === 0) return null;
|
|
1647
2664
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
1648
|
-
"aria-label":
|
|
2665
|
+
"aria-label": labels.attachments,
|
|
1649
2666
|
className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
|
|
1650
2667
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1651
2668
|
attachment: {
|
|
@@ -1662,7 +2679,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
|
1662
2679
|
})] });
|
|
1663
2680
|
}
|
|
1664
2681
|
/** Shows pasted text in a modal, editable when `onSave` is set. */
|
|
1665
|
-
function PastedTextDialog({ text, onClose, onSave, title
|
|
2682
|
+
function PastedTextDialog({ text, onClose, onSave, title }) {
|
|
2683
|
+
const labels = useLabels();
|
|
2684
|
+
title ??= labels.pastedText;
|
|
1666
2685
|
const [draft, setDraft] = useState(text ?? "");
|
|
1667
2686
|
useEffect(() => setDraft(text ?? ""), [text]);
|
|
1668
2687
|
return /* @__PURE__ */ jsxs(Modal, {
|
|
@@ -1674,34 +2693,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
|
|
|
1674
2693
|
onChange: (e) => setDraft(e.target.value),
|
|
1675
2694
|
readOnly: !onSave,
|
|
1676
2695
|
"aria-label": title,
|
|
1677
|
-
className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-
|
|
2696
|
+
className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg outline-none focus:border-chat-border-strong [scrollbar-width:thin]"
|
|
1678
2697
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1679
2698
|
className: "flex justify-end gap-2 px-5 py-4",
|
|
1680
2699
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
1681
2700
|
variant: "subtle",
|
|
1682
2701
|
onPress: onClose,
|
|
1683
|
-
children: onSave ?
|
|
2702
|
+
children: onSave ? labels.cancel : labels.close
|
|
1684
2703
|
}), onSave && /* @__PURE__ */ jsx(Button, {
|
|
1685
2704
|
variant: "primary",
|
|
1686
2705
|
onPress: () => {
|
|
1687
2706
|
onSave(draft);
|
|
1688
2707
|
onClose();
|
|
1689
2708
|
},
|
|
1690
|
-
children:
|
|
2709
|
+
children: labels.save
|
|
1691
2710
|
})]
|
|
1692
2711
|
})]
|
|
1693
2712
|
});
|
|
1694
2713
|
}
|
|
1695
2714
|
// src/components/messages.tsx
|
|
1696
2715
|
/** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
|
|
1697
|
-
function formatRelativeTime(iso, now = Date.now()) {
|
|
2716
|
+
function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
|
|
1698
2717
|
const seconds = (new Date(iso).getTime() - now) / 1e3;
|
|
1699
|
-
const rtf = new Intl.RelativeTimeFormat(
|
|
1700
|
-
if (Math.abs(seconds) < 60) return
|
|
2718
|
+
const rtf = new Intl.RelativeTimeFormat(labels.locale, { numeric: "auto" });
|
|
2719
|
+
if (Math.abs(seconds) < 60) return labels.justNow;
|
|
1701
2720
|
if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
|
|
1702
2721
|
if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
|
|
1703
2722
|
if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
|
|
1704
|
-
return new Date(iso).toLocaleDateString(
|
|
2723
|
+
return new Date(iso).toLocaleDateString(labels.locale, {
|
|
1705
2724
|
month: "short",
|
|
1706
2725
|
day: "numeric"
|
|
1707
2726
|
});
|
|
@@ -1711,21 +2730,25 @@ function messageText(message) {
|
|
|
1711
2730
|
return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
|
|
1712
2731
|
}
|
|
1713
2732
|
function CopyButton({ text }) {
|
|
2733
|
+
const icons = useIcons();
|
|
2734
|
+
const labels = useLabels();
|
|
1714
2735
|
const [copied, setCopied] = useState(false);
|
|
1715
2736
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
1716
|
-
label: copied ?
|
|
2737
|
+
label: copied ? labels.copied : labels.copy,
|
|
1717
2738
|
size: "sm",
|
|
1718
2739
|
onPress: () => {
|
|
1719
2740
|
navigator.clipboard?.writeText(text);
|
|
1720
2741
|
setCopied(true);
|
|
1721
2742
|
setTimeout(() => setCopied(false), 1500);
|
|
1722
2743
|
},
|
|
1723
|
-
children: copied ?
|
|
2744
|
+
children: copied ? icons.copied : icons.copy
|
|
1724
2745
|
});
|
|
1725
2746
|
}
|
|
1726
|
-
const timeClass = "px-1 font-chat-sans text-
|
|
2747
|
+
const timeClass = "px-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle";
|
|
1727
2748
|
/** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
|
|
1728
|
-
function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
2749
|
+
function UserMessage({ message, onRetry, onEdit, actions, copy = true, time = true, className }) {
|
|
2750
|
+
const icons = useIcons();
|
|
2751
|
+
const labels = useLabels();
|
|
1729
2752
|
const text = messageText(message);
|
|
1730
2753
|
const attachments = message.parts.filter((p) => p.type === "attachment");
|
|
1731
2754
|
const [viewing, setViewing] = useState();
|
|
@@ -1733,7 +2756,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1733
2756
|
className: clsx("group/message flex flex-col items-end", className),
|
|
1734
2757
|
children: [
|
|
1735
2758
|
attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1736
|
-
"aria-label":
|
|
2759
|
+
"aria-label": labels.attachments,
|
|
1737
2760
|
className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
|
|
1738
2761
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1739
2762
|
attachment: a,
|
|
@@ -1745,41 +2768,73 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1745
2768
|
text: viewing,
|
|
1746
2769
|
onClose: () => setViewing(void 0)
|
|
1747
2770
|
}),
|
|
1748
|
-
text && /* @__PURE__ */ jsx(
|
|
1749
|
-
className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-fg",
|
|
1750
|
-
children: text
|
|
1751
|
-
}),
|
|
2771
|
+
text && /* @__PURE__ */ jsx(UserBubble, { text }),
|
|
1752
2772
|
/* @__PURE__ */ jsxs("div", {
|
|
1753
2773
|
className: "mt-1 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100",
|
|
1754
2774
|
children: [
|
|
1755
|
-
/* @__PURE__ */ jsx("span", {
|
|
2775
|
+
time && /* @__PURE__ */ jsx("span", {
|
|
1756
2776
|
className: timeClass,
|
|
1757
|
-
children: formatRelativeTime(message.createdAt)
|
|
2777
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1758
2778
|
}),
|
|
1759
2779
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1760
|
-
label:
|
|
2780
|
+
label: labels.retry,
|
|
1761
2781
|
size: "sm",
|
|
1762
2782
|
onPress: onRetry,
|
|
1763
|
-
children:
|
|
2783
|
+
children: icons.retry
|
|
1764
2784
|
}),
|
|
1765
2785
|
onEdit && /* @__PURE__ */ jsx(IconButton, {
|
|
1766
|
-
label:
|
|
2786
|
+
label: labels.edit,
|
|
1767
2787
|
size: "sm",
|
|
1768
2788
|
onPress: onEdit,
|
|
1769
|
-
children:
|
|
2789
|
+
children: icons.edit
|
|
1770
2790
|
}),
|
|
1771
|
-
/* @__PURE__ */ jsx(CopyButton, { text }),
|
|
2791
|
+
copy && /* @__PURE__ */ jsx(CopyButton, { text }),
|
|
1772
2792
|
actions
|
|
1773
2793
|
]
|
|
1774
2794
|
})
|
|
1775
2795
|
]
|
|
1776
2796
|
});
|
|
1777
2797
|
}
|
|
2798
|
+
/** Taller than this (px), a user message folds to this height with "Show more", like a long paste. */
|
|
2799
|
+
const FOLD_HEIGHT = 240;
|
|
2800
|
+
function UserBubble({ text }) {
|
|
2801
|
+
const body = useRef(null);
|
|
2802
|
+
const [long, setLong] = useState(false);
|
|
2803
|
+
const [open, setOpen] = useState(false);
|
|
2804
|
+
const labels = useLabels();
|
|
2805
|
+
useLayoutEffect(() => {
|
|
2806
|
+
const el = body.current;
|
|
2807
|
+
if (!el) return;
|
|
2808
|
+
const measure = () => setLong(el.scrollHeight > 264);
|
|
2809
|
+
measure();
|
|
2810
|
+
const observer = new ResizeObserver(measure);
|
|
2811
|
+
observer.observe(el);
|
|
2812
|
+
return () => observer.disconnect();
|
|
2813
|
+
}, [text]);
|
|
2814
|
+
const folded = long && !open;
|
|
2815
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2816
|
+
className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-(length:--chat-bubble-font-size) leading-relaxed text-chat-fg",
|
|
2817
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2818
|
+
ref: body,
|
|
2819
|
+
style: folded ? { maxHeight: FOLD_HEIGHT } : void 0,
|
|
2820
|
+
className: clsx("whitespace-pre-wrap", folded && "overflow-hidden mask-b-from-60% mask-b-to-100%"),
|
|
2821
|
+
children: text
|
|
2822
|
+
}), long && /* @__PURE__ */ jsx("button", {
|
|
2823
|
+
type: "button",
|
|
2824
|
+
"aria-expanded": open,
|
|
2825
|
+
onClick: () => setOpen(!open),
|
|
2826
|
+
className: "mt-1.5 cursor-pointer rounded-chat-sm text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
|
|
2827
|
+
children: open ? labels.showLess : labels.showMore
|
|
2828
|
+
})]
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
1778
2831
|
/**
|
|
1779
2832
|
* The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
|
|
1780
2833
|
* order. When done, copy / read aloud / feedback / retry and the time sit under it.
|
|
1781
2834
|
*/
|
|
1782
|
-
function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
|
|
2835
|
+
function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, copy = true, time = true, className }) {
|
|
2836
|
+
const icons = useIcons();
|
|
2837
|
+
const labels = useLabels();
|
|
1783
2838
|
const streaming = message.status === "streaming";
|
|
1784
2839
|
const views = useToolViews();
|
|
1785
2840
|
const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
|
|
@@ -1787,56 +2842,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1787
2842
|
className: clsx("group/message flex flex-col gap-4", className),
|
|
1788
2843
|
children: [
|
|
1789
2844
|
blocks.map((block) => {
|
|
1790
|
-
if (block.kind === "activity")
|
|
2845
|
+
if (block.kind === "activity") {
|
|
2846
|
+
const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
|
|
2847
|
+
if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
|
|
2848
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
|
|
2849
|
+
if (p.type !== "tool_call") return null;
|
|
2850
|
+
const Card = views[p.name].Card;
|
|
2851
|
+
return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
|
|
2852
|
+
})] }, block.id);
|
|
2853
|
+
}
|
|
1791
2854
|
if (block.kind === "tool") {
|
|
1792
2855
|
const ToolBlock = views[block.part.name].Block;
|
|
1793
2856
|
return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
|
|
1794
2857
|
}
|
|
1795
2858
|
const part = block.part;
|
|
1796
2859
|
if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
|
|
2860
|
+
partId: part.id,
|
|
1797
2861
|
streaming: part.status === "streaming",
|
|
1798
2862
|
children: part.text
|
|
1799
2863
|
}, part.id);
|
|
1800
2864
|
const custom = renderPart?.(part);
|
|
1801
2865
|
if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
|
|
2866
|
+
if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
|
|
1802
2867
|
if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
|
|
1803
2868
|
return null;
|
|
1804
2869
|
}),
|
|
1805
2870
|
message.status === "error" && /* @__PURE__ */ jsx("p", {
|
|
1806
|
-
className: "font-chat-sans text-
|
|
1807
|
-
children: message.error ??
|
|
2871
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
2872
|
+
children: message.error ?? labels.somethingWentWrong
|
|
1808
2873
|
}),
|
|
1809
2874
|
streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
|
|
1810
2875
|
className: "-ml-1.5 flex h-7 items-center gap-0.5",
|
|
1811
2876
|
children: [
|
|
1812
|
-
/* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
|
|
2877
|
+
copy && /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
|
|
1813
2878
|
onReadAloud && /* @__PURE__ */ jsx(IconButton, {
|
|
1814
|
-
label:
|
|
2879
|
+
label: labels.readAloud,
|
|
1815
2880
|
size: "sm",
|
|
1816
2881
|
onPress: onReadAloud,
|
|
1817
|
-
children:
|
|
2882
|
+
children: icons.speak
|
|
1818
2883
|
}),
|
|
1819
2884
|
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
1820
|
-
label:
|
|
2885
|
+
label: labels.goodResponse,
|
|
1821
2886
|
size: "sm",
|
|
1822
2887
|
onPress: () => onFeedback("good"),
|
|
1823
|
-
children:
|
|
2888
|
+
children: icons.thumbsUp
|
|
1824
2889
|
}), /* @__PURE__ */ jsx(IconButton, {
|
|
1825
|
-
label:
|
|
2890
|
+
label: labels.badResponse,
|
|
1826
2891
|
size: "sm",
|
|
1827
2892
|
onPress: () => onFeedback("bad"),
|
|
1828
|
-
children:
|
|
2893
|
+
children: icons.thumbsDown
|
|
1829
2894
|
})] }),
|
|
1830
2895
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1831
|
-
label:
|
|
2896
|
+
label: labels.retry,
|
|
1832
2897
|
size: "sm",
|
|
1833
2898
|
onPress: onRetry,
|
|
1834
|
-
children:
|
|
2899
|
+
children: icons.retry
|
|
1835
2900
|
}),
|
|
1836
2901
|
actions,
|
|
1837
|
-
/* @__PURE__ */ jsx("span", {
|
|
2902
|
+
time && /* @__PURE__ */ jsx("span", {
|
|
1838
2903
|
className: timeClass,
|
|
1839
|
-
children: formatRelativeTime(message.createdAt)
|
|
2904
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1840
2905
|
})
|
|
1841
2906
|
]
|
|
1842
2907
|
})
|
|
@@ -1849,7 +2914,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1849
2914
|
* view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
|
|
1850
2915
|
*/
|
|
1851
2916
|
function ApprovalCard({ approval, onRespond, className }) {
|
|
2917
|
+
const icons = useIcons();
|
|
1852
2918
|
const { part } = approval;
|
|
2919
|
+
const labels = useLabels();
|
|
1853
2920
|
const view = useToolView(part.name);
|
|
1854
2921
|
const Detail = view.Detail;
|
|
1855
2922
|
const preview = {
|
|
@@ -1858,21 +2925,17 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1858
2925
|
};
|
|
1859
2926
|
return /* @__PURE__ */ jsxs("div", {
|
|
1860
2927
|
role: "alertdialog",
|
|
1861
|
-
"aria-label":
|
|
2928
|
+
"aria-label": labels.allowTool(part.name),
|
|
1862
2929
|
className: clsx("flex flex-col gap-3 rounded-chat-lg border border-chat-border-strong bg-chat-surface p-3 font-chat-sans text-chat-fg shadow-chat-popover", className),
|
|
1863
2930
|
children: [
|
|
1864
2931
|
/* @__PURE__ */ jsxs("div", {
|
|
1865
|
-
className: "flex items-center gap-2 text-
|
|
1866
|
-
children: [/* @__PURE__ */ jsx(
|
|
1867
|
-
"
|
|
1868
|
-
|
|
2932
|
+
className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5",
|
|
2933
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2934
|
+
className: "text-chat-accent [&_svg]:size-4",
|
|
2935
|
+
children: icons.approval
|
|
1869
2936
|
}), /* @__PURE__ */ jsx("span", {
|
|
1870
2937
|
className: "min-w-0 flex-1 font-medium",
|
|
1871
|
-
children: view.ask?.(part) ??
|
|
1872
|
-
"Allow ",
|
|
1873
|
-
activityLabel(part, view),
|
|
1874
|
-
"?"
|
|
1875
|
-
] })
|
|
2938
|
+
children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
|
|
1876
2939
|
})]
|
|
1877
2940
|
}),
|
|
1878
2941
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -1889,12 +2952,12 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1889
2952
|
variant: "ghost",
|
|
1890
2953
|
className: "border border-chat-border-strong",
|
|
1891
2954
|
onPress: () => onRespond(part.id, false),
|
|
1892
|
-
children:
|
|
2955
|
+
children: labels.deny
|
|
1893
2956
|
}), /* @__PURE__ */ jsx(Button, {
|
|
1894
2957
|
variant: "primary",
|
|
1895
2958
|
autoFocus: true,
|
|
1896
2959
|
onPress: () => onRespond(part.id, true),
|
|
1897
|
-
children:
|
|
2960
|
+
children: labels.allow
|
|
1898
2961
|
})]
|
|
1899
2962
|
})
|
|
1900
2963
|
]
|
|
@@ -1907,7 +2970,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
|
|
|
1907
2970
|
* What fills the context window: a usage line, a segmented bar, one row per category with its share, and
|
|
1908
2971
|
* expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
|
|
1909
2972
|
*/
|
|
1910
|
-
function ContextPanel({ usage, title
|
|
2973
|
+
function ContextPanel({ usage, title, defaultExpanded = true, className }) {
|
|
2974
|
+
const labels = useLabels();
|
|
1911
2975
|
const counted = usage.categories.filter((c) => !c.deferred);
|
|
1912
2976
|
const deferred = usage.categories.filter((c) => c.deferred);
|
|
1913
2977
|
const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
|
|
@@ -1915,16 +2979,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1915
2979
|
const free = Math.max(usage.limit - used - reserved, 0);
|
|
1916
2980
|
const share = (tokens) => formatShare(tokens / usage.limit);
|
|
1917
2981
|
return /* @__PURE__ */ jsxs(Disclosure, {
|
|
1918
|
-
summary: title,
|
|
2982
|
+
summary: title ?? labels.context,
|
|
1919
2983
|
defaultExpanded,
|
|
1920
2984
|
className: clsx("font-chat-sans text-chat-fg", className),
|
|
1921
|
-
summaryClassName: "text-
|
|
2985
|
+
summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
|
|
1922
2986
|
panelClassName: "pt-3",
|
|
1923
2987
|
children: [
|
|
1924
2988
|
/* @__PURE__ */ jsxs("div", {
|
|
1925
|
-
className: "mb-2 flex items-baseline justify-between text-
|
|
1926
|
-
children: [/* @__PURE__ */
|
|
1927
|
-
className: "text-
|
|
2989
|
+
className: "mb-2 flex items-baseline justify-between text-(length:--chat-activity-font-size)",
|
|
2990
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.percentUsed(Math.round(used / usage.limit * 100)) }), /* @__PURE__ */ jsxs("span", {
|
|
2991
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted tabular-nums",
|
|
1928
2992
|
children: [
|
|
1929
2993
|
formatTokens(used),
|
|
1930
2994
|
" / ",
|
|
@@ -1934,7 +2998,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1934
2998
|
}),
|
|
1935
2999
|
/* @__PURE__ */ jsx("div", {
|
|
1936
3000
|
role: "meter",
|
|
1937
|
-
"aria-label":
|
|
3001
|
+
"aria-label": labels.contextUsed,
|
|
1938
3002
|
"aria-valuemin": 0,
|
|
1939
3003
|
"aria-valuemax": usage.limit,
|
|
1940
3004
|
"aria-valuenow": used,
|
|
@@ -1956,7 +3020,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1956
3020
|
}, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
|
|
1957
3021
|
category: {
|
|
1958
3022
|
id: "free",
|
|
1959
|
-
label:
|
|
3023
|
+
label: labels.freeSpace,
|
|
1960
3024
|
tokens: free,
|
|
1961
3025
|
color: FREE_COLOR
|
|
1962
3026
|
},
|
|
@@ -1987,7 +3051,7 @@ function CategoryRow({ category, share }) {
|
|
|
1987
3051
|
}) })
|
|
1988
3052
|
}) : category.label;
|
|
1989
3053
|
return /* @__PURE__ */ jsxs("li", {
|
|
1990
|
-
className: "flex items-center gap-2.5 text-
|
|
3054
|
+
className: "flex items-center gap-2.5 text-(length:--chat-ui-font-size) leading-5",
|
|
1991
3055
|
children: [
|
|
1992
3056
|
/* @__PURE__ */ jsx("span", {
|
|
1993
3057
|
"aria-hidden": true,
|
|
@@ -2013,16 +3077,16 @@ function GroupList({ group }) {
|
|
|
2013
3077
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2014
3078
|
summary: group.label,
|
|
2015
3079
|
trailing: /* @__PURE__ */ jsx("span", {
|
|
2016
|
-
className: "text-
|
|
3080
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
|
|
2017
3081
|
children: group.items.length
|
|
2018
3082
|
}),
|
|
2019
3083
|
indicator: "chevron",
|
|
2020
|
-
summaryClassName: "text-
|
|
3084
|
+
summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
|
|
2021
3085
|
panelClassName: "pt-1.5",
|
|
2022
3086
|
children: /* @__PURE__ */ jsx("ul", {
|
|
2023
3087
|
className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
|
|
2024
3088
|
children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
2025
|
-
className: "flex items-center gap-2 text-
|
|
3089
|
+
className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
2026
3090
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2027
3091
|
className: "min-w-0 flex-1 truncate",
|
|
2028
3092
|
children: item.label
|
|
@@ -2041,7 +3105,9 @@ function formatShare(fraction) {
|
|
|
2041
3105
|
/** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
|
|
2042
3106
|
const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
|
|
2043
3107
|
/** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
|
|
2044
|
-
function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
3108
|
+
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 }) {
|
|
3109
|
+
const icons = useIcons();
|
|
3110
|
+
const labels = useLabels();
|
|
2045
3111
|
const [own, setOwn] = useState("");
|
|
2046
3112
|
const text = value ?? own;
|
|
2047
3113
|
const setText = (next) => {
|
|
@@ -2114,21 +3180,21 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2114
3180
|
};
|
|
2115
3181
|
const compact = variant === "compact";
|
|
2116
3182
|
const action = busy ? /* @__PURE__ */ jsx(IconButton, {
|
|
2117
|
-
label:
|
|
3183
|
+
label: labels.stop,
|
|
2118
3184
|
variant: "primary",
|
|
2119
3185
|
onPress: onStop,
|
|
2120
|
-
className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
|
|
2121
|
-
children:
|
|
3186
|
+
className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]! [&_svg]:size-3.5 [&_svg]:fill-current",
|
|
3187
|
+
children: icons.stop
|
|
2122
3188
|
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
2123
|
-
label:
|
|
3189
|
+
label: labels.send,
|
|
2124
3190
|
variant: "primary",
|
|
2125
3191
|
onPress: submit,
|
|
2126
3192
|
isDisabled: !canSend,
|
|
2127
|
-
className: "rounded-chat-md",
|
|
2128
|
-
children:
|
|
3193
|
+
className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]!",
|
|
3194
|
+
children: icons.send
|
|
2129
3195
|
});
|
|
2130
3196
|
return /* @__PURE__ */ jsxs("div", {
|
|
2131
|
-
className: clsx("flex flex-col rounded-[1.25rem] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
|
|
3197
|
+
className: clsx("flex flex-col rounded-[var(--chat-composer-radius,1.25rem)] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
|
|
2132
3198
|
...dropProps,
|
|
2133
3199
|
onPointerDown: (e) => {
|
|
2134
3200
|
if (e.target === e.currentTarget) {
|
|
@@ -2147,8 +3213,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2147
3213
|
onChange: (e) => setText(e.target.value),
|
|
2148
3214
|
onKeyDown,
|
|
2149
3215
|
onPaste,
|
|
2150
|
-
placeholder,
|
|
2151
|
-
"aria-label":
|
|
3216
|
+
placeholder: placeholder ?? labels.replyPlaceholder,
|
|
3217
|
+
"aria-label": labels.message,
|
|
2152
3218
|
disabled: isDisabled,
|
|
2153
3219
|
autoFocus,
|
|
2154
3220
|
className: clsx("w-full resize-none bg-transparent px-1.5 text-chat-fg outline-none placeholder:text-chat-subtle", "text-base leading-relaxed [scrollbar-width:thin]", compact ? "max-h-[min(10rem,30cqh)] py-1" : variant === "centered" ? "max-h-[min(24rem,45cqh)] min-h-12 pt-1" : "max-h-[min(18rem,35cqh)] min-h-6 pt-1")
|
|
@@ -2172,18 +3238,24 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2172
3238
|
});
|
|
2173
3239
|
}
|
|
2174
3240
|
/** The `+` button that opens the attach menu above the composer. */
|
|
2175
|
-
function AttachButton({ children, label
|
|
3241
|
+
function AttachButton({ children, label, icon, className, menuClassName }) {
|
|
3242
|
+
const icons = useIcons();
|
|
3243
|
+
const labels = useLabels();
|
|
2176
3244
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2177
|
-
label,
|
|
2178
|
-
|
|
3245
|
+
label: label ?? labels.addFiles,
|
|
3246
|
+
...className && { className },
|
|
3247
|
+
children: icon ?? icons.attach
|
|
2179
3248
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2180
3249
|
placement: "top start",
|
|
3250
|
+
...menuClassName && { className: menuClassName },
|
|
2181
3251
|
children
|
|
2182
3252
|
})] });
|
|
2183
3253
|
}
|
|
2184
3254
|
// src/components/model-picker.tsx
|
|
2185
3255
|
/** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
|
|
2186
|
-
function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote
|
|
3256
|
+
function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote, footnote, className }) {
|
|
3257
|
+
const icons = useIcons();
|
|
3258
|
+
const labels = useLabels();
|
|
2187
3259
|
const current = models.find((m) => m.id === model);
|
|
2188
3260
|
const efforts = current?.efforts ?? [];
|
|
2189
3261
|
const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
|
|
@@ -2202,7 +3274,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2202
3274
|
};
|
|
2203
3275
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
2204
3276
|
size: "sm",
|
|
2205
|
-
"aria-label":
|
|
3277
|
+
"aria-label": labels.modelButton(current?.label ?? model),
|
|
2206
3278
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
2207
3279
|
children: [
|
|
2208
3280
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -2213,16 +3285,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2213
3285
|
className: "chat-picker-detail text-chat-muted",
|
|
2214
3286
|
children: currentEffort.label
|
|
2215
3287
|
}),
|
|
2216
|
-
/* @__PURE__ */ jsx(
|
|
2217
|
-
"
|
|
2218
|
-
|
|
3288
|
+
/* @__PURE__ */ jsx(IconSlot, {
|
|
3289
|
+
className: "[&_svg]:size-3.5",
|
|
3290
|
+
children: icons.dropdown
|
|
2219
3291
|
})
|
|
2220
3292
|
]
|
|
2221
3293
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2222
3294
|
placement: "top end",
|
|
2223
3295
|
className: "w-72",
|
|
2224
3296
|
children: [/* @__PURE__ */ jsxs(Menu, {
|
|
2225
|
-
"aria-label":
|
|
3297
|
+
"aria-label": labels.model,
|
|
2226
3298
|
children: [
|
|
2227
3299
|
/* @__PURE__ */ jsx(MenuSection, {
|
|
2228
3300
|
selectionMode: "single",
|
|
@@ -2234,12 +3306,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2234
3306
|
efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2235
3307
|
id: "__effort",
|
|
2236
3308
|
detail: currentEffort?.label,
|
|
2237
|
-
children:
|
|
3309
|
+
children: labels.effort
|
|
2238
3310
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2239
3311
|
placement: "end top",
|
|
2240
3312
|
className: "w-80",
|
|
2241
|
-
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
2242
|
-
"aria-label":
|
|
3313
|
+
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
3314
|
+
"aria-label": labels.effort,
|
|
2243
3315
|
selectionMode: "single",
|
|
2244
3316
|
selectedKeys: currentEffort ? [currentEffort.id] : [],
|
|
2245
3317
|
onSelectionChange: (keys) => {
|
|
@@ -2254,10 +3326,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2254
3326
|
className: "flex items-center gap-2",
|
|
2255
3327
|
children: [
|
|
2256
3328
|
e.label,
|
|
2257
|
-
e.recommended && /* @__PURE__ */ jsx(Badge, { children:
|
|
3329
|
+
e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
|
|
2258
3330
|
e.warning && /* @__PURE__ */ jsx(Badge, {
|
|
2259
3331
|
tone: "warning",
|
|
2260
|
-
icon:
|
|
3332
|
+
icon: icons.warning,
|
|
2261
3333
|
children: e.warning
|
|
2262
3334
|
})
|
|
2263
3335
|
]
|
|
@@ -2267,12 +3339,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2267
3339
|
})] }),
|
|
2268
3340
|
more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2269
3341
|
id: "__more",
|
|
2270
|
-
children:
|
|
3342
|
+
children: labels.moreModels
|
|
2271
3343
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2272
3344
|
placement: "end top",
|
|
2273
3345
|
className: "w-72",
|
|
2274
3346
|
children: /* @__PURE__ */ jsx(Menu, {
|
|
2275
|
-
"aria-label":
|
|
3347
|
+
"aria-label": labels.moreModels,
|
|
2276
3348
|
selectionMode: "single",
|
|
2277
3349
|
selectedKeys: [model],
|
|
2278
3350
|
onSelectionChange: pick,
|
|
@@ -2281,7 +3353,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2281
3353
|
})] })
|
|
2282
3354
|
]
|
|
2283
3355
|
}), footnote && /* @__PURE__ */ jsx(MenuNote, {
|
|
2284
|
-
className: "border-t border-chat-popover-border text-
|
|
3356
|
+
className: "border-t border-chat-popover-border text-(length:--chat-ui-font-size-sm) leading-4",
|
|
2285
3357
|
children: footnote
|
|
2286
3358
|
})]
|
|
2287
3359
|
})] });
|
|
@@ -2293,10 +3365,12 @@ const STICK_SLOP = 48;
|
|
|
2293
3365
|
* The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
|
|
2294
3366
|
* them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
|
|
2295
3367
|
*/
|
|
2296
|
-
function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer, className }) {
|
|
3368
|
+
function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actions, renderPart, header, footer, start = "bottom", className }) {
|
|
3369
|
+
const icons = useIcons();
|
|
2297
3370
|
const scroller = useRef(null);
|
|
2298
3371
|
const content = useRef(null);
|
|
2299
|
-
const stuck = useRef(
|
|
3372
|
+
const stuck = useRef(start === "bottom");
|
|
3373
|
+
const labels = useLabels();
|
|
2300
3374
|
const [atBottom, setAtBottom] = useState(true);
|
|
2301
3375
|
const toBottom = useCallback((behavior = "auto") => {
|
|
2302
3376
|
const el = scroller.current;
|
|
@@ -2322,7 +3396,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2322
3396
|
return () => observer.disconnect();
|
|
2323
3397
|
}, [toBottom]);
|
|
2324
3398
|
const lastUser = messages.findLast((m) => m.role === "user")?.id;
|
|
3399
|
+
const jumpedFor = useRef(start === "top" ? lastUser : null);
|
|
2325
3400
|
useLayoutEffect(() => {
|
|
3401
|
+
if (lastUser === jumpedFor.current) {
|
|
3402
|
+
onScroll();
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
jumpedFor.current = lastUser;
|
|
2326
3406
|
stuck.current = true;
|
|
2327
3407
|
setAtBottom(true);
|
|
2328
3408
|
toBottom();
|
|
@@ -2336,39 +3416,47 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2336
3416
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2337
3417
|
ref: content,
|
|
2338
3418
|
className: "flex min-h-full flex-col",
|
|
2339
|
-
children: [/* @__PURE__ */
|
|
3419
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2340
3420
|
role: "log",
|
|
2341
|
-
"aria-label":
|
|
3421
|
+
"aria-label": labels.conversation,
|
|
2342
3422
|
className: "mx-auto flex w-full max-w-[calc(var(--chat-content-width)+2*var(--chat-gutter,var(--chat-content-padding)))] flex-1 flex-col gap-6 px-[var(--chat-gutter,var(--chat-content-padding))] pt-6 pb-4",
|
|
2343
|
-
children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
3423
|
+
children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
2344
3424
|
message: m,
|
|
2345
|
-
...onEdit && { onEdit: () => onEdit(m.id) }
|
|
3425
|
+
...onEdit && { onEdit: () => onEdit(m.id) },
|
|
3426
|
+
...actions?.extra && { actions: actions.extra(m) },
|
|
3427
|
+
...actions?.copy === false && { copy: false },
|
|
3428
|
+
...actions?.time === false && { time: false }
|
|
2346
3429
|
}, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
|
|
2347
3430
|
message: m,
|
|
2348
3431
|
...onRetry && { onRetry: () => onRetry(m.id) },
|
|
2349
3432
|
...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
|
|
3433
|
+
...onReadAloud && { onReadAloud: () => onReadAloud(m.id) },
|
|
3434
|
+
...actions?.extra && { actions: actions.extra(m) },
|
|
3435
|
+
...actions?.copy === false && { copy: false },
|
|
3436
|
+
...actions?.time === false && { time: false },
|
|
2350
3437
|
...renderPart && { renderPart }
|
|
2351
|
-
}, m.id))
|
|
3438
|
+
}, m.id))]
|
|
2352
3439
|
}), footer && /* @__PURE__ */ jsx("div", {
|
|
2353
3440
|
className: "sticky bottom-0",
|
|
2354
3441
|
children: footer
|
|
2355
3442
|
})]
|
|
2356
3443
|
})
|
|
2357
3444
|
}), !atBottom && /* @__PURE__ */ jsx(IconButton, {
|
|
2358
|
-
label:
|
|
3445
|
+
label: labels.scrollToBottom,
|
|
2359
3446
|
variant: "subtle",
|
|
2360
3447
|
onPress: () => {
|
|
2361
3448
|
stuck.current = true;
|
|
2362
3449
|
toBottom("smooth");
|
|
2363
3450
|
},
|
|
2364
3451
|
className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
|
|
2365
|
-
children:
|
|
3452
|
+
children: icons.scrollToBottom
|
|
2366
3453
|
})]
|
|
2367
3454
|
});
|
|
2368
3455
|
}
|
|
2369
3456
|
// src/components/chat-header.tsx
|
|
2370
3457
|
/** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
|
|
2371
3458
|
function ChatHeader({ title, menu, leading, actions, className }) {
|
|
3459
|
+
const icons = useIcons();
|
|
2372
3460
|
return /* @__PURE__ */ jsxs("header", {
|
|
2373
3461
|
className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
|
|
2374
3462
|
children: [
|
|
@@ -2379,15 +3467,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2379
3467
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2380
3468
|
className: "truncate",
|
|
2381
3469
|
children: title
|
|
2382
|
-
}), /* @__PURE__ */ jsx(
|
|
2383
|
-
"
|
|
2384
|
-
|
|
3470
|
+
}), /* @__PURE__ */ jsx(IconSlot, {
|
|
3471
|
+
className: "text-chat-muted [&_svg]:size-3.5",
|
|
3472
|
+
children: icons.dropdown
|
|
2385
3473
|
})]
|
|
2386
3474
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2387
3475
|
placement: "bottom start",
|
|
2388
3476
|
children: menu
|
|
2389
3477
|
})] }) : /* @__PURE__ */ jsx("h1", {
|
|
2390
|
-
className: "min-w-0 truncate px-2 text-
|
|
3478
|
+
className: "min-w-0 truncate px-2 text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
2391
3479
|
children: title
|
|
2392
3480
|
}),
|
|
2393
3481
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -2399,6 +3487,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2399
3487
|
}
|
|
2400
3488
|
/** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
|
|
2401
3489
|
function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
|
|
3490
|
+
const icons = useIcons();
|
|
3491
|
+
const labels = useLabels();
|
|
2402
3492
|
const act = (key) => {
|
|
2403
3493
|
if (key === "pin") onPin?.();
|
|
2404
3494
|
else if (key === "rename") onRename?.();
|
|
@@ -2406,39 +3496,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2406
3496
|
else if (key === "delete") onDelete?.();
|
|
2407
3497
|
};
|
|
2408
3498
|
return /* @__PURE__ */ jsxs(Menu, {
|
|
2409
|
-
"aria-label":
|
|
3499
|
+
"aria-label": labels.chatActions,
|
|
2410
3500
|
className: "min-w-44",
|
|
2411
3501
|
onAction: act,
|
|
2412
3502
|
children: [
|
|
2413
3503
|
onPin && /* @__PURE__ */ jsx(MenuItem, {
|
|
2414
3504
|
id: "pin",
|
|
2415
|
-
icon: pinned ?
|
|
3505
|
+
icon: pinned ? icons.unpin : icons.pin,
|
|
2416
3506
|
shortcut: "P",
|
|
2417
|
-
children: pinned ?
|
|
3507
|
+
children: pinned ? labels.unpin : labels.pin
|
|
2418
3508
|
}),
|
|
2419
3509
|
onRename && /* @__PURE__ */ jsx(MenuItem, {
|
|
2420
3510
|
id: "rename",
|
|
2421
|
-
icon:
|
|
3511
|
+
icon: icons.rename,
|
|
2422
3512
|
shortcut: "R",
|
|
2423
|
-
children:
|
|
3513
|
+
children: labels.rename
|
|
2424
3514
|
}),
|
|
2425
3515
|
onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2426
3516
|
id: "project",
|
|
2427
|
-
icon:
|
|
2428
|
-
children:
|
|
3517
|
+
icon: icons.addToProject,
|
|
3518
|
+
children: labels.addToProject
|
|
2429
3519
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2430
3520
|
placement: "end top",
|
|
2431
3521
|
className: "w-60",
|
|
2432
3522
|
children: /* @__PURE__ */ jsx(MenuSearch, {
|
|
2433
|
-
placeholder:
|
|
3523
|
+
placeholder: labels.searchProjects,
|
|
2434
3524
|
children: /* @__PURE__ */ jsxs(Menu, {
|
|
2435
|
-
"aria-label":
|
|
3525
|
+
"aria-label": labels.projects,
|
|
2436
3526
|
className: "max-h-72",
|
|
2437
3527
|
onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
|
|
2438
3528
|
children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
|
|
2439
3529
|
id: "__new",
|
|
2440
|
-
icon:
|
|
2441
|
-
children:
|
|
3530
|
+
icon: icons.add,
|
|
3531
|
+
children: labels.newProject
|
|
2442
3532
|
}), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
|
|
2443
3533
|
id: p.id,
|
|
2444
3534
|
children: p.name
|
|
@@ -2448,28 +3538,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2448
3538
|
})] }),
|
|
2449
3539
|
onShare && /* @__PURE__ */ jsx(MenuItem, {
|
|
2450
3540
|
id: "share",
|
|
2451
|
-
icon:
|
|
2452
|
-
children:
|
|
3541
|
+
icon: icons.share,
|
|
3542
|
+
children: labels.shareChat
|
|
2453
3543
|
}),
|
|
2454
3544
|
children,
|
|
2455
3545
|
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
2456
3546
|
id: "delete",
|
|
2457
|
-
icon:
|
|
3547
|
+
icon: icons.delete,
|
|
2458
3548
|
shortcut: "D",
|
|
2459
3549
|
tone: "danger",
|
|
2460
|
-
children:
|
|
3550
|
+
children: labels.delete
|
|
2461
3551
|
})] })
|
|
2462
3552
|
]
|
|
2463
3553
|
});
|
|
2464
3554
|
}
|
|
3555
|
+
// src/components/chat-notice.tsx
|
|
3556
|
+
/** A bordered note in the conversation column: "Shared by Alex. This is a copy of a chat…" with actions under it. */
|
|
3557
|
+
function ChatNotice({ children, actions, icon, className }) {
|
|
3558
|
+
const icons = useIcons();
|
|
3559
|
+
const shown = icon === void 0 ? icons.info : icon;
|
|
3560
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3561
|
+
role: "note",
|
|
3562
|
+
className: clsx("flex gap-3 rounded-chat-lg border border-(--chat-notice-border,var(--chat-border)) bg-(--chat-notice-bg,transparent) px-4 py-3.5 font-chat-sans text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg", className),
|
|
3563
|
+
children: [shown && /* @__PURE__ */ jsx("span", {
|
|
3564
|
+
className: "mt-0.5 shrink-0 text-chat-muted [&_svg]:size-4",
|
|
3565
|
+
children: shown
|
|
3566
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
3567
|
+
className: "min-w-0 flex-1",
|
|
3568
|
+
children: [/* @__PURE__ */ jsx("div", { children }), actions && /* @__PURE__ */ jsx("div", {
|
|
3569
|
+
className: "mt-2.5 flex flex-wrap gap-2",
|
|
3570
|
+
children: actions
|
|
3571
|
+
})]
|
|
3572
|
+
})]
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
2465
3575
|
// src/components/chat-list.tsx
|
|
2466
3576
|
const isProject = (item) => "type" in item && item.type === "project";
|
|
2467
|
-
const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-
|
|
3577
|
+
const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus data-focus-visible:ring-inset", "[@media(pointer:coarse)]:h-10");
|
|
2468
3578
|
/**
|
|
2469
3579
|
* The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
|
|
2470
3580
|
* markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
|
|
2471
3581
|
*/
|
|
2472
3582
|
function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
|
|
3583
|
+
const icons = useIcons();
|
|
3584
|
+
const labels = useLabels();
|
|
2473
3585
|
const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
|
|
2474
3586
|
chat,
|
|
2475
3587
|
active: chat.id === activeId,
|
|
@@ -2483,13 +3595,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2483
3595
|
row
|
|
2484
3596
|
}, item.id) : row(item));
|
|
2485
3597
|
return /* @__PURE__ */ jsx("nav", {
|
|
2486
|
-
"aria-label": props["aria-label"] ??
|
|
3598
|
+
"aria-label": props["aria-label"] ?? labels.chats,
|
|
2487
3599
|
className: clsx("flex flex-col gap-3", className),
|
|
2488
3600
|
children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
|
|
2489
3601
|
defaultExpanded: section.defaultExpanded ?? true,
|
|
2490
3602
|
className: "group/section",
|
|
2491
3603
|
children: [/* @__PURE__ */ jsxs(Heading, {
|
|
2492
|
-
className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-
|
|
3604
|
+
className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 font-normal text-chat-subtle",
|
|
2493
3605
|
children: [
|
|
2494
3606
|
/* @__PURE__ */ jsxs(Button$1, {
|
|
2495
3607
|
slot: "trigger",
|
|
@@ -2497,9 +3609,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2497
3609
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2498
3610
|
className: "truncate",
|
|
2499
3611
|
children: section.title
|
|
2500
|
-
}), /* @__PURE__ */ jsx(
|
|
2501
|
-
"aria-
|
|
2502
|
-
|
|
3612
|
+
}), /* @__PURE__ */ jsx(IconSlot, {
|
|
3613
|
+
className: "opacity-0 transition group-hover/section:opacity-100 group-aria-expanded/trigger:rotate-90 group-data-focus-visible/trigger:opacity-100 [@media(hover:none)]:opacity-100 [&_svg]:size-3",
|
|
3614
|
+
children: icons.expand
|
|
2503
3615
|
})]
|
|
2504
3616
|
}),
|
|
2505
3617
|
section.badge,
|
|
@@ -2514,7 +3626,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2514
3626
|
}), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
|
|
2515
3627
|
onPress: section.onViewAll,
|
|
2516
3628
|
className: clsx(rowClass, "text-chat-muted"),
|
|
2517
|
-
children:
|
|
3629
|
+
children: labels.viewAll
|
|
2518
3630
|
})] })]
|
|
2519
3631
|
}, section.id) : /* @__PURE__ */ jsx("ul", {
|
|
2520
3632
|
className: "flex flex-col gap-px",
|
|
@@ -2523,16 +3635,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2523
3635
|
});
|
|
2524
3636
|
}
|
|
2525
3637
|
function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
3638
|
+
const icons = useIcons();
|
|
2526
3639
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2527
3640
|
const { dismiss } = useSidebar();
|
|
3641
|
+
const labels = useLabels();
|
|
2528
3642
|
const markerClass = "size-2 shrink-0 rounded-full";
|
|
2529
3643
|
const marker = chat.running ? /* @__PURE__ */ jsx("span", {
|
|
2530
3644
|
role: "img",
|
|
2531
|
-
"aria-label":
|
|
3645
|
+
"aria-label": labels.running,
|
|
2532
3646
|
className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
|
|
2533
3647
|
}) : chat.unread ? /* @__PURE__ */ jsx("span", {
|
|
2534
3648
|
role: "img",
|
|
2535
|
-
"aria-label":
|
|
3649
|
+
"aria-label": labels.unread,
|
|
2536
3650
|
className: clsx(markerClass, "bg-chat-check")
|
|
2537
3651
|
}) : null;
|
|
2538
3652
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -2557,11 +3671,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2557
3671
|
isOpen: menuOpen,
|
|
2558
3672
|
onOpenChange: setMenuOpen,
|
|
2559
3673
|
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2560
|
-
label:
|
|
3674
|
+
label: labels.moreOptionsFor(chat.title),
|
|
2561
3675
|
size: "sm",
|
|
2562
3676
|
tooltip: false,
|
|
2563
3677
|
className: clsx("absolute top-1/2 right-1 size-6 -translate-y-1/2 [@media(pointer:coarse)]:size-8", menuOpen || active ? "opacity-100" : "opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100"),
|
|
2564
|
-
children:
|
|
3678
|
+
children: icons.more
|
|
2565
3679
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2566
3680
|
placement: "bottom start",
|
|
2567
3681
|
children: menu
|
|
@@ -2570,6 +3684,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2570
3684
|
});
|
|
2571
3685
|
}
|
|
2572
3686
|
function ProjectRow({ project, activeId, row }) {
|
|
3687
|
+
const icons = useIcons();
|
|
3688
|
+
const labels = useLabels();
|
|
2573
3689
|
const holdsActive = project.chats.some((c) => c.id === activeId);
|
|
2574
3690
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
|
|
2575
3691
|
defaultExpanded: project.defaultExpanded ?? holdsActive,
|
|
@@ -2579,18 +3695,15 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2579
3695
|
slot: "trigger",
|
|
2580
3696
|
className: rowClass,
|
|
2581
3697
|
children: [
|
|
2582
|
-
|
|
2583
|
-
"
|
|
2584
|
-
|
|
2585
|
-
}) : /* @__PURE__ */ jsx(Folder, {
|
|
2586
|
-
"aria-hidden": true,
|
|
2587
|
-
className: "size-4 shrink-0 text-chat-muted"
|
|
3698
|
+
/* @__PURE__ */ jsx(IconSlot, {
|
|
3699
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
3700
|
+
children: isExpanded ? icons.folderOpen : icons.folder
|
|
2588
3701
|
}),
|
|
2589
3702
|
/* @__PURE__ */ jsx("span", {
|
|
2590
3703
|
className: "min-w-0 flex-1 truncate",
|
|
2591
3704
|
children: project.name
|
|
2592
3705
|
}),
|
|
2593
|
-
!isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children:
|
|
3706
|
+
!isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: labels.new })
|
|
2594
3707
|
]
|
|
2595
3708
|
})
|
|
2596
3709
|
}), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
|
|
@@ -2600,7 +3713,11 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2600
3713
|
}) });
|
|
2601
3714
|
}
|
|
2602
3715
|
// src/views/chat-view.tsx
|
|
2603
|
-
/**
|
|
3716
|
+
/**
|
|
3717
|
+
* Column defaults per density; `contentWidth` / `contentPadding` override them. Only the message size changes
|
|
3718
|
+
* with density: UI text is the same in all three. A theme can set the other text-size variables per density
|
|
3719
|
+
* with `[data-chat-density="panel"] { --chat-ui-font-size: … }`.
|
|
3720
|
+
*/
|
|
2604
3721
|
const densityVars = {
|
|
2605
3722
|
full: {},
|
|
2606
3723
|
panel: {
|
|
@@ -2615,14 +3732,20 @@ const densityVars = {
|
|
|
2615
3732
|
}
|
|
2616
3733
|
};
|
|
2617
3734
|
/** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
|
|
2618
|
-
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
|
|
3735
|
+
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
|
|
2619
3736
|
const style = {
|
|
2620
3737
|
...densityVars[density],
|
|
2621
3738
|
...contentWidth && { "--chat-content-width": contentWidth },
|
|
2622
3739
|
...contentPadding && { "--chat-content-padding": contentPadding }
|
|
2623
3740
|
};
|
|
3741
|
+
const host = useMemo(() => ({
|
|
3742
|
+
onPrompt: onSend,
|
|
3743
|
+
onOpenLink: openInNewTab,
|
|
3744
|
+
...widgets
|
|
3745
|
+
}), [onSend, widgets]);
|
|
2624
3746
|
const empty = messages.length === 0;
|
|
2625
3747
|
const [body, short] = useShortPane();
|
|
3748
|
+
const labels = useLabels();
|
|
2626
3749
|
const pending = onApprove && approvals?.[0];
|
|
2627
3750
|
const box = (variant) => /* @__PURE__ */ jsx(Composer, {
|
|
2628
3751
|
variant: density === "inline" || short && !empty ? "compact" : variant,
|
|
@@ -2630,7 +3753,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2630
3753
|
onSubmit: onSend,
|
|
2631
3754
|
...onStop && { onStop },
|
|
2632
3755
|
...composer,
|
|
2633
|
-
...pending && { placeholder:
|
|
3756
|
+
...pending && { placeholder: labels.replyDirectly }
|
|
2634
3757
|
});
|
|
2635
3758
|
return /* @__PURE__ */ jsxs("div", {
|
|
2636
3759
|
"data-chat-density": density,
|
|
@@ -2645,28 +3768,33 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2645
3768
|
className: "w-full max-w-(--chat-content-width)",
|
|
2646
3769
|
children: emptyState ? emptyState(box("centered")) : box("centered")
|
|
2647
3770
|
})
|
|
2648
|
-
}) : /* @__PURE__ */ jsx(
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3771
|
+
}) : /* @__PURE__ */ jsx(WidgetHostProvider, {
|
|
3772
|
+
host,
|
|
3773
|
+
children: /* @__PURE__ */ jsx(MessageList, {
|
|
3774
|
+
messages,
|
|
3775
|
+
...onRetry && { onRetry },
|
|
3776
|
+
...onEdit && { onEdit },
|
|
3777
|
+
...onFeedback && { onFeedback },
|
|
3778
|
+
...onReadAloud && { onReadAloud },
|
|
3779
|
+
...messageActions && { actions: messageActions },
|
|
3780
|
+
...renderPart && { renderPart },
|
|
3781
|
+
footer: /* @__PURE__ */ jsx("div", {
|
|
3782
|
+
className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
|
|
3783
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
3784
|
+
className: "mx-auto w-full max-w-(--chat-content-width)",
|
|
3785
|
+
children: [
|
|
3786
|
+
pending && /* @__PURE__ */ jsx(ApprovalCard, {
|
|
3787
|
+
approval: pending,
|
|
3788
|
+
onRespond: onApprove,
|
|
3789
|
+
className: "mb-2"
|
|
3790
|
+
}, pending.part.id),
|
|
3791
|
+
box("docked"),
|
|
3792
|
+
/* @__PURE__ */ jsx("div", {
|
|
3793
|
+
className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", short ? "min-h-2" : "min-h-6"),
|
|
3794
|
+
children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
|
|
3795
|
+
})
|
|
3796
|
+
]
|
|
3797
|
+
})
|
|
2670
3798
|
})
|
|
2671
3799
|
})
|
|
2672
3800
|
})
|
|
@@ -2703,6 +3831,51 @@ function EmptyState({ greeting, icon, children, footer }) {
|
|
|
2703
3831
|
]
|
|
2704
3832
|
});
|
|
2705
3833
|
}
|
|
3834
|
+
// src/views/chat-transcript.tsx
|
|
3835
|
+
/**
|
|
3836
|
+
* A conversation to read, not to continue: a shared chat, an export, a log. Same messages as ChatView,
|
|
3837
|
+
* with no composer, approvals, retry, edit or feedback. Copy stays.
|
|
3838
|
+
*/
|
|
3839
|
+
function ChatTranscript({ messages, renderPart, widgets, header, notice, footer, start = "top", density = "full", contentWidth, contentPadding, className }) {
|
|
3840
|
+
const style = {
|
|
3841
|
+
...densityVars[density],
|
|
3842
|
+
...contentWidth && { "--chat-content-width": contentWidth },
|
|
3843
|
+
...contentPadding && { "--chat-content-padding": contentPadding }
|
|
3844
|
+
};
|
|
3845
|
+
const host = useMemo(() => {
|
|
3846
|
+
const { allow, readResource } = { ...widgets };
|
|
3847
|
+
const onOpenLink = widgets && "onOpenLink" in widgets ? widgets.onOpenLink : openInNewTab;
|
|
3848
|
+
return {
|
|
3849
|
+
...allow && { allow },
|
|
3850
|
+
...onOpenLink && { onOpenLink },
|
|
3851
|
+
...readResource && { readResource }
|
|
3852
|
+
};
|
|
3853
|
+
}, [widgets]);
|
|
3854
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3855
|
+
"data-chat-density": density,
|
|
3856
|
+
style,
|
|
3857
|
+
className: clsx("chat-view flex h-full min-h-0 flex-col bg-chat-bg text-chat-fg", className),
|
|
3858
|
+
children: [header, /* @__PURE__ */ jsx("div", {
|
|
3859
|
+
className: "chat-view-body flex min-h-0 flex-1 flex-col",
|
|
3860
|
+
children: /* @__PURE__ */ jsx(WidgetHostProvider, {
|
|
3861
|
+
host,
|
|
3862
|
+
children: /* @__PURE__ */ jsx(MessageList, {
|
|
3863
|
+
messages,
|
|
3864
|
+
start,
|
|
3865
|
+
...notice && { header: notice },
|
|
3866
|
+
...renderPart && { renderPart },
|
|
3867
|
+
...footer && { footer: /* @__PURE__ */ jsx("div", {
|
|
3868
|
+
className: "bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter) pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
|
3869
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3870
|
+
className: "mx-auto w-full max-w-(--chat-content-width)",
|
|
3871
|
+
children: footer
|
|
3872
|
+
})
|
|
3873
|
+
}) }
|
|
3874
|
+
})
|
|
3875
|
+
})
|
|
3876
|
+
})]
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
2706
3879
|
// src/views/app-shell.tsx
|
|
2707
3880
|
/**
|
|
2708
3881
|
* A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
|
|
@@ -2727,6 +3900,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2727
3900
|
toggle,
|
|
2728
3901
|
dismiss
|
|
2729
3902
|
]);
|
|
3903
|
+
const labels = useLabels();
|
|
2730
3904
|
const inDrawer = useMemo(() => ({
|
|
2731
3905
|
...pinned,
|
|
2732
3906
|
collapsed: false
|
|
@@ -2745,7 +3919,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2745
3919
|
}), drawer && /* @__PURE__ */ jsx(Drawer, {
|
|
2746
3920
|
isOpen: drawerOpen,
|
|
2747
3921
|
onOpenChange: setDrawerOpen,
|
|
2748
|
-
"aria-label":
|
|
3922
|
+
"aria-label": labels.sidebar,
|
|
2749
3923
|
children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
2750
3924
|
value: inDrawer,
|
|
2751
3925
|
children: sidebar
|
|
@@ -2755,13 +3929,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2755
3929
|
}
|
|
2756
3930
|
/** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
|
|
2757
3931
|
function SidebarToggle({ drawerOnly = false, className }) {
|
|
3932
|
+
const icons = useIcons();
|
|
2758
3933
|
const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
|
|
3934
|
+
const labels = useLabels();
|
|
2759
3935
|
if (drawerOnly && !drawer) return null;
|
|
3936
|
+
const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
|
|
2760
3937
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
2761
|
-
label
|
|
3938
|
+
label,
|
|
2762
3939
|
onPress: toggle,
|
|
2763
3940
|
className,
|
|
2764
|
-
children:
|
|
3941
|
+
children: icons.sidebar
|
|
2765
3942
|
});
|
|
2766
3943
|
}
|
|
2767
3944
|
// src/views/sidebar.tsx
|
|
@@ -2801,7 +3978,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
|
|
|
2801
3978
|
/** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
|
|
2802
3979
|
function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
2803
3980
|
const { collapsed, dismiss } = useSidebar();
|
|
2804
|
-
const className = clsx("group flex h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-
|
|
3981
|
+
const className = clsx("group flex 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)]:h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "[&_svg]:size-4 [&_svg]:shrink-0", collapsed ? "w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", active && "bg-chat-selected");
|
|
2805
3982
|
const press = () => {
|
|
2806
3983
|
onPress?.();
|
|
2807
3984
|
dismiss();
|
|
@@ -2816,7 +3993,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2816
3993
|
children: label
|
|
2817
3994
|
}),
|
|
2818
3995
|
!collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
|
|
2819
|
-
className: "font-chat-sans text-
|
|
3996
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
|
|
2820
3997
|
children: shortcut
|
|
2821
3998
|
})
|
|
2822
3999
|
] });
|
|
@@ -2842,6 +4019,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2842
4019
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
2843
4020
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
2844
4021
|
function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
4022
|
+
const icons = useIcons();
|
|
2845
4023
|
const { collapsed } = useSidebar();
|
|
2846
4024
|
const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
2847
4025
|
src: avatarUrl,
|
|
@@ -2849,7 +4027,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2849
4027
|
className: "size-8 shrink-0 rounded-full object-cover"
|
|
2850
4028
|
}) : /* @__PURE__ */ jsx("span", {
|
|
2851
4029
|
"aria-hidden": true,
|
|
2852
|
-
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-
|
|
4030
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-(length:--chat-ui-font-size-sm) leading-4 font-medium text-chat-bg",
|
|
2853
4031
|
children: initials(name)
|
|
2854
4032
|
});
|
|
2855
4033
|
const button = /* @__PURE__ */ jsxs(Button$1, {
|
|
@@ -2858,15 +4036,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2858
4036
|
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2859
4037
|
className: "flex min-w-0 flex-1 flex-col",
|
|
2860
4038
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2861
|
-
className: "truncate text-
|
|
4039
|
+
className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
2862
4040
|
children: name
|
|
2863
4041
|
}), detail && /* @__PURE__ */ jsx("span", {
|
|
2864
|
-
className: "truncate text-
|
|
4042
|
+
className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
2865
4043
|
children: detail
|
|
2866
4044
|
})]
|
|
2867
|
-
}), menu && /* @__PURE__ */ jsx(
|
|
2868
|
-
"
|
|
2869
|
-
|
|
4045
|
+
}), menu && /* @__PURE__ */ jsx(IconSlot, {
|
|
4046
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
4047
|
+
children: icons.switcher
|
|
2870
4048
|
})] })]
|
|
2871
4049
|
});
|
|
2872
4050
|
if (!menu) return button;
|
|
@@ -2875,4 +4053,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2875
4053
|
children: menu
|
|
2876
4054
|
})] });
|
|
2877
4055
|
}
|
|
2878
|
-
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatTheme, ChatTitleMenu, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UserMessage, activityHeading, activityLabel, commandTool, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, memoryTool, messageText, sseTransport, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, userMessage, webSearchTool };
|
|
4056
|
+
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, 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 };
|