@gntik-ai/chat 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +829 -0
- package/dist/index.js +2597 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -3
- package/src/styles.css +2 -0
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2597 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Avatar, Badge, Button, Collapsible, CollapsiblePanel, CollapsibleTrigger, IconButton, Popover, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger, Radio, RadioGroup, Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectTrigger, SimpleTooltip, Spinner, StatusTag, Textarea, TooltipProvider, cn, fileKind, getInitials, interpolate, useI18n } from "@gntik-ai/ui";
|
|
3
|
+
import { AlertCircle, ArrowDown, ArrowUp, Check, CircleAlert, Copy, ExternalLink, File, FileArchive, FileAudio, FileCode, FileImage, FileSpreadsheet, FileText, FileVideo, Globe, Paperclip, RefreshCw, RotateCcw, Sparkles, Square, ThumbsDown, ThumbsUp, Upload, Wrench, X } from "lucide-react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Children, isValidElement, memo, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
6
|
+
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
|
|
7
|
+
import remarkGfm from "remark-gfm";
|
|
8
|
+
//#region src/utils/format.ts
|
|
9
|
+
/** "840 ms" below a second, "2.4 s" below a minute, "1 m 05 s" above. */
|
|
10
|
+
function formatDuration(ms) {
|
|
11
|
+
if (ms < 1e3) return `${Math.round(ms)} ms`;
|
|
12
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
|
|
13
|
+
const m = Math.floor(ms / 6e4);
|
|
14
|
+
const s = Math.round(ms % 6e4 / 1e3);
|
|
15
|
+
return `${m} m ${String(s).padStart(2, "0")} s`;
|
|
16
|
+
}
|
|
17
|
+
/** "12 KB", "3.4 MB". */
|
|
18
|
+
function formatBytes(bytes) {
|
|
19
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
20
|
+
if (bytes < 1048576) return `${Math.round(bytes / 1024)} KB`;
|
|
21
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
22
|
+
}
|
|
23
|
+
/** Short local time ("14:05") for a message timestamp. */
|
|
24
|
+
function formatTime(date) {
|
|
25
|
+
return date.toLocaleTimeString(void 0, {
|
|
26
|
+
hour: "2-digit",
|
|
27
|
+
minute: "2-digit"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Pretty-prints any value as JSON; falls back to String() for cyclic or exotic values. */
|
|
31
|
+
function toJson(value) {
|
|
32
|
+
if (typeof value === "string") return value;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/utils/i18n.ts
|
|
41
|
+
/**
|
|
42
|
+
* English fallbacks for the chat keys added after the first `@gntik-ai/ui` release. The
|
|
43
|
+
* strings live in the ui catalogs (en + es); these keep the pack readable when it runs
|
|
44
|
+
* against an older `@gntik-ai/ui` whose catalog does not have them yet.
|
|
45
|
+
*/
|
|
46
|
+
const FALLBACK = {
|
|
47
|
+
"chat.typing": "{author} is typing",
|
|
48
|
+
"chat.feedback": "Response feedback",
|
|
49
|
+
"chat.feedbackTitle": "What went wrong?",
|
|
50
|
+
"chat.feedbackReason": "Reason",
|
|
51
|
+
"chat.feedbackComment": "Comment (optional)",
|
|
52
|
+
"chat.feedbackPlaceholder": "Tell us more…",
|
|
53
|
+
"chat.feedbackSubmit": "Send feedback",
|
|
54
|
+
"chat.feedbackThanks": "Thanks for your feedback",
|
|
55
|
+
"chat.feedbackCleared": "Feedback removed",
|
|
56
|
+
"chat.reasonInaccurate": "Inaccurate",
|
|
57
|
+
"chat.reasonUnhelpful": "Not helpful",
|
|
58
|
+
"chat.reasonIncomplete": "Incomplete",
|
|
59
|
+
"chat.reasonUnsafe": "Harmful or unsafe",
|
|
60
|
+
"chat.reasonOther": "Other",
|
|
61
|
+
"chat.regenerate": "Regenerate",
|
|
62
|
+
"chat.uploading": "Uploading {name}",
|
|
63
|
+
"chat.uploadFailed": "Upload failed",
|
|
64
|
+
"chat.retryUpload": "Retry uploading {name}",
|
|
65
|
+
"chat.dropFiles": "Drop files to attach",
|
|
66
|
+
"chat.fileTooLarge": "{name} is larger than {max}",
|
|
67
|
+
"chat.fileTypeNotAllowed": "{name} is not an allowed file type",
|
|
68
|
+
"chat.tooManyFiles": "You can attach up to {max} files",
|
|
69
|
+
"chat.model": "Model",
|
|
70
|
+
"chat.selectModel": "Select a model",
|
|
71
|
+
"chat.capabilityVision": "Vision",
|
|
72
|
+
"chat.capabilityTools": "Tools",
|
|
73
|
+
"chat.capabilityReasoning": "Reasoning",
|
|
74
|
+
"chat.capabilityFiles": "Files",
|
|
75
|
+
"chat.capabilityWeb": "Web search",
|
|
76
|
+
"chat.capabilityAudio": "Audio",
|
|
77
|
+
"chat.contextWindow": "{size} context",
|
|
78
|
+
"chat.unavailable": "Unavailable"
|
|
79
|
+
};
|
|
80
|
+
/** `useI18n()` plus `tc()`, a translator for the newer chat keys with English fallbacks. */
|
|
81
|
+
function useChatI18n() {
|
|
82
|
+
const i18n = useI18n();
|
|
83
|
+
const tc = (key, vars) => {
|
|
84
|
+
const out = i18n.t(key, vars);
|
|
85
|
+
return out === key ? interpolate(FALLBACK[key], vars, i18n.locale) : out;
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
...i18n,
|
|
89
|
+
tc
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/utils/focus.ts
|
|
94
|
+
/** Focus ring shared by every interactive element in the pack (brand rule). */
|
|
95
|
+
const focusRing = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring";
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/Attachment/attachment.variants.ts
|
|
98
|
+
/** Slot classes for AttachmentChip / AttachmentList. */
|
|
99
|
+
const attachmentStyles = {
|
|
100
|
+
list: "flex flex-wrap gap-1.5",
|
|
101
|
+
item: "min-w-0",
|
|
102
|
+
chip: ["relative inline-flex h-10 max-w-64 min-w-0 items-center gap-2 overflow-hidden rounded-md border border-border bg-secondary/50 ps-1.5 pe-1 text-[12px] text-foreground", "data-[status=error]:border-destructive/60 data-[status=error]:bg-destructive/8"].join(" "),
|
|
103
|
+
tile: ["relative flex w-28 flex-col overflow-hidden rounded-lg border border-border bg-card text-[12px] text-foreground", "data-[status=error]:border-destructive/60"].join(" "),
|
|
104
|
+
media: "grid size-7 shrink-0 place-items-center overflow-hidden rounded bg-background text-muted-foreground",
|
|
105
|
+
mediaError: "text-destructive-text",
|
|
106
|
+
tileMedia: "grid aspect-square w-full place-items-center overflow-hidden bg-secondary/50 text-muted-foreground",
|
|
107
|
+
img: "size-full object-cover",
|
|
108
|
+
text: "flex min-w-0 flex-1 flex-col leading-4",
|
|
109
|
+
tileText: "flex min-w-0 flex-col gap-0.5 px-2 py-1.5 leading-4",
|
|
110
|
+
name: "truncate font-medium",
|
|
111
|
+
meta: "truncate font-mono text-[10.5px] text-muted-foreground",
|
|
112
|
+
metaError: "truncate text-[11px] text-destructive-chip-text",
|
|
113
|
+
button: `grid size-6 shrink-0 place-items-center rounded text-muted-foreground hover:bg-foreground/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-50 ${focusRing}`,
|
|
114
|
+
tileActions: "absolute end-1 top-1 flex gap-0.5 rounded-md bg-card/90",
|
|
115
|
+
track: "absolute inset-x-0 bottom-0 h-0.5 bg-border",
|
|
116
|
+
bar: "h-full bg-primary transition-[width] duration-200 motion-reduce:transition-none",
|
|
117
|
+
barIndeterminate: "h-full w-1/3 bg-primary animate-pulse motion-reduce:animate-none"
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/Attachment/AttachmentChip.tsx
|
|
121
|
+
const ICONS = {
|
|
122
|
+
image: FileImage,
|
|
123
|
+
video: FileVideo,
|
|
124
|
+
audio: FileAudio,
|
|
125
|
+
archive: FileArchive,
|
|
126
|
+
code: FileCode,
|
|
127
|
+
spreadsheet: FileSpreadsheet,
|
|
128
|
+
document: FileText,
|
|
129
|
+
file: File
|
|
130
|
+
};
|
|
131
|
+
/** Status of an attachment from its progress and error fields. */
|
|
132
|
+
function attachmentStatus(a) {
|
|
133
|
+
if (a.error) return "error";
|
|
134
|
+
if (a.progress === null || typeof a.progress === "number" && a.progress < 100) return "uploading";
|
|
135
|
+
return "idle";
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One attached file: image thumbnail or file-type icon, name, size, upload progress
|
|
139
|
+
* (a progressbar), error with retry, and a remove button.
|
|
140
|
+
*/
|
|
141
|
+
function AttachmentChip({ attachment: a, variant = "chip", onRemove, onRetry, disabled = false, className, ref }) {
|
|
142
|
+
const { t, tc } = useChatI18n();
|
|
143
|
+
const status = attachmentStatus(a);
|
|
144
|
+
const kind = fileKind(a.name, a.type);
|
|
145
|
+
const Icon = status === "error" ? AlertCircle : ICONS[kind];
|
|
146
|
+
const showImage = a.previewUrl !== void 0 && kind === "image" && status !== "error";
|
|
147
|
+
const tile = variant === "tile";
|
|
148
|
+
const pct = typeof a.progress === "number" ? Math.max(0, Math.min(100, Math.round(a.progress))) : null;
|
|
149
|
+
const media = /* @__PURE__ */ jsx("span", {
|
|
150
|
+
className: cn(tile ? attachmentStyles.tileMedia : attachmentStyles.media, status === "error" && attachmentStyles.mediaError),
|
|
151
|
+
children: showImage ? /* @__PURE__ */ jsx("img", {
|
|
152
|
+
src: a.previewUrl,
|
|
153
|
+
alt: "",
|
|
154
|
+
className: attachmentStyles.img
|
|
155
|
+
}) : /* @__PURE__ */ jsx(Icon, {
|
|
156
|
+
size: tile ? 22 : 14,
|
|
157
|
+
"aria-hidden": true
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
const meta = status === "error" ? /* @__PURE__ */ jsx("span", {
|
|
161
|
+
className: attachmentStyles.metaError,
|
|
162
|
+
children: typeof a.error === "string" ? a.error : tc("chat.uploadFailed")
|
|
163
|
+
}) : status === "uploading" ? /* @__PURE__ */ jsx("span", {
|
|
164
|
+
className: attachmentStyles.meta,
|
|
165
|
+
children: pct === null ? t("common.loading") : `${pct}%`
|
|
166
|
+
}) : a.size !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
167
|
+
className: attachmentStyles.meta,
|
|
168
|
+
children: formatBytes(a.size)
|
|
169
|
+
}) : null;
|
|
170
|
+
const buttons = /* @__PURE__ */ jsxs(Fragment, { children: [status === "error" && onRetry && /* @__PURE__ */ jsx("button", {
|
|
171
|
+
type: "button",
|
|
172
|
+
className: attachmentStyles.button,
|
|
173
|
+
"aria-label": tc("chat.retryUpload", { name: a.name }),
|
|
174
|
+
disabled,
|
|
175
|
+
onClick: () => onRetry(a.id),
|
|
176
|
+
children: /* @__PURE__ */ jsx(RotateCcw, {
|
|
177
|
+
size: 12,
|
|
178
|
+
"aria-hidden": true
|
|
179
|
+
})
|
|
180
|
+
}), onRemove && /* @__PURE__ */ jsx("button", {
|
|
181
|
+
type: "button",
|
|
182
|
+
className: attachmentStyles.button,
|
|
183
|
+
"aria-label": t("common.removeItem", { label: a.name }),
|
|
184
|
+
disabled,
|
|
185
|
+
onClick: () => onRemove(a.id),
|
|
186
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
187
|
+
size: 12,
|
|
188
|
+
"aria-hidden": true
|
|
189
|
+
})
|
|
190
|
+
})] });
|
|
191
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
192
|
+
ref,
|
|
193
|
+
"data-status": status,
|
|
194
|
+
"data-kind": kind,
|
|
195
|
+
className: cn(tile ? attachmentStyles.tile : attachmentStyles.chip, className),
|
|
196
|
+
children: [
|
|
197
|
+
media,
|
|
198
|
+
/* @__PURE__ */ jsxs("span", {
|
|
199
|
+
className: tile ? attachmentStyles.tileText : attachmentStyles.text,
|
|
200
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
201
|
+
className: attachmentStyles.name,
|
|
202
|
+
title: a.name,
|
|
203
|
+
children: a.name
|
|
204
|
+
}), meta]
|
|
205
|
+
}),
|
|
206
|
+
tile ? (onRemove || status === "error" && onRetry) && /* @__PURE__ */ jsx("span", {
|
|
207
|
+
className: attachmentStyles.tileActions,
|
|
208
|
+
children: buttons
|
|
209
|
+
}) : buttons,
|
|
210
|
+
status === "uploading" && /* @__PURE__ */ jsx("span", {
|
|
211
|
+
role: "progressbar",
|
|
212
|
+
"aria-label": tc("chat.uploading", { name: a.name }),
|
|
213
|
+
"aria-valuemin": 0,
|
|
214
|
+
"aria-valuemax": 100,
|
|
215
|
+
"aria-valuenow": pct ?? void 0,
|
|
216
|
+
className: attachmentStyles.track,
|
|
217
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
218
|
+
className: pct === null ? attachmentStyles.barIndeterminate : attachmentStyles.bar,
|
|
219
|
+
style: pct === null ? void 0 : { width: `${pct}%` }
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
]
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/Attachment/AttachmentList.tsx
|
|
227
|
+
/** A labelled list of AttachmentChips. Renders nothing when empty. */
|
|
228
|
+
function AttachmentList({ attachments, variant = "chip", onRemove, onRetry, disabled, label, className, ref }) {
|
|
229
|
+
const { t } = useChatI18n();
|
|
230
|
+
if (attachments.length === 0) return null;
|
|
231
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
232
|
+
ref,
|
|
233
|
+
"aria-label": label ?? t("chat.attachments"),
|
|
234
|
+
className: cn(attachmentStyles.list, className),
|
|
235
|
+
children: attachments.map((a) => /* @__PURE__ */ jsx("li", {
|
|
236
|
+
className: attachmentStyles.item,
|
|
237
|
+
children: /* @__PURE__ */ jsx(AttachmentChip, {
|
|
238
|
+
attachment: a,
|
|
239
|
+
variant,
|
|
240
|
+
onRemove,
|
|
241
|
+
onRetry,
|
|
242
|
+
disabled
|
|
243
|
+
})
|
|
244
|
+
}, a.id))
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/Attachment/validateFiles.ts
|
|
249
|
+
/** Whether a file matches an `accept` list (extensions, exact MIME types and `type/*`). */
|
|
250
|
+
function matchesAccept(file, accept) {
|
|
251
|
+
if (!accept?.trim()) return true;
|
|
252
|
+
const name = file.name.toLowerCase();
|
|
253
|
+
const type = file.type.toLowerCase();
|
|
254
|
+
return accept.split(",").map((a) => a.trim().toLowerCase()).filter(Boolean).some((rule) => {
|
|
255
|
+
if (rule.startsWith(".")) return name.endsWith(rule);
|
|
256
|
+
if (rule.endsWith("/*")) return type.startsWith(rule.slice(0, -1));
|
|
257
|
+
return type === rule;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Splits files into accepted ones and rejections (without messages) by type, size and count. */
|
|
261
|
+
function validateFiles(files, { accept, maxSize, maxFiles, current = 0 }) {
|
|
262
|
+
const accepted = [];
|
|
263
|
+
const rejected = [];
|
|
264
|
+
for (const file of files) if (!matchesAccept(file, accept)) rejected.push({
|
|
265
|
+
file,
|
|
266
|
+
reason: "type"
|
|
267
|
+
});
|
|
268
|
+
else if (maxSize !== void 0 && file.size > maxSize) rejected.push({
|
|
269
|
+
file,
|
|
270
|
+
reason: "size"
|
|
271
|
+
});
|
|
272
|
+
else if (maxFiles !== void 0 && current + accepted.length >= maxFiles) rejected.push({
|
|
273
|
+
file,
|
|
274
|
+
reason: "count"
|
|
275
|
+
});
|
|
276
|
+
else accepted.push(file);
|
|
277
|
+
return {
|
|
278
|
+
accepted,
|
|
279
|
+
rejected
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/Attachment/Attachment.doc.ts
|
|
284
|
+
const doc = {
|
|
285
|
+
name: "AttachmentList",
|
|
286
|
+
group: "Display",
|
|
287
|
+
status: "beta",
|
|
288
|
+
description: "Files attached to a message or waiting in the composer. Each AttachmentChip shows an image thumbnail or a file-type icon, the name, the size, upload progress (a labelled progressbar; indeterminate holds still under reduced motion), an error with Retry, and a Remove button. `variant=\"tile\"` gives larger image previews inside a message. `validateFiles` / `matchesAccept` back the composer’s accept and max-size checks.",
|
|
289
|
+
pattern: "list",
|
|
290
|
+
keyboard: [["Tab", "Moves through each chip’s Retry (on error) and Remove buttons"], ["Enter / Space", "Activates the focused Retry or Remove button"]],
|
|
291
|
+
tokens: [
|
|
292
|
+
"secondary",
|
|
293
|
+
"card",
|
|
294
|
+
"border",
|
|
295
|
+
"muted-foreground",
|
|
296
|
+
"primary",
|
|
297
|
+
"destructive",
|
|
298
|
+
"destructive-text",
|
|
299
|
+
"focus-ring"
|
|
300
|
+
]
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/ChatComposer/chatComposer.variants.ts
|
|
304
|
+
/** Slot classes for ChatComposer (focus ring on the frame, like the legacy composer). */
|
|
305
|
+
const chatComposerStyles = {
|
|
306
|
+
root: [
|
|
307
|
+
"relative overflow-hidden rounded-xl border border-border bg-card shadow-sm transition-colors motion-reduce:transition-none",
|
|
308
|
+
"focus-within:border-primary/60 focus-within:ring-2 focus-within:ring-ring/25",
|
|
309
|
+
"data-[disabled]:opacity-60 data-[dragging]:border-primary/60"
|
|
310
|
+
].join(" "),
|
|
311
|
+
dropOverlay: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center gap-2 rounded-xl border-2 border-dashed border-primary/60 bg-card/95 text-[13px] font-medium text-primary-text",
|
|
312
|
+
errors: "mx-3 mt-3 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/8 px-2.5 py-1.5 text-[12px] text-destructive-chip-text",
|
|
313
|
+
errorList: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
314
|
+
errorDismiss: `-me-1 grid size-5 shrink-0 place-items-center rounded text-destructive-chip-text hover:bg-foreground/10 ${focusRing}`,
|
|
315
|
+
attachments: "flex flex-wrap gap-1.5 px-3 pt-3",
|
|
316
|
+
/** @deprecated Chips render AttachmentChip (see attachmentStyles). */
|
|
317
|
+
chip: "inline-flex h-7 max-w-60 items-center gap-1.5 rounded-md border border-border bg-secondary/50 ps-2 pe-1 text-[12px] text-foreground",
|
|
318
|
+
chipName: "truncate font-medium",
|
|
319
|
+
chipSize: "shrink-0 font-mono text-[10.5px] text-muted-foreground",
|
|
320
|
+
chipRemove: `grid size-5 shrink-0 place-items-center rounded text-muted-foreground hover:bg-foreground/10 hover:text-foreground ${focusRing}`,
|
|
321
|
+
textarea: ["block max-h-[220px] min-h-[52px] w-full resize-none bg-transparent px-3.5 py-3 text-[14px] leading-6 text-foreground", "field-sizing-content placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed"].join(" "),
|
|
322
|
+
toolbar: "flex items-center justify-between gap-2 px-2 pb-2",
|
|
323
|
+
tools: "flex min-w-0 items-center gap-1",
|
|
324
|
+
end: "flex shrink-0 items-center gap-2",
|
|
325
|
+
count: "font-mono text-[11px] text-muted-foreground tabular-nums",
|
|
326
|
+
countOver: "text-destructive-text",
|
|
327
|
+
hint: "mt-1.5 px-1 text-center text-[11.5px] text-muted-foreground"
|
|
328
|
+
};
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/ChatComposer/ChatComposer.tsx
|
|
331
|
+
/**
|
|
332
|
+
* Message box for a chat: grows with its content, Enter sends and Shift+Enter adds a line,
|
|
333
|
+
* files (picked, pasted or dropped, validated against accept / maxSize / maxFiles) show as
|
|
334
|
+
* removable chips with previews and upload progress, a model picker can sit in the toolbar, and while a reply
|
|
335
|
+
* streams the send button becomes Stop (Escape also stops).
|
|
336
|
+
*/
|
|
337
|
+
function ChatComposer({ value, defaultValue = "", onValueChange, onSubmit, streaming = false, onStop, disabled = false, placeholder: placeholderProp, label: labelProp, maxLength, attachments = [], onAttach, onRemoveAttachment, onRetryAttachment, accept, maxSize, maxFiles, onReject, modelPicker, tools, hint, className, ref }) {
|
|
338
|
+
const { t, tc } = useChatI18n();
|
|
339
|
+
const placeholder = placeholderProp ?? t("chat.placeholder");
|
|
340
|
+
const label = labelProp ?? t("chat.messageLabel");
|
|
341
|
+
const id = useId();
|
|
342
|
+
const [inner, setInner] = useState(defaultValue);
|
|
343
|
+
const text = value ?? inner;
|
|
344
|
+
const fileInput = useRef(null);
|
|
345
|
+
const dragDepth = useRef(0);
|
|
346
|
+
const [dragging, setDragging] = useState(false);
|
|
347
|
+
const [errors, setErrors] = useState([]);
|
|
348
|
+
const canAttach = onAttach !== void 0 && !disabled;
|
|
349
|
+
const setText = (next) => {
|
|
350
|
+
if (value === void 0) setInner(next);
|
|
351
|
+
onValueChange?.(next);
|
|
352
|
+
};
|
|
353
|
+
const length = text.length;
|
|
354
|
+
const over = maxLength !== void 0 && length > maxLength;
|
|
355
|
+
const showCount = maxLength !== void 0 && length >= maxLength * .8;
|
|
356
|
+
const canSend = !disabled && !streaming && !over && (text.trim().length > 0 || attachments.length > 0);
|
|
357
|
+
const send = () => {
|
|
358
|
+
if (!canSend) return;
|
|
359
|
+
onSubmit(text.trim(), attachments);
|
|
360
|
+
if (value === void 0) setInner("");
|
|
361
|
+
};
|
|
362
|
+
const onKeyDown = (e) => {
|
|
363
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
send();
|
|
366
|
+
} else if (e.key === "Escape" && streaming && onStop) {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
onStop();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const addFiles = (files) => {
|
|
372
|
+
if (!onAttach || files.length === 0) return;
|
|
373
|
+
const { accepted, rejected } = validateFiles(files, {
|
|
374
|
+
accept,
|
|
375
|
+
maxSize,
|
|
376
|
+
maxFiles,
|
|
377
|
+
current: attachments.length
|
|
378
|
+
});
|
|
379
|
+
const rejections = rejected.map(({ file, reason }) => ({
|
|
380
|
+
file,
|
|
381
|
+
reason,
|
|
382
|
+
message: reason === "type" ? tc("chat.fileTypeNotAllowed", { name: file.name }) : reason === "size" ? tc("chat.fileTooLarge", {
|
|
383
|
+
name: file.name,
|
|
384
|
+
max: formatBytes(maxSize ?? 0)
|
|
385
|
+
}) : tc("chat.tooManyFiles", { max: maxFiles ?? 0 })
|
|
386
|
+
}));
|
|
387
|
+
setErrors([...new Set(rejections.map((r) => r.message))]);
|
|
388
|
+
if (rejections.length) onReject?.(rejections);
|
|
389
|
+
if (accepted.length) onAttach(accepted);
|
|
390
|
+
};
|
|
391
|
+
const onFiles = (e) => {
|
|
392
|
+
addFiles(Array.from(e.target.files ?? []));
|
|
393
|
+
e.target.value = "";
|
|
394
|
+
};
|
|
395
|
+
const onPaste = (e) => {
|
|
396
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
397
|
+
if (!canAttach || files.length === 0) return;
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
addFiles(files);
|
|
400
|
+
};
|
|
401
|
+
const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
402
|
+
const dropHandlers = canAttach ? {
|
|
403
|
+
onDragEnter: (e) => {
|
|
404
|
+
if (!hasFiles(e)) return;
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
dragDepth.current += 1;
|
|
407
|
+
setDragging(true);
|
|
408
|
+
},
|
|
409
|
+
onDragOver: (e) => {
|
|
410
|
+
if (!hasFiles(e)) return;
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
413
|
+
},
|
|
414
|
+
onDragLeave: () => {
|
|
415
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
416
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
417
|
+
},
|
|
418
|
+
onDrop: (e) => {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
dragDepth.current = 0;
|
|
421
|
+
setDragging(false);
|
|
422
|
+
addFiles(Array.from(e.dataTransfer?.files ?? []));
|
|
423
|
+
}
|
|
424
|
+
} : {};
|
|
425
|
+
const hintNode = hint === void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
426
|
+
/* @__PURE__ */ jsx("kbd", {
|
|
427
|
+
className: "font-sans",
|
|
428
|
+
children: t("kbd.enter")
|
|
429
|
+
}),
|
|
430
|
+
" ",
|
|
431
|
+
t("chat.hintSend"),
|
|
432
|
+
" ·",
|
|
433
|
+
" ",
|
|
434
|
+
/* @__PURE__ */ jsx("kbd", {
|
|
435
|
+
className: "font-sans",
|
|
436
|
+
children: t("chat.shiftEnter")
|
|
437
|
+
}),
|
|
438
|
+
" ",
|
|
439
|
+
t("chat.hintNewLine")
|
|
440
|
+
] }) : hint;
|
|
441
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
442
|
+
className,
|
|
443
|
+
children: [/* @__PURE__ */ jsxs("form", {
|
|
444
|
+
className: chatComposerStyles.root,
|
|
445
|
+
"data-disabled": disabled || void 0,
|
|
446
|
+
"data-dragging": dragging || void 0,
|
|
447
|
+
...dropHandlers,
|
|
448
|
+
onSubmit: (e) => {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
send();
|
|
451
|
+
},
|
|
452
|
+
children: [
|
|
453
|
+
dragging && /* @__PURE__ */ jsxs("div", {
|
|
454
|
+
"aria-hidden": true,
|
|
455
|
+
className: chatComposerStyles.dropOverlay,
|
|
456
|
+
children: [/* @__PURE__ */ jsx(Upload, { size: 16 }), tc("chat.dropFiles")]
|
|
457
|
+
}),
|
|
458
|
+
/* @__PURE__ */ jsx(AttachmentList, {
|
|
459
|
+
attachments,
|
|
460
|
+
onRemove: onRemoveAttachment,
|
|
461
|
+
onRetry: onRetryAttachment,
|
|
462
|
+
disabled,
|
|
463
|
+
className: chatComposerStyles.attachments
|
|
464
|
+
}),
|
|
465
|
+
/* @__PURE__ */ jsx("div", {
|
|
466
|
+
role: "alert",
|
|
467
|
+
className: cn(errors.length > 0 && chatComposerStyles.errors),
|
|
468
|
+
children: errors.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
469
|
+
className: chatComposerStyles.errorList,
|
|
470
|
+
children: errors.map((m) => /* @__PURE__ */ jsx("li", { children: m }, m))
|
|
471
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
472
|
+
type: "button",
|
|
473
|
+
className: chatComposerStyles.errorDismiss,
|
|
474
|
+
"aria-label": t("common.dismiss"),
|
|
475
|
+
onClick: () => setErrors([]),
|
|
476
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
477
|
+
size: 12,
|
|
478
|
+
"aria-hidden": true
|
|
479
|
+
})
|
|
480
|
+
})] })
|
|
481
|
+
}),
|
|
482
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
483
|
+
ref,
|
|
484
|
+
id: `${id}-input`,
|
|
485
|
+
rows: 1,
|
|
486
|
+
value: text,
|
|
487
|
+
onChange: (e) => setText(e.target.value),
|
|
488
|
+
onKeyDown,
|
|
489
|
+
onPaste,
|
|
490
|
+
disabled,
|
|
491
|
+
placeholder,
|
|
492
|
+
"aria-label": label,
|
|
493
|
+
"aria-invalid": over || void 0,
|
|
494
|
+
"aria-describedby": showCount ? `${id}-count` : void 0,
|
|
495
|
+
className: chatComposerStyles.textarea
|
|
496
|
+
}),
|
|
497
|
+
/* @__PURE__ */ jsxs("div", {
|
|
498
|
+
className: chatComposerStyles.toolbar,
|
|
499
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
500
|
+
className: chatComposerStyles.tools,
|
|
501
|
+
children: [onAttach && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
502
|
+
size: "sm",
|
|
503
|
+
icon: Paperclip,
|
|
504
|
+
label: t("chat.attach"),
|
|
505
|
+
disabled,
|
|
506
|
+
onClick: () => fileInput.current?.click()
|
|
507
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
508
|
+
ref: fileInput,
|
|
509
|
+
type: "file",
|
|
510
|
+
multiple: true,
|
|
511
|
+
hidden: true,
|
|
512
|
+
accept,
|
|
513
|
+
"aria-label": t("chat.attach"),
|
|
514
|
+
onChange: onFiles
|
|
515
|
+
})] }), tools]
|
|
516
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
517
|
+
className: chatComposerStyles.end,
|
|
518
|
+
children: [
|
|
519
|
+
showCount && /* @__PURE__ */ jsxs("span", {
|
|
520
|
+
id: `${id}-count`,
|
|
521
|
+
className: cn(chatComposerStyles.count, over && chatComposerStyles.countOver),
|
|
522
|
+
children: [
|
|
523
|
+
length,
|
|
524
|
+
"/",
|
|
525
|
+
maxLength,
|
|
526
|
+
over && /* @__PURE__ */ jsxs("span", {
|
|
527
|
+
className: "sr-only",
|
|
528
|
+
children: [" ", t("chat.overLimit")]
|
|
529
|
+
})
|
|
530
|
+
]
|
|
531
|
+
}),
|
|
532
|
+
modelPicker,
|
|
533
|
+
streaming ? /* @__PURE__ */ jsx(Button, {
|
|
534
|
+
size: "sm",
|
|
535
|
+
variant: "secondary",
|
|
536
|
+
icon: Square,
|
|
537
|
+
"aria-label": t("chat.stop"),
|
|
538
|
+
onClick: onStop,
|
|
539
|
+
disabled: !onStop,
|
|
540
|
+
children: t("common.stop")
|
|
541
|
+
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
542
|
+
size: "sm",
|
|
543
|
+
variant: "primary",
|
|
544
|
+
icon: ArrowUp,
|
|
545
|
+
label: t("chat.send"),
|
|
546
|
+
type: "submit",
|
|
547
|
+
disabled: !canSend
|
|
548
|
+
})
|
|
549
|
+
]
|
|
550
|
+
})]
|
|
551
|
+
})
|
|
552
|
+
]
|
|
553
|
+
}), hintNode !== null && /* @__PURE__ */ jsx("p", {
|
|
554
|
+
className: chatComposerStyles.hint,
|
|
555
|
+
children: hintNode
|
|
556
|
+
})]
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/ChatComposer/ChatComposer.doc.ts
|
|
561
|
+
const doc$1 = {
|
|
562
|
+
name: "ChatComposer",
|
|
563
|
+
group: "Forms",
|
|
564
|
+
status: "beta",
|
|
565
|
+
description: "Chat input: autosizing textarea, Enter sends and Shift+Enter adds a line, attachments (pick, paste or drag-and-drop files; `accept`, `maxSize` and `maxFiles` validation with messages in an alert; chips with image thumbnails, file-type icons, size, upload progress, error + Retry and Remove), a model-picker slot (e.g. a compact ModelPicker), a character counter near the limit, a disabled state, and a Stop button while a reply streams.",
|
|
566
|
+
keyboard: [
|
|
567
|
+
["Enter", "Sends the message (ignored while streaming or during IME composition)"],
|
|
568
|
+
["Shift+Enter", "Inserts a new line"],
|
|
569
|
+
["Escape", "While a reply streams: stops it"],
|
|
570
|
+
["Tab", "Moves through attachment Retry/Remove buttons, the text box, attach, tools, the model picker and Send/Stop"],
|
|
571
|
+
["Ctrl/⌘+V", "In the text box: pasted files are attached (validated) instead of inserted"]
|
|
572
|
+
],
|
|
573
|
+
tokens: [
|
|
574
|
+
"card",
|
|
575
|
+
"destructive-chip-text",
|
|
576
|
+
"primary-text",
|
|
577
|
+
"border",
|
|
578
|
+
"primary",
|
|
579
|
+
"ring",
|
|
580
|
+
"secondary",
|
|
581
|
+
"muted-foreground",
|
|
582
|
+
"destructive-text",
|
|
583
|
+
"focus-ring"
|
|
584
|
+
]
|
|
585
|
+
};
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/TypingIndicator/typingIndicator.variants.ts
|
|
588
|
+
/** Slot classes for TypingIndicator. Dots pulse in sequence; static under reduced motion. */
|
|
589
|
+
const typingIndicatorStyles = {
|
|
590
|
+
root: "inline-flex items-center gap-2 text-muted-foreground",
|
|
591
|
+
dots: "inline-flex h-6 items-center gap-1",
|
|
592
|
+
dot: "size-1.5 rounded-full bg-muted-foreground animate-pulse motion-reduce:animate-none",
|
|
593
|
+
label: "text-[12.5px]",
|
|
594
|
+
size: {
|
|
595
|
+
sm: {
|
|
596
|
+
dots: "h-4 gap-0.5",
|
|
597
|
+
dot: "size-1"
|
|
598
|
+
},
|
|
599
|
+
md: {
|
|
600
|
+
dots: "",
|
|
601
|
+
dot: ""
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
/** Thin blinking caret after streamed plain text (static under reduced motion). */
|
|
606
|
+
const streamingCaret = "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[2px] bg-primary animate-pulse motion-reduce:animate-none";
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/TypingIndicator/TypingIndicator.tsx
|
|
609
|
+
/**
|
|
610
|
+
* Three dots that pulse while a reply is being written. The dots are decorative; the text
|
|
611
|
+
* ("Assistant is typing") is always in the accessibility tree. Under reduced motion the dots
|
|
612
|
+
* hold still.
|
|
613
|
+
*/
|
|
614
|
+
function TypingIndicator({ author, showLabel = false, label, size = "md", announce = false, className, ref }) {
|
|
615
|
+
const { t, tc } = useChatI18n();
|
|
616
|
+
const text = label ?? tc("chat.typing", { author: author ?? t("chat.assistant") });
|
|
617
|
+
const sz = typingIndicatorStyles.size[size];
|
|
618
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
619
|
+
ref,
|
|
620
|
+
"data-slot": "typing-indicator",
|
|
621
|
+
role: announce ? "status" : void 0,
|
|
622
|
+
className: cn(typingIndicatorStyles.root, className),
|
|
623
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
624
|
+
"aria-hidden": true,
|
|
625
|
+
className: cn(typingIndicatorStyles.dots, sz.dots),
|
|
626
|
+
children: [
|
|
627
|
+
/* @__PURE__ */ jsx("span", { className: cn(typingIndicatorStyles.dot, sz.dot) }),
|
|
628
|
+
/* @__PURE__ */ jsx("span", { className: cn(typingIndicatorStyles.dot, sz.dot, "[animation-delay:150ms]") }),
|
|
629
|
+
/* @__PURE__ */ jsx("span", { className: cn(typingIndicatorStyles.dot, sz.dot, "[animation-delay:300ms]") })
|
|
630
|
+
]
|
|
631
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
632
|
+
className: showLabel ? typingIndicatorStyles.label : "sr-only",
|
|
633
|
+
children: text
|
|
634
|
+
})]
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/TypingIndicator/TypingIndicator.doc.ts
|
|
639
|
+
const doc$11 = {
|
|
640
|
+
name: "TypingIndicator",
|
|
641
|
+
group: "Feedback",
|
|
642
|
+
status: "beta",
|
|
643
|
+
description: "Three pulsing dots shown while a reply is being written, with the text \"Assistant is typing\" for screen readers (or visible with `showLabel`). The dots are decorative and hold still under reduced motion. ChatMessage renders it while a streaming turn is still empty.",
|
|
644
|
+
keyboard: [],
|
|
645
|
+
tokens: ["muted-foreground"]
|
|
646
|
+
};
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/utils/useCopy.ts
|
|
649
|
+
/**
|
|
650
|
+
* Copies text to the clipboard and exposes a short-lived `copied` flag for the button label.
|
|
651
|
+
* The reset timer is cleared on unmount and on every new copy.
|
|
652
|
+
*/
|
|
653
|
+
function useCopy(resetMs = 1600) {
|
|
654
|
+
const [copied, setCopied] = useState(false);
|
|
655
|
+
const timer = useRef(void 0);
|
|
656
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
657
|
+
return {
|
|
658
|
+
copied,
|
|
659
|
+
copy: useCallback(async (text) => {
|
|
660
|
+
try {
|
|
661
|
+
await navigator.clipboard?.writeText(text);
|
|
662
|
+
} catch {}
|
|
663
|
+
setCopied(true);
|
|
664
|
+
clearTimeout(timer.current);
|
|
665
|
+
timer.current = setTimeout(() => setCopied(false), resetMs);
|
|
666
|
+
}, [resetMs])
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/ChatMessage/chatMessage.variants.ts
|
|
671
|
+
/** Slot classes for ChatMessage, per role. */
|
|
672
|
+
const chatMessageStyles = {
|
|
673
|
+
root: "group/message relative flex gap-3 outline-none",
|
|
674
|
+
avatar: "mt-0.5 shrink-0",
|
|
675
|
+
body: "flex min-w-0 flex-1 flex-col gap-1",
|
|
676
|
+
meta: "flex items-baseline gap-2 text-[12px] leading-5",
|
|
677
|
+
author: "font-semibold text-foreground",
|
|
678
|
+
time: "font-mono text-[11px] text-muted-foreground",
|
|
679
|
+
content: "min-w-0 text-[14px] leading-[1.7] text-foreground",
|
|
680
|
+
actions: ["flex items-center gap-0.5 transition-opacity motion-reduce:transition-none", "opacity-0 group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100"].join(" "),
|
|
681
|
+
actionsVisible: "opacity-100",
|
|
682
|
+
/** @deprecated The empty streaming state renders TypingIndicator (see typingIndicatorStyles). */
|
|
683
|
+
typing: "inline-flex h-6 items-center gap-1",
|
|
684
|
+
/** @deprecated See typingIndicatorStyles.dot. */
|
|
685
|
+
typingDot: "size-1.5 rounded-full bg-muted-foreground animate-pulse motion-reduce:animate-none",
|
|
686
|
+
role: {
|
|
687
|
+
user: {
|
|
688
|
+
root: "flex-row-reverse",
|
|
689
|
+
body: "items-end",
|
|
690
|
+
meta: "flex-row-reverse",
|
|
691
|
+
content: "max-w-[85%] rounded-2xl rounded-se-md bg-secondary px-4 py-2.5 whitespace-pre-wrap [overflow-wrap:anywhere]"
|
|
692
|
+
},
|
|
693
|
+
assistant: {
|
|
694
|
+
root: "",
|
|
695
|
+
body: "",
|
|
696
|
+
meta: "",
|
|
697
|
+
content: ""
|
|
698
|
+
},
|
|
699
|
+
participant: {
|
|
700
|
+
root: "",
|
|
701
|
+
body: "items-start",
|
|
702
|
+
meta: "",
|
|
703
|
+
content: "max-w-[85%] rounded-2xl rounded-ss-md border border-border bg-card px-4 py-2.5 whitespace-pre-wrap [overflow-wrap:anywhere]"
|
|
704
|
+
},
|
|
705
|
+
system: {
|
|
706
|
+
root: "justify-center",
|
|
707
|
+
body: "items-center",
|
|
708
|
+
meta: "sr-only",
|
|
709
|
+
content: "rounded-full border border-border bg-card px-3 py-1 text-center text-[12px] leading-5 text-muted-foreground"
|
|
710
|
+
},
|
|
711
|
+
tool: {
|
|
712
|
+
root: "",
|
|
713
|
+
body: "",
|
|
714
|
+
meta: "",
|
|
715
|
+
content: "text-[13px]"
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region src/ChatMessage/ChatMessage.tsx
|
|
721
|
+
const DEFAULT_AUTHOR = {
|
|
722
|
+
user: "chat.you",
|
|
723
|
+
assistant: "chat.assistant",
|
|
724
|
+
participant: "chat.participant",
|
|
725
|
+
system: "chat.system",
|
|
726
|
+
tool: "chat.tool"
|
|
727
|
+
};
|
|
728
|
+
function DefaultAvatar({ role, author, src }) {
|
|
729
|
+
if (role === "assistant") return /* @__PURE__ */ jsx(Avatar, {
|
|
730
|
+
size: "sm",
|
|
731
|
+
tone: "primary",
|
|
732
|
+
fallback: /* @__PURE__ */ jsx(Sparkles, {
|
|
733
|
+
size: 15,
|
|
734
|
+
"aria-hidden": true
|
|
735
|
+
})
|
|
736
|
+
});
|
|
737
|
+
if (role === "tool") return /* @__PURE__ */ jsx(Avatar, {
|
|
738
|
+
size: "sm",
|
|
739
|
+
tone: "secondary",
|
|
740
|
+
fallback: /* @__PURE__ */ jsx(Wrench, {
|
|
741
|
+
size: 14,
|
|
742
|
+
"aria-hidden": true
|
|
743
|
+
})
|
|
744
|
+
});
|
|
745
|
+
return /* @__PURE__ */ jsx(Avatar, {
|
|
746
|
+
size: "sm",
|
|
747
|
+
tone: role === "participant" ? "accent" : "secondary",
|
|
748
|
+
src,
|
|
749
|
+
initials: getInitials(author)
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* One turn in a conversation. User turns are right-aligned bubbles, participant turns (another
|
|
754
|
+
* person) are start-aligned outlined bubbles, assistant turns are full-width prose, system notes are centred pills and tool turns hold a ToolCallCard. Copy,
|
|
755
|
+
* retry and feedback actions appear on hover or keyboard focus and are reachable with Tab.
|
|
756
|
+
*/
|
|
757
|
+
function ChatMessage({ role, author, avatar, avatarSrc, timestamp, children, streaming = false, showCaret = true, copyText, onRetry, feedback = null, onFeedback, actions, alwaysShowActions = false, className, ref }) {
|
|
758
|
+
const id = useId();
|
|
759
|
+
const { t } = useI18n();
|
|
760
|
+
const name = author ?? t(DEFAULT_AUTHOR[role]);
|
|
761
|
+
const v = chatMessageStyles.role[role];
|
|
762
|
+
const { copied, copy } = useCopy();
|
|
763
|
+
const isEmpty = children === void 0 || children === null || children === "";
|
|
764
|
+
const time = timestamp instanceof Date ? /* @__PURE__ */ jsx("time", {
|
|
765
|
+
dateTime: timestamp.toISOString(),
|
|
766
|
+
className: chatMessageStyles.time,
|
|
767
|
+
children: formatTime(timestamp)
|
|
768
|
+
}) : timestamp ? /* @__PURE__ */ jsx("span", {
|
|
769
|
+
className: chatMessageStyles.time,
|
|
770
|
+
children: timestamp
|
|
771
|
+
}) : null;
|
|
772
|
+
const hasActions = !streaming && role !== "system" && (copyText !== void 0 || onRetry || onFeedback || actions);
|
|
773
|
+
const showAvatar = role !== "system" && avatar !== null;
|
|
774
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
775
|
+
ref,
|
|
776
|
+
"aria-labelledby": `${id}-author`,
|
|
777
|
+
"aria-busy": streaming || void 0,
|
|
778
|
+
"data-role": role,
|
|
779
|
+
"data-streaming": streaming || void 0,
|
|
780
|
+
className: cn(chatMessageStyles.root, v.root, className),
|
|
781
|
+
children: [showAvatar && /* @__PURE__ */ jsx("div", {
|
|
782
|
+
className: chatMessageStyles.avatar,
|
|
783
|
+
children: avatar ?? /* @__PURE__ */ jsx(DefaultAvatar, {
|
|
784
|
+
role,
|
|
785
|
+
author: name,
|
|
786
|
+
src: avatarSrc
|
|
787
|
+
})
|
|
788
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
789
|
+
className: cn(chatMessageStyles.body, v.body),
|
|
790
|
+
children: [
|
|
791
|
+
/* @__PURE__ */ jsxs("div", {
|
|
792
|
+
className: cn(chatMessageStyles.meta, v.meta),
|
|
793
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
794
|
+
id: `${id}-author`,
|
|
795
|
+
className: chatMessageStyles.author,
|
|
796
|
+
children: name
|
|
797
|
+
}), time]
|
|
798
|
+
}),
|
|
799
|
+
/* @__PURE__ */ jsx("div", {
|
|
800
|
+
className: cn(chatMessageStyles.content, v.content),
|
|
801
|
+
children: streaming && isEmpty ? /* @__PURE__ */ jsx(TypingIndicator, { author: name }) : streaming && showCaret && (typeof children === "string" || typeof children === "number") ? /* @__PURE__ */ jsxs(Fragment, { children: [children, /* @__PURE__ */ jsx("span", {
|
|
802
|
+
"aria-hidden": true,
|
|
803
|
+
"data-slot": "streaming-caret",
|
|
804
|
+
className: streamingCaret
|
|
805
|
+
})] }) : children
|
|
806
|
+
}),
|
|
807
|
+
hasActions && /* @__PURE__ */ jsxs("div", {
|
|
808
|
+
role: "group",
|
|
809
|
+
"aria-label": t("chat.actionsFor", { name }),
|
|
810
|
+
className: cn(chatMessageStyles.actions, alwaysShowActions && chatMessageStyles.actionsVisible),
|
|
811
|
+
children: [
|
|
812
|
+
copyText !== void 0 && /* @__PURE__ */ jsx(SimpleTooltip, {
|
|
813
|
+
content: copied ? t("common.copied") : t("common.copy"),
|
|
814
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
815
|
+
size: "sm",
|
|
816
|
+
icon: copied ? Check : Copy,
|
|
817
|
+
label: copied ? t("common.copied") : t("chat.copyMessage"),
|
|
818
|
+
onClick: () => void copy(copyText)
|
|
819
|
+
})
|
|
820
|
+
}),
|
|
821
|
+
onRetry && /* @__PURE__ */ jsx(SimpleTooltip, {
|
|
822
|
+
content: t("common.retry"),
|
|
823
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
824
|
+
size: "sm",
|
|
825
|
+
icon: RotateCcw,
|
|
826
|
+
label: t("common.retry"),
|
|
827
|
+
onClick: onRetry
|
|
828
|
+
})
|
|
829
|
+
}),
|
|
830
|
+
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SimpleTooltip, {
|
|
831
|
+
content: t("chat.goodResponse"),
|
|
832
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
833
|
+
size: "sm",
|
|
834
|
+
icon: ThumbsUp,
|
|
835
|
+
label: t("chat.goodResponse"),
|
|
836
|
+
"aria-pressed": feedback === "up",
|
|
837
|
+
className: cn(feedback === "up" && "text-primary-text"),
|
|
838
|
+
onClick: () => onFeedback(feedback === "up" ? null : "up")
|
|
839
|
+
})
|
|
840
|
+
}), /* @__PURE__ */ jsx(SimpleTooltip, {
|
|
841
|
+
content: t("chat.badResponse"),
|
|
842
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
843
|
+
size: "sm",
|
|
844
|
+
icon: ThumbsDown,
|
|
845
|
+
label: t("chat.badResponse"),
|
|
846
|
+
"aria-pressed": feedback === "down",
|
|
847
|
+
className: cn(feedback === "down" && "text-destructive-text"),
|
|
848
|
+
onClick: () => onFeedback(feedback === "down" ? null : "down")
|
|
849
|
+
})
|
|
850
|
+
})] }),
|
|
851
|
+
actions
|
|
852
|
+
]
|
|
853
|
+
})
|
|
854
|
+
]
|
|
855
|
+
})]
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/ChatMessage/ChatAnnouncer.tsx
|
|
860
|
+
/**
|
|
861
|
+
* Screen-reader strategy for streaming: the thread itself is not a live region (it would
|
|
862
|
+
* re-announce every token). This single polite, atomic status says "Assistant is responding…"
|
|
863
|
+
* when a stream starts and reads the finished reply once when it ends.
|
|
864
|
+
*/
|
|
865
|
+
function ChatAnnouncer({ streaming, lastReply, author, maxLength = 600 }) {
|
|
866
|
+
const { t } = useI18n();
|
|
867
|
+
let text = "";
|
|
868
|
+
if (streaming) text = t("chat.responding", { author: author ?? t("chat.assistant") });
|
|
869
|
+
else if (lastReply) text = lastReply.length > maxLength ? `${lastReply.slice(0, maxLength)}…` : lastReply;
|
|
870
|
+
return /* @__PURE__ */ jsx("div", {
|
|
871
|
+
role: "status",
|
|
872
|
+
"aria-live": "polite",
|
|
873
|
+
"aria-atomic": "true",
|
|
874
|
+
className: "sr-only",
|
|
875
|
+
children: text
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/ChatMessage/ChatMessage.doc.ts
|
|
880
|
+
const doc$4 = {
|
|
881
|
+
name: "ChatMessage",
|
|
882
|
+
group: "Display",
|
|
883
|
+
status: "beta",
|
|
884
|
+
description: "One conversation turn for the roles user (end-aligned bubble), participant (another person, e.g. a teammate: start-aligned outlined bubble with their name and avatar via `author` / `avatarSrc`), assistant, system and tool: avatar, author, timestamp, body and hover/focus actions (copy, retry, thumbs feedback). While streaming it is aria-busy, hides its actions and shows a TypingIndicator (\"Assistant is typing\") until text arrives and a caret after streamed plain text; ChatAnnouncer announces start and end once instead of every token.",
|
|
885
|
+
pattern: "article",
|
|
886
|
+
keyboard: [
|
|
887
|
+
["Tab", "Reaches each action in order (copy, retry, good, bad); focusing one reveals the action group"],
|
|
888
|
+
["Enter", "Activates the focused action"],
|
|
889
|
+
["Space", "Activates the focused action; thumbs toggle (aria-pressed)"]
|
|
890
|
+
],
|
|
891
|
+
tokens: [
|
|
892
|
+
"foreground",
|
|
893
|
+
"muted-foreground",
|
|
894
|
+
"secondary",
|
|
895
|
+
"card",
|
|
896
|
+
"border",
|
|
897
|
+
"primary",
|
|
898
|
+
"primary-text",
|
|
899
|
+
"destructive-text",
|
|
900
|
+
"focus-ring"
|
|
901
|
+
]
|
|
902
|
+
};
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/MessageFeedback/messageFeedback.variants.ts
|
|
905
|
+
/** Slot classes for MessageFeedback. */
|
|
906
|
+
const messageFeedbackStyles = {
|
|
907
|
+
root: "inline-flex items-center gap-0.5",
|
|
908
|
+
up: "text-primary-text",
|
|
909
|
+
down: "text-destructive-text",
|
|
910
|
+
popup: "w-80 max-w-[calc(100vw-2rem)]",
|
|
911
|
+
form: "flex flex-col gap-3",
|
|
912
|
+
title: "text-[13px] font-semibold text-foreground",
|
|
913
|
+
label: "mb-1.5 block text-[12.5px] font-medium text-foreground",
|
|
914
|
+
footer: "flex justify-end gap-2"
|
|
915
|
+
};
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/MessageFeedback/MessageFeedback.tsx
|
|
918
|
+
const DEFAULT_REASONS = [
|
|
919
|
+
{
|
|
920
|
+
value: "inaccurate",
|
|
921
|
+
key: "chat.reasonInaccurate"
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
value: "unhelpful",
|
|
925
|
+
key: "chat.reasonUnhelpful"
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
value: "incomplete",
|
|
929
|
+
key: "chat.reasonIncomplete"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
value: "unsafe",
|
|
933
|
+
key: "chat.reasonUnsafe"
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
value: "other",
|
|
937
|
+
key: "chat.reasonOther"
|
|
938
|
+
}
|
|
939
|
+
];
|
|
940
|
+
const EMPTY = { rating: null };
|
|
941
|
+
/**
|
|
942
|
+
* Feedback for an assistant reply: thumbs up / down toggles (aria-pressed), and on thumbs down
|
|
943
|
+
* a popover asking for a reason and an optional comment. Copy and Regenerate sit alongside.
|
|
944
|
+
*/
|
|
945
|
+
function MessageFeedback({ value: valueProp, onFeedback, reasons: reasonsProp, askDetails = true, copyText, onRegenerate, label, disabled = false, className, ref }) {
|
|
946
|
+
const { t, tc } = useChatI18n();
|
|
947
|
+
const id = useId();
|
|
948
|
+
const [inner, setInner] = useState(EMPTY);
|
|
949
|
+
const value = valueProp ?? inner;
|
|
950
|
+
const [open, setOpen] = useState(false);
|
|
951
|
+
const [reason, setReason] = useState("");
|
|
952
|
+
const [comment, setComment] = useState("");
|
|
953
|
+
const [announcement, setAnnouncement] = useState("");
|
|
954
|
+
const { copied, copy } = useCopy();
|
|
955
|
+
const reasons = reasonsProp ?? DEFAULT_REASONS.map((r) => ({
|
|
956
|
+
value: r.value,
|
|
957
|
+
label: tc(r.key)
|
|
958
|
+
}));
|
|
959
|
+
const emit = (next) => {
|
|
960
|
+
if (valueProp === void 0) setInner(next);
|
|
961
|
+
onFeedback?.(next);
|
|
962
|
+
};
|
|
963
|
+
const pressUp = () => {
|
|
964
|
+
setOpen(false);
|
|
965
|
+
const next = value.rating === "up" ? EMPTY : { rating: "up" };
|
|
966
|
+
emit(next);
|
|
967
|
+
setAnnouncement(next.rating ? tc("chat.feedbackThanks") : tc("chat.feedbackCleared"));
|
|
968
|
+
};
|
|
969
|
+
const pressDown = () => {
|
|
970
|
+
if (value.rating === "down") {
|
|
971
|
+
setOpen(false);
|
|
972
|
+
emit(EMPTY);
|
|
973
|
+
setAnnouncement(tc("chat.feedbackCleared"));
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
emit({ rating: "down" });
|
|
977
|
+
setReason("");
|
|
978
|
+
setComment("");
|
|
979
|
+
if (askDetails) setOpen(true);
|
|
980
|
+
else setAnnouncement(tc("chat.feedbackThanks"));
|
|
981
|
+
};
|
|
982
|
+
const submit = (e) => {
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
const trimmed = comment.trim();
|
|
985
|
+
emit({
|
|
986
|
+
rating: "down",
|
|
987
|
+
...reason ? { reason } : {},
|
|
988
|
+
...trimmed ? { comment: trimmed } : {}
|
|
989
|
+
});
|
|
990
|
+
setOpen(false);
|
|
991
|
+
setAnnouncement(tc("chat.feedbackThanks"));
|
|
992
|
+
};
|
|
993
|
+
const upLabel = t("chat.goodResponse");
|
|
994
|
+
const downLabel = t("chat.badResponse");
|
|
995
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
996
|
+
ref,
|
|
997
|
+
role: "group",
|
|
998
|
+
"aria-label": label ?? tc("chat.feedback"),
|
|
999
|
+
className: cn(messageFeedbackStyles.root, className),
|
|
1000
|
+
children: [
|
|
1001
|
+
copyText !== void 0 && /* @__PURE__ */ jsx(SimpleTooltip, {
|
|
1002
|
+
content: copied ? t("common.copied") : t("common.copy"),
|
|
1003
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1004
|
+
size: "sm",
|
|
1005
|
+
icon: copied ? Check : Copy,
|
|
1006
|
+
label: copied ? t("common.copied") : t("chat.copyMessage"),
|
|
1007
|
+
disabled,
|
|
1008
|
+
onClick: () => void copy(copyText)
|
|
1009
|
+
})
|
|
1010
|
+
}),
|
|
1011
|
+
onRegenerate && /* @__PURE__ */ jsx(SimpleTooltip, {
|
|
1012
|
+
content: tc("chat.regenerate"),
|
|
1013
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1014
|
+
size: "sm",
|
|
1015
|
+
icon: RefreshCw,
|
|
1016
|
+
label: tc("chat.regenerate"),
|
|
1017
|
+
disabled,
|
|
1018
|
+
onClick: onRegenerate
|
|
1019
|
+
})
|
|
1020
|
+
}),
|
|
1021
|
+
/* @__PURE__ */ jsx(SimpleTooltip, {
|
|
1022
|
+
content: upLabel,
|
|
1023
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1024
|
+
size: "sm",
|
|
1025
|
+
icon: ThumbsUp,
|
|
1026
|
+
label: upLabel,
|
|
1027
|
+
"aria-pressed": value.rating === "up",
|
|
1028
|
+
disabled,
|
|
1029
|
+
className: cn(value.rating === "up" && messageFeedbackStyles.up),
|
|
1030
|
+
onClick: pressUp
|
|
1031
|
+
})
|
|
1032
|
+
}),
|
|
1033
|
+
/* @__PURE__ */ jsxs(Popover, {
|
|
1034
|
+
open,
|
|
1035
|
+
onOpenChange: (next) => {
|
|
1036
|
+
if (!next) setOpen(false);
|
|
1037
|
+
},
|
|
1038
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
1039
|
+
render: /* @__PURE__ */ jsx(IconButton, {
|
|
1040
|
+
size: "sm",
|
|
1041
|
+
icon: ThumbsDown,
|
|
1042
|
+
label: downLabel,
|
|
1043
|
+
"aria-pressed": value.rating === "down",
|
|
1044
|
+
disabled,
|
|
1045
|
+
className: cn(value.rating === "down" && messageFeedbackStyles.down)
|
|
1046
|
+
}),
|
|
1047
|
+
onClick: pressDown
|
|
1048
|
+
}), /* @__PURE__ */ jsx(PopoverContent, {
|
|
1049
|
+
side: "top",
|
|
1050
|
+
align: "end",
|
|
1051
|
+
className: messageFeedbackStyles.popup,
|
|
1052
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
1053
|
+
className: messageFeedbackStyles.form,
|
|
1054
|
+
onSubmit: submit,
|
|
1055
|
+
children: [
|
|
1056
|
+
/* @__PURE__ */ jsx(PopoverTitle, {
|
|
1057
|
+
id: `${id}-title`,
|
|
1058
|
+
className: messageFeedbackStyles.title,
|
|
1059
|
+
children: tc("chat.feedbackTitle")
|
|
1060
|
+
}),
|
|
1061
|
+
reasons.length > 0 && /* @__PURE__ */ jsx(RadioGroup, {
|
|
1062
|
+
"aria-labelledby": `${id}-title`,
|
|
1063
|
+
value: reason,
|
|
1064
|
+
onValueChange: (v) => setReason(String(v)),
|
|
1065
|
+
children: reasons.map((r) => /* @__PURE__ */ jsx(Radio, {
|
|
1066
|
+
value: r.value,
|
|
1067
|
+
label: r.label
|
|
1068
|
+
}, r.value))
|
|
1069
|
+
}),
|
|
1070
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
1071
|
+
htmlFor: `${id}-comment`,
|
|
1072
|
+
className: messageFeedbackStyles.label,
|
|
1073
|
+
children: tc("chat.feedbackComment")
|
|
1074
|
+
}), /* @__PURE__ */ jsx(Textarea, {
|
|
1075
|
+
id: `${id}-comment`,
|
|
1076
|
+
rows: 3,
|
|
1077
|
+
value: comment,
|
|
1078
|
+
onValueChange: setComment,
|
|
1079
|
+
placeholder: tc("chat.feedbackPlaceholder")
|
|
1080
|
+
})] }),
|
|
1081
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1082
|
+
className: messageFeedbackStyles.footer,
|
|
1083
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1084
|
+
type: "button",
|
|
1085
|
+
size: "sm",
|
|
1086
|
+
variant: "ghost",
|
|
1087
|
+
onClick: () => setOpen(false),
|
|
1088
|
+
children: t("common.cancel")
|
|
1089
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1090
|
+
type: "submit",
|
|
1091
|
+
size: "sm",
|
|
1092
|
+
children: tc("chat.feedbackSubmit")
|
|
1093
|
+
})]
|
|
1094
|
+
})
|
|
1095
|
+
]
|
|
1096
|
+
})
|
|
1097
|
+
})]
|
|
1098
|
+
}),
|
|
1099
|
+
/* @__PURE__ */ jsx("span", {
|
|
1100
|
+
role: "status",
|
|
1101
|
+
"aria-live": "polite",
|
|
1102
|
+
className: "sr-only",
|
|
1103
|
+
children: announcement
|
|
1104
|
+
})
|
|
1105
|
+
]
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region src/MessageFeedback/MessageFeedback.doc.ts
|
|
1110
|
+
const doc$7 = {
|
|
1111
|
+
name: "MessageFeedback",
|
|
1112
|
+
group: "Actions",
|
|
1113
|
+
status: "beta",
|
|
1114
|
+
description: "Actions for an assistant reply: copy, regenerate and thumbs up / down toggles (aria-pressed). Thumbs down opens a popover with a reason picker and an optional comment. Controlled with `value` + `onFeedback`; pressing the active thumb clears it. A polite status confirms the feedback.",
|
|
1115
|
+
primitive: "Popover",
|
|
1116
|
+
pattern: "toggle button + dialog",
|
|
1117
|
+
keyboard: [
|
|
1118
|
+
["Tab", "Moves through copy, regenerate, good and bad"],
|
|
1119
|
+
["Enter / Space", "Toggles the focused thumb (aria-pressed); a second press clears it"],
|
|
1120
|
+
["Enter / Space (on bad)", "Also opens the details popover; focus moves into it"],
|
|
1121
|
+
["Arrow keys", "Inside the popover: move between reasons"],
|
|
1122
|
+
["Escape", "Closes the popover (the rating stays) and returns focus to the thumb"]
|
|
1123
|
+
],
|
|
1124
|
+
tokens: [
|
|
1125
|
+
"muted-foreground",
|
|
1126
|
+
"primary-text",
|
|
1127
|
+
"destructive-text",
|
|
1128
|
+
"popover",
|
|
1129
|
+
"border",
|
|
1130
|
+
"focus-ring"
|
|
1131
|
+
]
|
|
1132
|
+
};
|
|
1133
|
+
//#endregion
|
|
1134
|
+
//#region src/ModelPicker/modelPicker.variants.ts
|
|
1135
|
+
/** Slot classes for ModelPicker. */
|
|
1136
|
+
const modelPickerStyles = {
|
|
1137
|
+
trigger: "w-auto max-w-full min-w-0",
|
|
1138
|
+
compactTrigger: "h-7 w-auto max-w-56 min-w-0 gap-1 border-transparent bg-transparent ps-2 pe-1.5 shadow-none hover:bg-secondary/70 hover:border-transparent",
|
|
1139
|
+
triggerValue: "flex min-w-0 items-center gap-1.5",
|
|
1140
|
+
triggerIcon: "shrink-0 text-muted-foreground",
|
|
1141
|
+
triggerName: "truncate font-medium",
|
|
1142
|
+
triggerProvider: "truncate text-muted-foreground",
|
|
1143
|
+
popup: "w-80 max-w-[calc(100vw-2rem)]",
|
|
1144
|
+
item: "items-start",
|
|
1145
|
+
itemBody: "flex min-w-0 flex-col gap-1 py-0.5 whitespace-normal",
|
|
1146
|
+
itemName: "font-medium text-foreground",
|
|
1147
|
+
itemDescription: "text-[11.5px] leading-4 text-muted-foreground",
|
|
1148
|
+
badges: "flex flex-wrap gap-1",
|
|
1149
|
+
reason: "text-[11.5px] leading-4 text-muted-foreground italic"
|
|
1150
|
+
};
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/ModelPicker/ModelPicker.tsx
|
|
1153
|
+
const CAPABILITY_KEY = {
|
|
1154
|
+
vision: "chat.capabilityVision",
|
|
1155
|
+
tools: "chat.capabilityTools",
|
|
1156
|
+
reasoning: "chat.capabilityReasoning",
|
|
1157
|
+
files: "chat.capabilityFiles",
|
|
1158
|
+
web: "chat.capabilityWeb",
|
|
1159
|
+
audio: "chat.capabilityAudio"
|
|
1160
|
+
};
|
|
1161
|
+
/** Models grouped by provider, keeping first-seen order. */
|
|
1162
|
+
function groupModels(models) {
|
|
1163
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const m of models) {
|
|
1165
|
+
const list = groups.get(m.provider);
|
|
1166
|
+
if (list) list.push(m);
|
|
1167
|
+
else groups.set(m.provider, [m]);
|
|
1168
|
+
}
|
|
1169
|
+
return [...groups.entries()].map(([provider, items]) => ({
|
|
1170
|
+
provider,
|
|
1171
|
+
items
|
|
1172
|
+
}));
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Model / provider select for a chat: options grouped by provider with capability badges
|
|
1176
|
+
* (vision, tools, context size…) and disabled models that say why. `compact` renders a small
|
|
1177
|
+
* borderless trigger for the composer toolbar. Built on Select (Base UI).
|
|
1178
|
+
*/
|
|
1179
|
+
function ModelPicker({ models, value, onValueChange, compact = false, label, placeholder, disabled, hideProvider = false, className, ref }) {
|
|
1180
|
+
const { tc, formatNumber } = useChatI18n();
|
|
1181
|
+
const byId = new Map(models.map((m) => [m.id, m]));
|
|
1182
|
+
const items = models.map((m) => ({
|
|
1183
|
+
value: m.id,
|
|
1184
|
+
label: m.name
|
|
1185
|
+
}));
|
|
1186
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
1187
|
+
items,
|
|
1188
|
+
value,
|
|
1189
|
+
disabled,
|
|
1190
|
+
onValueChange: (next) => {
|
|
1191
|
+
if (next !== null) onValueChange(next);
|
|
1192
|
+
},
|
|
1193
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1194
|
+
ref,
|
|
1195
|
+
size: "sm",
|
|
1196
|
+
"aria-label": label ?? tc("chat.model"),
|
|
1197
|
+
placeholder: placeholder ?? tc("chat.selectModel"),
|
|
1198
|
+
className: cn(compact ? modelPickerStyles.compactTrigger : modelPickerStyles.trigger, className),
|
|
1199
|
+
children: (id) => {
|
|
1200
|
+
const m = id === null ? void 0 : byId.get(id);
|
|
1201
|
+
if (!m) return placeholder ?? tc("chat.selectModel");
|
|
1202
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1203
|
+
className: modelPickerStyles.triggerValue,
|
|
1204
|
+
children: [
|
|
1205
|
+
/* @__PURE__ */ jsx(Sparkles, {
|
|
1206
|
+
size: 13,
|
|
1207
|
+
"aria-hidden": true,
|
|
1208
|
+
className: modelPickerStyles.triggerIcon
|
|
1209
|
+
}),
|
|
1210
|
+
/* @__PURE__ */ jsx("span", {
|
|
1211
|
+
className: modelPickerStyles.triggerName,
|
|
1212
|
+
children: m.name
|
|
1213
|
+
}),
|
|
1214
|
+
!compact && !hideProvider && /* @__PURE__ */ jsxs("span", {
|
|
1215
|
+
className: modelPickerStyles.triggerProvider,
|
|
1216
|
+
children: ["· ", m.provider]
|
|
1217
|
+
})
|
|
1218
|
+
]
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
1222
|
+
className: modelPickerStyles.popup,
|
|
1223
|
+
align: compact ? "end" : "start",
|
|
1224
|
+
children: groupModels(models).map((g) => /* @__PURE__ */ jsxs(SelectGroup, { children: [/* @__PURE__ */ jsx(SelectGroupLabel, { children: g.provider }), g.items.map((m) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1225
|
+
value: m.id,
|
|
1226
|
+
disabled: m.disabled,
|
|
1227
|
+
label: m.name,
|
|
1228
|
+
className: modelPickerStyles.item,
|
|
1229
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
1230
|
+
className: modelPickerStyles.itemBody,
|
|
1231
|
+
children: [
|
|
1232
|
+
/* @__PURE__ */ jsx("span", {
|
|
1233
|
+
className: modelPickerStyles.itemName,
|
|
1234
|
+
children: m.name
|
|
1235
|
+
}),
|
|
1236
|
+
m.description && /* @__PURE__ */ jsx("span", {
|
|
1237
|
+
className: modelPickerStyles.itemDescription,
|
|
1238
|
+
children: m.description
|
|
1239
|
+
}),
|
|
1240
|
+
(m.capabilities?.length || m.contextWindow) && /* @__PURE__ */ jsxs("span", {
|
|
1241
|
+
className: modelPickerStyles.badges,
|
|
1242
|
+
children: [m.capabilities?.map((c) => /* @__PURE__ */ jsx(Badge, {
|
|
1243
|
+
size: "sm",
|
|
1244
|
+
tone: "neutral",
|
|
1245
|
+
children: tc(CAPABILITY_KEY[c])
|
|
1246
|
+
}, c)), m.contextWindow !== void 0 && /* @__PURE__ */ jsx(Badge, {
|
|
1247
|
+
size: "sm",
|
|
1248
|
+
tone: "neutral",
|
|
1249
|
+
variant: "outline",
|
|
1250
|
+
children: tc("chat.contextWindow", { size: formatNumber(m.contextWindow, {
|
|
1251
|
+
notation: "compact",
|
|
1252
|
+
maximumFractionDigits: 1
|
|
1253
|
+
}) })
|
|
1254
|
+
})]
|
|
1255
|
+
}),
|
|
1256
|
+
m.disabled && /* @__PURE__ */ jsx("span", {
|
|
1257
|
+
className: modelPickerStyles.reason,
|
|
1258
|
+
children: m.disabledReason ?? tc("chat.unavailable")
|
|
1259
|
+
})
|
|
1260
|
+
]
|
|
1261
|
+
})
|
|
1262
|
+
}, m.id))] }, g.provider))
|
|
1263
|
+
})]
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/ModelPicker/fixtures.ts
|
|
1268
|
+
/** Generic, product-agnostic models for examples and the demo. */
|
|
1269
|
+
const DEMO_MODELS = [
|
|
1270
|
+
{
|
|
1271
|
+
id: "swift",
|
|
1272
|
+
name: "Swift",
|
|
1273
|
+
provider: "Hosted",
|
|
1274
|
+
description: "Fast answers for everyday questions.",
|
|
1275
|
+
capabilities: ["tools"],
|
|
1276
|
+
contextWindow: 128e3
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
id: "balanced",
|
|
1280
|
+
name: "Balanced",
|
|
1281
|
+
provider: "Hosted",
|
|
1282
|
+
description: "Good default for analysis and writing.",
|
|
1283
|
+
capabilities: [
|
|
1284
|
+
"vision",
|
|
1285
|
+
"tools",
|
|
1286
|
+
"files"
|
|
1287
|
+
],
|
|
1288
|
+
contextWindow: 2e5
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
id: "deep",
|
|
1292
|
+
name: "Deep reasoning",
|
|
1293
|
+
provider: "Hosted",
|
|
1294
|
+
description: "Plans multi-step work; slower.",
|
|
1295
|
+
capabilities: [
|
|
1296
|
+
"vision",
|
|
1297
|
+
"tools",
|
|
1298
|
+
"reasoning"
|
|
1299
|
+
],
|
|
1300
|
+
contextWindow: 1e6,
|
|
1301
|
+
disabled: true,
|
|
1302
|
+
disabledReason: "Needs an upgraded plan"
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
id: "local-small",
|
|
1306
|
+
name: "Local small",
|
|
1307
|
+
provider: "Self-hosted",
|
|
1308
|
+
description: "Runs in your own cluster.",
|
|
1309
|
+
contextWindow: 32e3
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
id: "local-vision",
|
|
1313
|
+
name: "Local vision",
|
|
1314
|
+
provider: "Self-hosted",
|
|
1315
|
+
capabilities: ["vision"],
|
|
1316
|
+
contextWindow: 64e3
|
|
1317
|
+
}
|
|
1318
|
+
];
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/ModelPicker/ModelPicker.doc.ts
|
|
1321
|
+
const doc$8 = {
|
|
1322
|
+
name: "ModelPicker",
|
|
1323
|
+
group: "Forms",
|
|
1324
|
+
status: "beta",
|
|
1325
|
+
description: "Choose the model (and provider) for a conversation: options grouped by provider with a description, capability badges (vision, tools, reasoning, files, web, audio) and the context size; unavailable models stay listed, disabled, with the reason. `compact` gives a small borderless trigger for the ChatComposer toolbar. Controlled (`value` + `onValueChange`).",
|
|
1326
|
+
primitive: "Select",
|
|
1327
|
+
pattern: "select-only combobox",
|
|
1328
|
+
keyboard: [
|
|
1329
|
+
["Enter / Space / ArrowDown", "On the trigger: opens the list"],
|
|
1330
|
+
["ArrowDown / ArrowUp", "Moves between models; a disabled one can be highlighted (its reason is read) but not selected"],
|
|
1331
|
+
["Type a character", "Jumps to the model whose name starts with it"],
|
|
1332
|
+
["Enter", "Selects the highlighted model and closes"],
|
|
1333
|
+
["Escape", "Closes without changing; focus returns to the trigger"]
|
|
1334
|
+
],
|
|
1335
|
+
tokens: [
|
|
1336
|
+
"background",
|
|
1337
|
+
"popover",
|
|
1338
|
+
"border",
|
|
1339
|
+
"secondary",
|
|
1340
|
+
"primary",
|
|
1341
|
+
"primary-text",
|
|
1342
|
+
"muted-foreground",
|
|
1343
|
+
"focus-ring"
|
|
1344
|
+
]
|
|
1345
|
+
};
|
|
1346
|
+
//#endregion
|
|
1347
|
+
//#region src/useChatScroll/useChatScroll.ts
|
|
1348
|
+
/** Imperative DOM write kept outside the hook (state values must not be mutated in place). */
|
|
1349
|
+
function pinToBottom(el) {
|
|
1350
|
+
el.scrollTop = el.scrollHeight;
|
|
1351
|
+
}
|
|
1352
|
+
function distanceFromBottom(el) {
|
|
1353
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Auto-scroll for a chat thread that respects the reader: new content keeps the view pinned
|
|
1357
|
+
* to the bottom only while the user is already there. Scrolling up unpins it (and
|
|
1358
|
+
* `isAtBottom` turns false, so a "scroll to latest" button can show); `scrollToBottom()`
|
|
1359
|
+
* pins it again.
|
|
1360
|
+
*/
|
|
1361
|
+
function useChatScroll({ threshold = 48 } = {}) {
|
|
1362
|
+
const [scrollEl, setScrollEl] = useState(null);
|
|
1363
|
+
const [contentEl, setContentEl] = useState(null);
|
|
1364
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
1365
|
+
const pinned = useRef(true);
|
|
1366
|
+
const lastTop = useRef(0);
|
|
1367
|
+
useEffect(() => {
|
|
1368
|
+
if (!scrollEl) return;
|
|
1369
|
+
lastTop.current = scrollEl.scrollTop;
|
|
1370
|
+
const onScroll = () => {
|
|
1371
|
+
const top = scrollEl.scrollTop;
|
|
1372
|
+
if (distanceFromBottom(scrollEl) <= threshold) pinned.current = true;
|
|
1373
|
+
else if (top < lastTop.current) pinned.current = false;
|
|
1374
|
+
lastTop.current = top;
|
|
1375
|
+
setIsAtBottom(pinned.current);
|
|
1376
|
+
};
|
|
1377
|
+
scrollEl.addEventListener("scroll", onScroll, { passive: true });
|
|
1378
|
+
return () => scrollEl.removeEventListener("scroll", onScroll);
|
|
1379
|
+
}, [scrollEl, threshold]);
|
|
1380
|
+
useEffect(() => {
|
|
1381
|
+
if (!scrollEl) return;
|
|
1382
|
+
const follow = () => {
|
|
1383
|
+
if (!pinned.current) return;
|
|
1384
|
+
pinToBottom(scrollEl);
|
|
1385
|
+
lastTop.current = scrollEl.scrollTop;
|
|
1386
|
+
};
|
|
1387
|
+
follow();
|
|
1388
|
+
const target = contentEl ?? scrollEl;
|
|
1389
|
+
const mo = new MutationObserver(follow);
|
|
1390
|
+
mo.observe(target, {
|
|
1391
|
+
childList: true,
|
|
1392
|
+
subtree: true,
|
|
1393
|
+
characterData: true
|
|
1394
|
+
});
|
|
1395
|
+
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(follow) : void 0;
|
|
1396
|
+
ro?.observe(target);
|
|
1397
|
+
return () => {
|
|
1398
|
+
mo.disconnect();
|
|
1399
|
+
ro?.disconnect();
|
|
1400
|
+
};
|
|
1401
|
+
}, [scrollEl, contentEl]);
|
|
1402
|
+
return {
|
|
1403
|
+
scrollRef: setScrollEl,
|
|
1404
|
+
contentRef: setContentEl,
|
|
1405
|
+
isAtBottom,
|
|
1406
|
+
scrollToBottom: useCallback((behavior = "smooth") => {
|
|
1407
|
+
if (!scrollEl) return;
|
|
1408
|
+
pinned.current = true;
|
|
1409
|
+
setIsAtBottom(true);
|
|
1410
|
+
const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1411
|
+
if (typeof scrollEl.scrollTo === "function") scrollEl.scrollTo({
|
|
1412
|
+
top: scrollEl.scrollHeight,
|
|
1413
|
+
behavior: reduce ? "auto" : behavior
|
|
1414
|
+
});
|
|
1415
|
+
else pinToBottom(scrollEl);
|
|
1416
|
+
}, [scrollEl])
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/ChatLayout/chatLayout.variants.ts
|
|
1421
|
+
/** Slot classes for ChatLayout (plain strings: this package does not ship tailwind-variants). */
|
|
1422
|
+
const chatLayoutStyles = {
|
|
1423
|
+
root: "flex h-full min-h-0 w-full bg-background text-foreground",
|
|
1424
|
+
thread: "flex min-w-0 flex-1 flex-col",
|
|
1425
|
+
header: "shrink-0 border-b border-border",
|
|
1426
|
+
viewport: "relative min-h-0 flex-1",
|
|
1427
|
+
scroller: `h-full overflow-y-auto overscroll-contain ${focusRing} focus-visible:-outline-offset-2`,
|
|
1428
|
+
content: "mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6 sm:px-6",
|
|
1429
|
+
jump: "pointer-events-none absolute inset-x-0 bottom-3 flex justify-center",
|
|
1430
|
+
jumpButton: "pointer-events-auto rounded-full shadow-md",
|
|
1431
|
+
composer: "shrink-0 bg-background px-4 pb-4 pt-2 sm:px-6",
|
|
1432
|
+
composerInner: "mx-auto w-full max-w-3xl",
|
|
1433
|
+
handle: [
|
|
1434
|
+
"group relative w-1.5 shrink-0 cursor-col-resize touch-none bg-transparent",
|
|
1435
|
+
"after:absolute after:inset-y-0 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-border",
|
|
1436
|
+
"hover:after:w-0.5 hover:after:bg-primary/60 data-[dragging]:after:w-0.5 data-[dragging]:after:bg-primary",
|
|
1437
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-focus-ring"
|
|
1438
|
+
].join(" "),
|
|
1439
|
+
panel: "flex min-h-0 shrink-0 flex-col overflow-hidden border-s border-border bg-card"
|
|
1440
|
+
};
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/ChatLayout/ChatLayout.tsx
|
|
1443
|
+
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
|
|
1444
|
+
const isRtl = (el) => getComputedStyle(el).direction === "rtl";
|
|
1445
|
+
/**
|
|
1446
|
+
* Full-height chat frame: a scrollable thread with auto-scroll that respects scrolling up,
|
|
1447
|
+
* a "jump to latest" button, the composer pinned below, and an optional side panel that the
|
|
1448
|
+
* user can resize by dragging or with the arrow keys on its separator.
|
|
1449
|
+
*/
|
|
1450
|
+
function ChatLayout({ children, composer, header, panel, panelLabel: panelLabelProp, defaultPanelWidth = 380, minPanelWidth = 260, maxPanelWidth = 720, onPanelWidthChange, label: labelProp, scrollButtonLabel: scrollButtonLabelProp, className, contentClassName, ref }) {
|
|
1451
|
+
const { t } = useI18n();
|
|
1452
|
+
const panelLabel = panelLabelProp ?? t("chat.details");
|
|
1453
|
+
const label = labelProp ?? t("chat.conversation");
|
|
1454
|
+
const scrollButtonLabel = scrollButtonLabelProp ?? t("chat.scrollToBottom");
|
|
1455
|
+
const { scrollRef, contentRef, isAtBottom, scrollToBottom } = useChatScroll();
|
|
1456
|
+
const [width, setWidth] = useState(() => clamp(defaultPanelWidth, minPanelWidth, maxPanelWidth));
|
|
1457
|
+
const [dragging, setDragging] = useState(false);
|
|
1458
|
+
const drag = useRef(null);
|
|
1459
|
+
const resize = (next) => {
|
|
1460
|
+
const w = clamp(Math.round(next), minPanelWidth, maxPanelWidth);
|
|
1461
|
+
setWidth(w);
|
|
1462
|
+
onPanelWidthChange?.(w);
|
|
1463
|
+
};
|
|
1464
|
+
const onPointerDown = (e) => {
|
|
1465
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
1466
|
+
drag.current = {
|
|
1467
|
+
x: e.clientX,
|
|
1468
|
+
width,
|
|
1469
|
+
sign: isRtl(e.currentTarget) ? -1 : 1
|
|
1470
|
+
};
|
|
1471
|
+
setDragging(true);
|
|
1472
|
+
};
|
|
1473
|
+
const onPointerMove = (e) => {
|
|
1474
|
+
if (!drag.current) return;
|
|
1475
|
+
resize(drag.current.width + (drag.current.x - e.clientX) * drag.current.sign);
|
|
1476
|
+
};
|
|
1477
|
+
const endDrag = () => {
|
|
1478
|
+
drag.current = null;
|
|
1479
|
+
setDragging(false);
|
|
1480
|
+
};
|
|
1481
|
+
const onHandleKey = (e) => {
|
|
1482
|
+
const step = (e.shiftKey ? 64 : 16) * (isRtl(e.currentTarget) ? -1 : 1);
|
|
1483
|
+
const action = {
|
|
1484
|
+
ArrowLeft: () => resize(width + step),
|
|
1485
|
+
ArrowRight: () => resize(width - step),
|
|
1486
|
+
Home: () => resize(maxPanelWidth),
|
|
1487
|
+
End: () => resize(minPanelWidth)
|
|
1488
|
+
}[e.key];
|
|
1489
|
+
if (action) {
|
|
1490
|
+
e.preventDefault();
|
|
1491
|
+
action();
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1495
|
+
ref,
|
|
1496
|
+
className: cn(chatLayoutStyles.root, className),
|
|
1497
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
1498
|
+
"aria-label": label,
|
|
1499
|
+
className: chatLayoutStyles.thread,
|
|
1500
|
+
children: [
|
|
1501
|
+
header && /* @__PURE__ */ jsx("div", {
|
|
1502
|
+
className: chatLayoutStyles.header,
|
|
1503
|
+
children: header
|
|
1504
|
+
}),
|
|
1505
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1506
|
+
className: chatLayoutStyles.viewport,
|
|
1507
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1508
|
+
ref: scrollRef,
|
|
1509
|
+
tabIndex: 0,
|
|
1510
|
+
"aria-label": t("chat.messages"),
|
|
1511
|
+
role: "region",
|
|
1512
|
+
className: chatLayoutStyles.scroller,
|
|
1513
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1514
|
+
ref: contentRef,
|
|
1515
|
+
className: cn(chatLayoutStyles.content, contentClassName),
|
|
1516
|
+
children
|
|
1517
|
+
})
|
|
1518
|
+
}), !isAtBottom && /* @__PURE__ */ jsx("div", {
|
|
1519
|
+
className: chatLayoutStyles.jump,
|
|
1520
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1521
|
+
icon: ArrowDown,
|
|
1522
|
+
label: scrollButtonLabel,
|
|
1523
|
+
variant: "secondary",
|
|
1524
|
+
size: "sm",
|
|
1525
|
+
className: chatLayoutStyles.jumpButton,
|
|
1526
|
+
onClick: () => scrollToBottom()
|
|
1527
|
+
})
|
|
1528
|
+
})]
|
|
1529
|
+
}),
|
|
1530
|
+
composer && /* @__PURE__ */ jsx("div", {
|
|
1531
|
+
className: chatLayoutStyles.composer,
|
|
1532
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1533
|
+
className: chatLayoutStyles.composerInner,
|
|
1534
|
+
children: composer
|
|
1535
|
+
})
|
|
1536
|
+
})
|
|
1537
|
+
]
|
|
1538
|
+
}), panel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
1539
|
+
role: "separator",
|
|
1540
|
+
"aria-orientation": "vertical",
|
|
1541
|
+
"aria-label": t("chat.resizePanel", { panel: panelLabel.toLowerCase() }),
|
|
1542
|
+
"aria-valuenow": width,
|
|
1543
|
+
"aria-valuemin": minPanelWidth,
|
|
1544
|
+
"aria-valuemax": maxPanelWidth,
|
|
1545
|
+
tabIndex: 0,
|
|
1546
|
+
"data-dragging": dragging || void 0,
|
|
1547
|
+
className: chatLayoutStyles.handle,
|
|
1548
|
+
onPointerDown,
|
|
1549
|
+
onPointerMove,
|
|
1550
|
+
onPointerUp: endDrag,
|
|
1551
|
+
onPointerCancel: endDrag,
|
|
1552
|
+
onKeyDown: onHandleKey
|
|
1553
|
+
}), /* @__PURE__ */ jsx("aside", {
|
|
1554
|
+
"aria-label": panelLabel,
|
|
1555
|
+
className: chatLayoutStyles.panel,
|
|
1556
|
+
style: { width },
|
|
1557
|
+
children: panel
|
|
1558
|
+
})] })]
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/ChatLayout/ChatLayout.doc.ts
|
|
1563
|
+
const doc$3 = {
|
|
1564
|
+
name: "ChatLayout",
|
|
1565
|
+
group: "Layout",
|
|
1566
|
+
status: "beta",
|
|
1567
|
+
description: "Full-height chat frame: scrollable thread with auto-scroll that respects scrolling up, a jump-to-latest button, a composer pinned below and an optional resizable side panel (artifact or details).",
|
|
1568
|
+
pattern: "window splitter",
|
|
1569
|
+
keyboard: [
|
|
1570
|
+
["Tab", "Moves through the thread, the composer, the resize handle and the panel"],
|
|
1571
|
+
["ArrowLeft", "On the resize handle: widens the side panel (Shift for bigger steps)"],
|
|
1572
|
+
["ArrowRight", "On the resize handle: narrows the side panel"],
|
|
1573
|
+
["Home", "On the resize handle: panel to its maximum width"],
|
|
1574
|
+
["End", "On the resize handle: panel to its minimum width"]
|
|
1575
|
+
],
|
|
1576
|
+
tokens: [
|
|
1577
|
+
"background",
|
|
1578
|
+
"foreground",
|
|
1579
|
+
"card",
|
|
1580
|
+
"border",
|
|
1581
|
+
"primary",
|
|
1582
|
+
"focus-ring"
|
|
1583
|
+
]
|
|
1584
|
+
};
|
|
1585
|
+
//#endregion
|
|
1586
|
+
//#region src/Citation/citation.variants.ts
|
|
1587
|
+
/** Slot classes for Citation and CitationList. */
|
|
1588
|
+
const citationStyles = {
|
|
1589
|
+
trigger: [
|
|
1590
|
+
"mx-0.5 inline-flex h-[18px] min-w-[18px] -translate-y-px cursor-pointer items-center justify-center rounded-md px-1",
|
|
1591
|
+
"bg-primary/14 align-baseline font-mono text-[10.5px] font-semibold leading-none text-primary-chip-text",
|
|
1592
|
+
`transition-colors hover:bg-primary/24 data-[popup-open]:bg-primary/24 motion-reduce:transition-none ${focusRing}`
|
|
1593
|
+
].join(" "),
|
|
1594
|
+
popup: "w-80",
|
|
1595
|
+
head: "flex items-center gap-2 text-[11.5px] text-muted-foreground",
|
|
1596
|
+
index: "grid h-[18px] min-w-[18px] place-items-center rounded-md bg-secondary px-1 font-mono text-[10.5px] font-semibold text-foreground",
|
|
1597
|
+
title: "mt-1.5 text-[13px] font-semibold leading-snug text-foreground",
|
|
1598
|
+
snippet: "mt-1 line-clamp-4 text-[12.5px] leading-relaxed text-muted-foreground",
|
|
1599
|
+
open: `mt-2.5 inline-flex items-center gap-1 rounded-sm text-[12.5px] font-medium text-primary-text hover:underline ${focusRing}`,
|
|
1600
|
+
list: "flex flex-col gap-1.5",
|
|
1601
|
+
heading: "mb-1 text-[11.5px] font-semibold tracking-wide text-muted-foreground uppercase",
|
|
1602
|
+
item: "flex gap-2.5 rounded-lg border border-border bg-card px-3 py-2.5",
|
|
1603
|
+
itemBody: "min-w-0 flex-1",
|
|
1604
|
+
itemTitle: `rounded-sm text-[13px] font-medium text-foreground hover:text-primary-text hover:underline ${focusRing}`,
|
|
1605
|
+
itemHost: "mt-0.5 truncate font-mono text-[11px] text-muted-foreground"
|
|
1606
|
+
};
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/Citation/Citation.tsx
|
|
1609
|
+
/** Host name of a URL ("docs.example.com"), or undefined if it cannot be parsed. */
|
|
1610
|
+
function sourceHost(url) {
|
|
1611
|
+
if (!url) return void 0;
|
|
1612
|
+
try {
|
|
1613
|
+
return new URL(url).host.replace(/^www\./, "");
|
|
1614
|
+
} catch {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
/** Only http(s) sources become links. */
|
|
1619
|
+
function safeSourceUrl(url) {
|
|
1620
|
+
return url && /^https?:\/\//i.test(url) ? url : void 0;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Inline numbered reference ([1]). Hovering, focusing + Enter or clicking opens a preview
|
|
1624
|
+
* card with the title, host, snippet and a link to the source.
|
|
1625
|
+
*/
|
|
1626
|
+
function Citation({ index, source, delay = 200, className }) {
|
|
1627
|
+
const { t } = useI18n();
|
|
1628
|
+
const host = source.publisher ?? sourceHost(source.url);
|
|
1629
|
+
const href = safeSourceUrl(source.url);
|
|
1630
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
1631
|
+
openOnHover: true,
|
|
1632
|
+
delay,
|
|
1633
|
+
"aria-label": t("chat.source", {
|
|
1634
|
+
index,
|
|
1635
|
+
title: source.title
|
|
1636
|
+
}),
|
|
1637
|
+
className: cn(citationStyles.trigger, className),
|
|
1638
|
+
children: index
|
|
1639
|
+
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
1640
|
+
side: "top",
|
|
1641
|
+
align: "start",
|
|
1642
|
+
className: citationStyles.popup,
|
|
1643
|
+
children: [
|
|
1644
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1645
|
+
className: citationStyles.head,
|
|
1646
|
+
children: [
|
|
1647
|
+
/* @__PURE__ */ jsx("span", {
|
|
1648
|
+
className: citationStyles.index,
|
|
1649
|
+
children: index
|
|
1650
|
+
}),
|
|
1651
|
+
/* @__PURE__ */ jsx(Globe, {
|
|
1652
|
+
size: 12,
|
|
1653
|
+
"aria-hidden": true
|
|
1654
|
+
}),
|
|
1655
|
+
host && /* @__PURE__ */ jsx("span", {
|
|
1656
|
+
className: "truncate",
|
|
1657
|
+
children: host
|
|
1658
|
+
})
|
|
1659
|
+
]
|
|
1660
|
+
}),
|
|
1661
|
+
/* @__PURE__ */ jsx(PopoverTitle, {
|
|
1662
|
+
className: citationStyles.title,
|
|
1663
|
+
children: source.title
|
|
1664
|
+
}),
|
|
1665
|
+
source.snippet && /* @__PURE__ */ jsx(PopoverDescription, {
|
|
1666
|
+
className: citationStyles.snippet,
|
|
1667
|
+
children: source.snippet
|
|
1668
|
+
}),
|
|
1669
|
+
href && /* @__PURE__ */ jsxs("a", {
|
|
1670
|
+
href,
|
|
1671
|
+
target: "_blank",
|
|
1672
|
+
rel: "noopener noreferrer nofollow",
|
|
1673
|
+
className: citationStyles.open,
|
|
1674
|
+
children: [
|
|
1675
|
+
t("chat.openSource"),
|
|
1676
|
+
/* @__PURE__ */ jsx(ExternalLink, {
|
|
1677
|
+
size: 12,
|
|
1678
|
+
"aria-hidden": true
|
|
1679
|
+
}),
|
|
1680
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1681
|
+
className: "sr-only",
|
|
1682
|
+
children: [" ", t("link.external")]
|
|
1683
|
+
})
|
|
1684
|
+
]
|
|
1685
|
+
})
|
|
1686
|
+
]
|
|
1687
|
+
})] });
|
|
1688
|
+
}
|
|
1689
|
+
/** Numbered list of the sources cited in a reply, matching the inline markers. */
|
|
1690
|
+
function CitationList({ sources, heading: headingProp, className }) {
|
|
1691
|
+
const { t } = useI18n();
|
|
1692
|
+
const heading = headingProp ?? t("chat.sources");
|
|
1693
|
+
const headingId = useId();
|
|
1694
|
+
if (sources.length === 0) return null;
|
|
1695
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
1696
|
+
"aria-labelledby": headingId,
|
|
1697
|
+
className,
|
|
1698
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1699
|
+
id: headingId,
|
|
1700
|
+
className: citationStyles.heading,
|
|
1701
|
+
children: heading
|
|
1702
|
+
}), /* @__PURE__ */ jsx("ol", {
|
|
1703
|
+
className: citationStyles.list,
|
|
1704
|
+
children: sources.map((src, i) => {
|
|
1705
|
+
const href = safeSourceUrl(src.url);
|
|
1706
|
+
const host = src.publisher ?? sourceHost(src.url);
|
|
1707
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
1708
|
+
className: citationStyles.item,
|
|
1709
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1710
|
+
className: citationStyles.index,
|
|
1711
|
+
"aria-hidden": true,
|
|
1712
|
+
children: i + 1
|
|
1713
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1714
|
+
className: citationStyles.itemBody,
|
|
1715
|
+
children: [href ? /* @__PURE__ */ jsxs("a", {
|
|
1716
|
+
href,
|
|
1717
|
+
target: "_blank",
|
|
1718
|
+
rel: "noopener noreferrer nofollow",
|
|
1719
|
+
className: citationStyles.itemTitle,
|
|
1720
|
+
children: [src.title, /* @__PURE__ */ jsxs("span", {
|
|
1721
|
+
className: "sr-only",
|
|
1722
|
+
children: [" ", t("link.external")]
|
|
1723
|
+
})]
|
|
1724
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1725
|
+
className: "text-[13px] font-medium text-foreground",
|
|
1726
|
+
children: src.title
|
|
1727
|
+
}), host && /* @__PURE__ */ jsx("div", {
|
|
1728
|
+
className: citationStyles.itemHost,
|
|
1729
|
+
children: host
|
|
1730
|
+
})]
|
|
1731
|
+
})]
|
|
1732
|
+
}, `${i}-${src.title}`);
|
|
1733
|
+
})
|
|
1734
|
+
})]
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
//#endregion
|
|
1738
|
+
//#region src/Citation/Citation.doc.ts
|
|
1739
|
+
const doc$5 = {
|
|
1740
|
+
name: "Citation",
|
|
1741
|
+
group: "Display",
|
|
1742
|
+
status: "beta",
|
|
1743
|
+
description: "Numbered source references for grounded answers: an inline [n] marker that previews the source (title, host, snippet, link) on hover, focus + Enter or click, and CitationList, the numbered list of sources under a reply. Only http(s) URLs become links.",
|
|
1744
|
+
primitive: "@base-ui/react/popover",
|
|
1745
|
+
pattern: "dialog (non-modal)",
|
|
1746
|
+
keyboard: [
|
|
1747
|
+
["Enter", "On a marker: opens the source preview"],
|
|
1748
|
+
["Space", "On a marker: opens the source preview"],
|
|
1749
|
+
["Escape", "Closes the preview and returns focus to the marker"],
|
|
1750
|
+
["Tab", "Moves to the \"Open source\" link inside an open preview"]
|
|
1751
|
+
],
|
|
1752
|
+
tokens: [
|
|
1753
|
+
"primary",
|
|
1754
|
+
"primary-text",
|
|
1755
|
+
"popover",
|
|
1756
|
+
"secondary",
|
|
1757
|
+
"card",
|
|
1758
|
+
"border",
|
|
1759
|
+
"muted-foreground",
|
|
1760
|
+
"focus-ring"
|
|
1761
|
+
]
|
|
1762
|
+
};
|
|
1763
|
+
//#endregion
|
|
1764
|
+
//#region src/Markdown/markdown.variants.ts
|
|
1765
|
+
/**
|
|
1766
|
+
* Blinking caret drawn after the last block while text streams in. Pure CSS (an ::after on the
|
|
1767
|
+
* last child), so it follows paragraphs, list items and code without extra DOM.
|
|
1768
|
+
*/
|
|
1769
|
+
const streamingCaretClass = [
|
|
1770
|
+
"[&>:last-child]:after:ms-0.5 [&>:last-child]:after:inline-block [&>:last-child]:after:h-[1.05em] [&>:last-child]:after:w-[2px]",
|
|
1771
|
+
"[&>:last-child]:after:translate-y-[2px] [&>:last-child]:after:bg-primary [&>:last-child]:after:content-['']",
|
|
1772
|
+
"[&>:last-child]:after:animate-pulse motion-reduce:[&>:last-child]:after:animate-none"
|
|
1773
|
+
].join(" ");
|
|
1774
|
+
/** Element classes for Markdown (token colours only). */
|
|
1775
|
+
const markdownStyles = {
|
|
1776
|
+
root: "min-w-0 text-[14px] leading-[1.7] text-foreground [overflow-wrap:anywhere] [&>:first-child]:mt-0 [&>:last-child]:mb-0",
|
|
1777
|
+
h1: "mt-5 mb-2 text-[18px] font-semibold tracking-tight",
|
|
1778
|
+
h2: "mt-5 mb-2 text-[16px] font-semibold tracking-tight",
|
|
1779
|
+
h3: "mt-4 mb-1.5 text-[14.5px] font-semibold",
|
|
1780
|
+
h4: "mt-4 mb-1.5 text-[13.5px] font-semibold text-muted-foreground",
|
|
1781
|
+
p: "my-2.5",
|
|
1782
|
+
ul: "my-2.5 list-disc space-y-1 ps-5 marker:text-muted-foreground",
|
|
1783
|
+
ol: "my-2.5 list-decimal space-y-1 ps-5 marker:text-muted-foreground",
|
|
1784
|
+
li: "ps-1 [&>ul]:my-1 [&>ol]:my-1 [&.task-list-item]:list-none [&.task-list-item]:-ms-5",
|
|
1785
|
+
blockquote: "my-3 border-s-2 border-primary/50 ps-3.5 text-muted-foreground",
|
|
1786
|
+
hr: "my-5 border-border",
|
|
1787
|
+
link: `rounded-sm font-medium text-primary-text underline decoration-primary/40 underline-offset-2 hover:decoration-primary ${focusRing}`,
|
|
1788
|
+
linkIcon: "ms-0.5 inline-block align-[-1px]",
|
|
1789
|
+
inlineCode: "rounded-md border border-border bg-secondary/60 px-1.5 py-px font-mono text-[12.5px]",
|
|
1790
|
+
tableWrap: "my-3 overflow-x-auto rounded-lg border border-border",
|
|
1791
|
+
table: "w-full border-collapse text-start text-[13px]",
|
|
1792
|
+
th: "border-b border-border bg-secondary/50 px-3 py-2 font-semibold",
|
|
1793
|
+
td: "border-b border-border px-3 py-2 align-top [tr:last-child_&]:border-b-0",
|
|
1794
|
+
checkbox: "me-1.5 align-[-2px] accent-primary",
|
|
1795
|
+
image: "text-muted-foreground italic",
|
|
1796
|
+
code: {
|
|
1797
|
+
root: "my-3 overflow-hidden rounded-lg border border-border bg-secondary/40",
|
|
1798
|
+
header: "flex h-9 items-center justify-between border-b border-border bg-secondary/50 ps-3 pe-1.5",
|
|
1799
|
+
lang: "font-mono text-[11px] tracking-wide text-muted-foreground uppercase",
|
|
1800
|
+
pre: "overflow-x-auto p-3 font-mono text-[12.5px] leading-6"
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
//#endregion
|
|
1804
|
+
//#region src/Markdown/CodeBlock.tsx
|
|
1805
|
+
const s$1 = markdownStyles.code;
|
|
1806
|
+
/** Fenced code with a language label and a copy button. */
|
|
1807
|
+
function CodeBlock({ code, language, className }) {
|
|
1808
|
+
const { copied, copy } = useCopy();
|
|
1809
|
+
const { t } = useI18n();
|
|
1810
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1811
|
+
className: className ? `${s$1.root} ${className}` : s$1.root,
|
|
1812
|
+
"data-language": language,
|
|
1813
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1814
|
+
className: s$1.header,
|
|
1815
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1816
|
+
className: s$1.lang,
|
|
1817
|
+
children: language || "text"
|
|
1818
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1819
|
+
size: "sm",
|
|
1820
|
+
variant: "ghost",
|
|
1821
|
+
icon: copied ? Check : Copy,
|
|
1822
|
+
"aria-label": copied ? t("chat.copiedCode") : t("chat.copyCode"),
|
|
1823
|
+
className: "h-7 px-2",
|
|
1824
|
+
onClick: () => void copy(code),
|
|
1825
|
+
children: copied ? t("common.copied") : t("common.copy")
|
|
1826
|
+
})]
|
|
1827
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
1828
|
+
className: s$1.pre,
|
|
1829
|
+
children: /* @__PURE__ */ jsx("code", { children: code })
|
|
1830
|
+
})]
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
//#endregion
|
|
1834
|
+
//#region src/Markdown/Markdown.tsx
|
|
1835
|
+
const SAFE_PROTOCOL = /^(https?|mailto|tel):/i;
|
|
1836
|
+
/**
|
|
1837
|
+
* URL policy: relative URLs, anchors and http(s)/mailto/tel only. Everything else
|
|
1838
|
+
* (javascript:, data:, vbscript:, …) is dropped, on top of react-markdown's own transform.
|
|
1839
|
+
*/
|
|
1840
|
+
function safeUrl(url) {
|
|
1841
|
+
const cleaned = defaultUrlTransform(url);
|
|
1842
|
+
if (!cleaned) return "";
|
|
1843
|
+
const compact = Array.from(cleaned).filter((c) => c.charCodeAt(0) > 32).join("");
|
|
1844
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(compact) && !SAFE_PROTOCOL.test(compact)) return "";
|
|
1845
|
+
return cleaned;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Makes partial streamed markdown render sanely: a fence that has opened but not closed is
|
|
1849
|
+
* closed at the end, so the tail renders as code instead of flashing as paragraphs.
|
|
1850
|
+
*/
|
|
1851
|
+
function repairStreamingMarkdown(text) {
|
|
1852
|
+
const fences = text.match(/^ {0,3}(`{3,}|~{3,})/gm) ?? [];
|
|
1853
|
+
if (fences.length % 2 === 0) return text;
|
|
1854
|
+
const opener = (fences[fences.length - 1] ?? "```").trim();
|
|
1855
|
+
return `${text}${text.endsWith("\n") ? "" : "\n"}${opener}`;
|
|
1856
|
+
}
|
|
1857
|
+
const isExternal = (href) => /^https?:\/\//i.test(href);
|
|
1858
|
+
/** Drops react-markdown's `node` (hast) prop before spreading onto a DOM element. */
|
|
1859
|
+
/** Screen-reader note on links that open a new tab. */
|
|
1860
|
+
function NewTabHint() {
|
|
1861
|
+
const { t } = useI18n();
|
|
1862
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1863
|
+
className: "sr-only",
|
|
1864
|
+
children: [" ", t("link.external")]
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
/** Remote images can track readers and break layout mid-stream: show them as links. */
|
|
1868
|
+
function ImageLink({ href, alt }) {
|
|
1869
|
+
const { t } = useI18n();
|
|
1870
|
+
const text = t("chat.image", { alt: alt || t("chat.untitled") });
|
|
1871
|
+
return href ? /* @__PURE__ */ jsxs("a", {
|
|
1872
|
+
href,
|
|
1873
|
+
target: "_blank",
|
|
1874
|
+
rel: "noopener noreferrer nofollow",
|
|
1875
|
+
className: markdownStyles.link,
|
|
1876
|
+
children: [text, /* @__PURE__ */ jsx(NewTabHint, {})]
|
|
1877
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1878
|
+
className: markdownStyles.image,
|
|
1879
|
+
children: text
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
function TaskCheckbox({ checked }) {
|
|
1883
|
+
const { t } = useI18n();
|
|
1884
|
+
return /* @__PURE__ */ jsx("input", {
|
|
1885
|
+
type: "checkbox",
|
|
1886
|
+
checked,
|
|
1887
|
+
readOnly: true,
|
|
1888
|
+
disabled: true,
|
|
1889
|
+
"aria-label": checked ? t("chat.taskDone") : t("chat.taskNotDone"),
|
|
1890
|
+
className: markdownStyles.checkbox
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
function omitNode(props) {
|
|
1894
|
+
const { node, ...rest } = props;
|
|
1895
|
+
return rest;
|
|
1896
|
+
}
|
|
1897
|
+
const components = {
|
|
1898
|
+
h1: (p) => /* @__PURE__ */ jsx("h1", {
|
|
1899
|
+
...omitNode(p),
|
|
1900
|
+
className: markdownStyles.h1
|
|
1901
|
+
}),
|
|
1902
|
+
h2: (p) => /* @__PURE__ */ jsx("h2", {
|
|
1903
|
+
...omitNode(p),
|
|
1904
|
+
className: markdownStyles.h2
|
|
1905
|
+
}),
|
|
1906
|
+
h3: (p) => /* @__PURE__ */ jsx("h3", {
|
|
1907
|
+
...omitNode(p),
|
|
1908
|
+
className: markdownStyles.h3
|
|
1909
|
+
}),
|
|
1910
|
+
h4: (p) => /* @__PURE__ */ jsx("h4", {
|
|
1911
|
+
...omitNode(p),
|
|
1912
|
+
className: markdownStyles.h4
|
|
1913
|
+
}),
|
|
1914
|
+
h5: (p) => /* @__PURE__ */ jsx("h4", {
|
|
1915
|
+
...omitNode(p),
|
|
1916
|
+
className: markdownStyles.h4
|
|
1917
|
+
}),
|
|
1918
|
+
h6: (p) => /* @__PURE__ */ jsx("h4", {
|
|
1919
|
+
...omitNode(p),
|
|
1920
|
+
className: markdownStyles.h4
|
|
1921
|
+
}),
|
|
1922
|
+
p: (p) => /* @__PURE__ */ jsx("p", {
|
|
1923
|
+
...omitNode(p),
|
|
1924
|
+
className: markdownStyles.p
|
|
1925
|
+
}),
|
|
1926
|
+
ul: (p) => /* @__PURE__ */ jsx("ul", {
|
|
1927
|
+
...omitNode(p),
|
|
1928
|
+
className: cn(markdownStyles.ul, p.className === "contains-task-list" && "ps-1")
|
|
1929
|
+
}),
|
|
1930
|
+
ol: (p) => /* @__PURE__ */ jsx("ol", {
|
|
1931
|
+
...omitNode(p),
|
|
1932
|
+
className: markdownStyles.ol
|
|
1933
|
+
}),
|
|
1934
|
+
li: (p) => /* @__PURE__ */ jsx("li", {
|
|
1935
|
+
...omitNode(p),
|
|
1936
|
+
className: cn(markdownStyles.li, p.className)
|
|
1937
|
+
}),
|
|
1938
|
+
blockquote: (p) => /* @__PURE__ */ jsx("blockquote", {
|
|
1939
|
+
...omitNode(p),
|
|
1940
|
+
className: markdownStyles.blockquote
|
|
1941
|
+
}),
|
|
1942
|
+
hr: () => /* @__PURE__ */ jsx("hr", { className: markdownStyles.hr }),
|
|
1943
|
+
table: (p) => /* @__PURE__ */ jsx("div", {
|
|
1944
|
+
className: markdownStyles.tableWrap,
|
|
1945
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
1946
|
+
...omitNode(p),
|
|
1947
|
+
className: markdownStyles.table
|
|
1948
|
+
})
|
|
1949
|
+
}),
|
|
1950
|
+
th: (p) => /* @__PURE__ */ jsx("th", {
|
|
1951
|
+
...omitNode(p),
|
|
1952
|
+
className: markdownStyles.th
|
|
1953
|
+
}),
|
|
1954
|
+
td: (p) => /* @__PURE__ */ jsx("td", {
|
|
1955
|
+
...omitNode(p),
|
|
1956
|
+
className: markdownStyles.td
|
|
1957
|
+
}),
|
|
1958
|
+
a: (props) => {
|
|
1959
|
+
const { href, children, ...p } = omitNode(props);
|
|
1960
|
+
if (!href) return /* @__PURE__ */ jsx("span", { children });
|
|
1961
|
+
if (!isExternal(href)) return /* @__PURE__ */ jsx("a", {
|
|
1962
|
+
href,
|
|
1963
|
+
className: markdownStyles.link,
|
|
1964
|
+
...p,
|
|
1965
|
+
children
|
|
1966
|
+
});
|
|
1967
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
1968
|
+
href,
|
|
1969
|
+
target: "_blank",
|
|
1970
|
+
rel: "noopener noreferrer nofollow",
|
|
1971
|
+
className: markdownStyles.link,
|
|
1972
|
+
...p,
|
|
1973
|
+
children: [
|
|
1974
|
+
children,
|
|
1975
|
+
/* @__PURE__ */ jsx(ExternalLink, {
|
|
1976
|
+
size: 12,
|
|
1977
|
+
"aria-hidden": true,
|
|
1978
|
+
className: markdownStyles.linkIcon
|
|
1979
|
+
}),
|
|
1980
|
+
/* @__PURE__ */ jsx(NewTabHint, {})
|
|
1981
|
+
]
|
|
1982
|
+
});
|
|
1983
|
+
},
|
|
1984
|
+
img: ({ src, alt }) => /* @__PURE__ */ jsx(ImageLink, {
|
|
1985
|
+
href: typeof src === "string" ? src : "",
|
|
1986
|
+
alt
|
|
1987
|
+
}),
|
|
1988
|
+
input: ({ checked, type }) => type === "checkbox" ? /* @__PURE__ */ jsx(TaskCheckbox, { checked: !!checked }) : null,
|
|
1989
|
+
code: ({ className, children }) => /* @__PURE__ */ jsx("code", {
|
|
1990
|
+
className: cn(markdownStyles.inlineCode, className),
|
|
1991
|
+
children
|
|
1992
|
+
}),
|
|
1993
|
+
pre: ({ children }) => {
|
|
1994
|
+
const child = Children.toArray(children)[0];
|
|
1995
|
+
if (!isValidElement(child)) return /* @__PURE__ */ jsx("pre", {
|
|
1996
|
+
className: markdownStyles.code.pre,
|
|
1997
|
+
children
|
|
1998
|
+
});
|
|
1999
|
+
const language = /language-([\w+#.-]+)/.exec(child.props.className ?? "")?.[1];
|
|
2000
|
+
const code = String(Children.toArray(child.props.children).join("")).replace(/\n$/, "");
|
|
2001
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
2002
|
+
code,
|
|
2003
|
+
language
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
const remarkPlugins = [remarkGfm];
|
|
2008
|
+
/**
|
|
2009
|
+
* Safe markdown for model output: raw HTML is never rendered (skipped, not parsed), links are
|
|
2010
|
+
* restricted to http(s)/mailto/tel/relative, external links open in a new tab with a marker,
|
|
2011
|
+
* and fenced code gets a copy button. Partial input from a stream is tolerated.
|
|
2012
|
+
*/
|
|
2013
|
+
const Markdown = memo(function Markdown({ children, streaming = false, className }) {
|
|
2014
|
+
const source = streaming ? repairStreamingMarkdown(children) : children;
|
|
2015
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2016
|
+
"data-streaming": streaming || void 0,
|
|
2017
|
+
className: cn(markdownStyles.root, streaming && streamingCaretClass, className),
|
|
2018
|
+
children: /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
2019
|
+
remarkPlugins,
|
|
2020
|
+
components,
|
|
2021
|
+
skipHtml: true,
|
|
2022
|
+
urlTransform: safeUrl,
|
|
2023
|
+
children: source
|
|
2024
|
+
})
|
|
2025
|
+
});
|
|
2026
|
+
});
|
|
2027
|
+
//#endregion
|
|
2028
|
+
//#region src/Markdown/Markdown.doc.ts
|
|
2029
|
+
const doc$6 = {
|
|
2030
|
+
name: "Markdown",
|
|
2031
|
+
group: "Display",
|
|
2032
|
+
status: "beta",
|
|
2033
|
+
description: "Safe, token-styled markdown for model output (react-markdown + remark-gfm): headings, lists, task lists, tables, blockquotes, inline and fenced code with a copy button, links with an external marker. Raw HTML is never rendered, unsafe URL schemes are stripped, and partial streamed input (unclosed fences) renders cleanly.",
|
|
2034
|
+
keyboard: [["Tab", "Moves through links and code-block copy buttons in reading order"], ["Enter", "Follows a link or copies the focused code block"]],
|
|
2035
|
+
tokens: [
|
|
2036
|
+
"foreground",
|
|
2037
|
+
"muted-foreground",
|
|
2038
|
+
"primary-text",
|
|
2039
|
+
"secondary",
|
|
2040
|
+
"border",
|
|
2041
|
+
"focus-ring"
|
|
2042
|
+
]
|
|
2043
|
+
};
|
|
2044
|
+
//#endregion
|
|
2045
|
+
//#region src/SuggestionChips/suggestionChips.variants.ts
|
|
2046
|
+
/** Slot classes for SuggestionChips. */
|
|
2047
|
+
const suggestionChipsStyles = {
|
|
2048
|
+
root: "w-full",
|
|
2049
|
+
heading: "mb-2.5 text-[12px] font-semibold tracking-wide text-muted-foreground uppercase",
|
|
2050
|
+
grid: "grid gap-2 sm:grid-cols-2",
|
|
2051
|
+
row: "flex flex-wrap gap-2",
|
|
2052
|
+
card: [
|
|
2053
|
+
"group flex w-full cursor-pointer items-start gap-2.5 rounded-lg border border-border bg-card px-3.5 py-3 text-start shadow-sm",
|
|
2054
|
+
"transition-colors hover:border-primary/40 hover:bg-secondary/40 motion-reduce:transition-none",
|
|
2055
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
2056
|
+
focusRing
|
|
2057
|
+
].join(" "),
|
|
2058
|
+
chip: [
|
|
2059
|
+
"inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-card px-3 text-[12.5px] font-medium text-foreground",
|
|
2060
|
+
"transition-colors hover:border-primary/40 hover:bg-secondary/40 motion-reduce:transition-none",
|
|
2061
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
2062
|
+
focusRing
|
|
2063
|
+
].join(" "),
|
|
2064
|
+
icon: "mt-0.5 shrink-0 text-muted-foreground group-hover:text-primary-text",
|
|
2065
|
+
label: "block text-[13px] font-medium text-foreground",
|
|
2066
|
+
description: "mt-0.5 block text-[12px] leading-5 text-muted-foreground"
|
|
2067
|
+
};
|
|
2068
|
+
//#endregion
|
|
2069
|
+
//#region src/SuggestionChips/SuggestionChips.tsx
|
|
2070
|
+
const normalize = (x) => typeof x === "string" ? { label: x } : x;
|
|
2071
|
+
/** Prompt suggestions for an empty conversation (cards) or as follow-ups after a reply (chips). */
|
|
2072
|
+
function SuggestionChips({ suggestions, onSelect, layout = "cards", heading, label: labelProp, disabled = false, className }) {
|
|
2073
|
+
const { t } = useI18n();
|
|
2074
|
+
const label = labelProp ?? t("chat.suggestions");
|
|
2075
|
+
const headingId = useId();
|
|
2076
|
+
const items = suggestions.map(normalize);
|
|
2077
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2078
|
+
role: "group",
|
|
2079
|
+
"aria-labelledby": heading ? headingId : void 0,
|
|
2080
|
+
"aria-label": heading ? void 0 : label,
|
|
2081
|
+
className: cn(suggestionChipsStyles.root, className),
|
|
2082
|
+
children: [heading && /* @__PURE__ */ jsx("div", {
|
|
2083
|
+
id: headingId,
|
|
2084
|
+
className: suggestionChipsStyles.heading,
|
|
2085
|
+
children: heading
|
|
2086
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2087
|
+
className: layout === "cards" ? suggestionChipsStyles.grid : suggestionChipsStyles.row,
|
|
2088
|
+
children: items.map((item) => {
|
|
2089
|
+
const Icon = item.icon;
|
|
2090
|
+
const prompt = item.prompt ?? item.label;
|
|
2091
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
2092
|
+
type: "button",
|
|
2093
|
+
disabled,
|
|
2094
|
+
className: layout === "cards" ? suggestionChipsStyles.card : suggestionChipsStyles.chip,
|
|
2095
|
+
onClick: () => onSelect(prompt, item),
|
|
2096
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
2097
|
+
size: layout === "cards" ? 16 : 14,
|
|
2098
|
+
"aria-hidden": true,
|
|
2099
|
+
className: layout === "cards" ? suggestionChipsStyles.icon : "shrink-0 text-muted-foreground"
|
|
2100
|
+
}), layout === "cards" ? /* @__PURE__ */ jsxs("span", {
|
|
2101
|
+
className: "min-w-0",
|
|
2102
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2103
|
+
className: suggestionChipsStyles.label,
|
|
2104
|
+
children: item.label
|
|
2105
|
+
}), item.description && /* @__PURE__ */ jsx("span", {
|
|
2106
|
+
className: suggestionChipsStyles.description,
|
|
2107
|
+
children: item.description
|
|
2108
|
+
})]
|
|
2109
|
+
}) : item.label]
|
|
2110
|
+
}, item.id ?? item.label);
|
|
2111
|
+
})
|
|
2112
|
+
})]
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/SuggestionChips/SuggestionChips.doc.ts
|
|
2117
|
+
const doc$9 = {
|
|
2118
|
+
name: "SuggestionChips",
|
|
2119
|
+
group: "Actions",
|
|
2120
|
+
status: "beta",
|
|
2121
|
+
description: "Prompt suggestions: a two-column grid of cards with descriptions for an empty conversation, or a row of pills for follow-ups after a reply. Choosing one calls onSelect with its prompt.",
|
|
2122
|
+
pattern: "button group",
|
|
2123
|
+
keyboard: [
|
|
2124
|
+
["Tab", "Moves between suggestions"],
|
|
2125
|
+
["Enter", "Sends the focused suggestion"],
|
|
2126
|
+
["Space", "Sends the focused suggestion"]
|
|
2127
|
+
],
|
|
2128
|
+
tokens: [
|
|
2129
|
+
"card",
|
|
2130
|
+
"border",
|
|
2131
|
+
"primary",
|
|
2132
|
+
"primary-text",
|
|
2133
|
+
"secondary",
|
|
2134
|
+
"muted-foreground",
|
|
2135
|
+
"focus-ring"
|
|
2136
|
+
]
|
|
2137
|
+
};
|
|
2138
|
+
//#endregion
|
|
2139
|
+
//#region src/ToolCallCard/toolCallCard.variants.ts
|
|
2140
|
+
/** Slot classes for ToolCallCard and JsonView. */
|
|
2141
|
+
const toolCallCardStyles = {
|
|
2142
|
+
root: "w-full overflow-hidden rounded-lg border border-border bg-card text-[13px]",
|
|
2143
|
+
trigger: ["flex h-auto w-full min-w-0 justify-start gap-2.5 rounded-none px-3 py-2.5 text-start hover:bg-secondary/50", "focus-visible:-outline-offset-2"].join(" "),
|
|
2144
|
+
icon: "grid size-6 shrink-0 place-items-center rounded-md bg-secondary text-muted-foreground",
|
|
2145
|
+
iconFailed: "bg-destructive/15 text-destructive-chip-text",
|
|
2146
|
+
label: "flex min-w-0 flex-1 items-baseline gap-1.5",
|
|
2147
|
+
verb: "shrink-0 text-muted-foreground",
|
|
2148
|
+
name: "truncate font-mono text-[12.5px] font-semibold text-foreground",
|
|
2149
|
+
meta: "flex shrink-0 items-center gap-2",
|
|
2150
|
+
duration: "font-mono text-[11px] text-muted-foreground tabular-nums",
|
|
2151
|
+
body: "flex flex-col gap-2 border-t border-border px-3 pb-3 pt-1",
|
|
2152
|
+
error: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[12.5px] text-destructive-chip-text",
|
|
2153
|
+
json: {
|
|
2154
|
+
trigger: "mx-0 h-7 px-1 text-[11.5px] font-semibold tracking-wide text-muted-foreground uppercase",
|
|
2155
|
+
content: "pt-1",
|
|
2156
|
+
pre: "max-h-64 overflow-auto rounded-md border border-border bg-secondary/40 p-2.5 font-mono text-[12px] leading-5 text-foreground"
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
//#endregion
|
|
2160
|
+
//#region src/ToolCallCard/JsonView.tsx
|
|
2161
|
+
const s = toolCallCardStyles.json;
|
|
2162
|
+
/** A small collapsible, scrollable JSON block. */
|
|
2163
|
+
function JsonView({ label, value, defaultOpen = true }) {
|
|
2164
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
2165
|
+
defaultOpen,
|
|
2166
|
+
children: [/* @__PURE__ */ jsx(CollapsibleTrigger, {
|
|
2167
|
+
className: s.trigger,
|
|
2168
|
+
children: label
|
|
2169
|
+
}), /* @__PURE__ */ jsx(CollapsiblePanel, {
|
|
2170
|
+
className: s.content,
|
|
2171
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
2172
|
+
tabIndex: 0,
|
|
2173
|
+
"aria-label": label,
|
|
2174
|
+
className: s.pre,
|
|
2175
|
+
children: /* @__PURE__ */ jsx("code", { children: toJson(value) })
|
|
2176
|
+
})
|
|
2177
|
+
})]
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
//#endregion
|
|
2181
|
+
//#region src/ToolCallCard/ToolCallCard.tsx
|
|
2182
|
+
const VERB = {
|
|
2183
|
+
running: "toolCall.calling",
|
|
2184
|
+
succeeded: "toolCall.called",
|
|
2185
|
+
failed: "toolCall.failed"
|
|
2186
|
+
};
|
|
2187
|
+
/**
|
|
2188
|
+
* A tool invocation inside an assistant turn: name, status, duration, and a collapsible body
|
|
2189
|
+
* with the arguments and the result (or error) as small JSON views.
|
|
2190
|
+
*/
|
|
2191
|
+
function ToolCallCard({ name, status, durationMs, args, result, error, defaultOpen, open, onOpenChange, className, ref }) {
|
|
2192
|
+
const { t } = useI18n();
|
|
2193
|
+
const failed = status === "failed";
|
|
2194
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
2195
|
+
ref,
|
|
2196
|
+
defaultOpen: defaultOpen ?? failed,
|
|
2197
|
+
open,
|
|
2198
|
+
onOpenChange: onOpenChange ? (o) => onOpenChange(o) : void 0,
|
|
2199
|
+
"data-status": status,
|
|
2200
|
+
className: cn(toolCallCardStyles.root, className),
|
|
2201
|
+
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
2202
|
+
className: toolCallCardStyles.trigger,
|
|
2203
|
+
children: [
|
|
2204
|
+
/* @__PURE__ */ jsx("span", {
|
|
2205
|
+
className: cn(toolCallCardStyles.icon, failed && toolCallCardStyles.iconFailed),
|
|
2206
|
+
children: status === "running" ? /* @__PURE__ */ jsx(Spinner, { size: 13 }) : failed ? /* @__PURE__ */ jsx(CircleAlert, {
|
|
2207
|
+
size: 13,
|
|
2208
|
+
"aria-hidden": true
|
|
2209
|
+
}) : /* @__PURE__ */ jsx(Wrench, {
|
|
2210
|
+
size: 13,
|
|
2211
|
+
"aria-hidden": true
|
|
2212
|
+
})
|
|
2213
|
+
}),
|
|
2214
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2215
|
+
className: toolCallCardStyles.label,
|
|
2216
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2217
|
+
className: toolCallCardStyles.verb,
|
|
2218
|
+
children: t(VERB[status])
|
|
2219
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2220
|
+
className: toolCallCardStyles.name,
|
|
2221
|
+
children: name
|
|
2222
|
+
})]
|
|
2223
|
+
}),
|
|
2224
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2225
|
+
className: toolCallCardStyles.meta,
|
|
2226
|
+
children: [durationMs !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
2227
|
+
className: toolCallCardStyles.duration,
|
|
2228
|
+
children: formatDuration(durationMs)
|
|
2229
|
+
}), /* @__PURE__ */ jsx(StatusTag, {
|
|
2230
|
+
status,
|
|
2231
|
+
size: "sm"
|
|
2232
|
+
})]
|
|
2233
|
+
})
|
|
2234
|
+
]
|
|
2235
|
+
}), /* @__PURE__ */ jsx(CollapsiblePanel, {
|
|
2236
|
+
className: "pt-0",
|
|
2237
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2238
|
+
className: toolCallCardStyles.body,
|
|
2239
|
+
children: [
|
|
2240
|
+
args !== void 0 && /* @__PURE__ */ jsx(JsonView, {
|
|
2241
|
+
label: t("toolCall.arguments"),
|
|
2242
|
+
value: args
|
|
2243
|
+
}),
|
|
2244
|
+
failed && error && /* @__PURE__ */ jsx("p", {
|
|
2245
|
+
role: "alert",
|
|
2246
|
+
className: toolCallCardStyles.error,
|
|
2247
|
+
children: error
|
|
2248
|
+
}),
|
|
2249
|
+
result !== void 0 && /* @__PURE__ */ jsx(JsonView, {
|
|
2250
|
+
label: t("toolCall.result"),
|
|
2251
|
+
value: result
|
|
2252
|
+
}),
|
|
2253
|
+
status === "running" && result === void 0 && /* @__PURE__ */ jsx("p", {
|
|
2254
|
+
className: "text-[12.5px] text-muted-foreground",
|
|
2255
|
+
children: t("toolCall.waiting")
|
|
2256
|
+
})
|
|
2257
|
+
]
|
|
2258
|
+
})
|
|
2259
|
+
})]
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/ToolCallCard/ToolCallCard.doc.ts
|
|
2264
|
+
const doc$10 = {
|
|
2265
|
+
name: "ToolCallCard",
|
|
2266
|
+
group: "Display",
|
|
2267
|
+
status: "beta",
|
|
2268
|
+
description: "A tool invocation in an assistant turn: name, status (running, succeeded, failed), duration and a collapsible body with the arguments and the result or error as small collapsible JSON views. Failed calls open by default.",
|
|
2269
|
+
primitive: "@base-ui/react/collapsible",
|
|
2270
|
+
pattern: "disclosure",
|
|
2271
|
+
keyboard: [
|
|
2272
|
+
["Enter", "Expands or collapses the card (or a JSON section)"],
|
|
2273
|
+
["Space", "Expands or collapses the card (or a JSON section)"],
|
|
2274
|
+
["Tab", "Moves to the JSON sections and their scrollable code"]
|
|
2275
|
+
],
|
|
2276
|
+
tokens: [
|
|
2277
|
+
"card",
|
|
2278
|
+
"border",
|
|
2279
|
+
"secondary",
|
|
2280
|
+
"muted-foreground",
|
|
2281
|
+
"success-text",
|
|
2282
|
+
"destructive",
|
|
2283
|
+
"destructive-text",
|
|
2284
|
+
"focus-ring"
|
|
2285
|
+
]
|
|
2286
|
+
};
|
|
2287
|
+
//#endregion
|
|
2288
|
+
//#region src/ChatDemo/demoScript.ts
|
|
2289
|
+
/** Fixtures for ChatDemo: generic workspace data, no product names. */
|
|
2290
|
+
const DEMO_SUGGESTIONS = [
|
|
2291
|
+
{
|
|
2292
|
+
label: "Summarise deployments",
|
|
2293
|
+
description: "What shipped this week across projects"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
label: "Find unpaid invoices",
|
|
2297
|
+
description: "Invoices still open from last month"
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
label: "Review member access",
|
|
2301
|
+
description: "Who has admin rights"
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
label: "Explain a failed build",
|
|
2305
|
+
description: "Likely cause and a fix"
|
|
2306
|
+
}
|
|
2307
|
+
];
|
|
2308
|
+
const DEMO_SOURCES = [{
|
|
2309
|
+
title: "Deployment history · web-app",
|
|
2310
|
+
url: "https://app.example.com/projects/web-app/deployments",
|
|
2311
|
+
snippet: "12 deployments in the last 7 days, 1 rolled back."
|
|
2312
|
+
}, {
|
|
2313
|
+
title: "Rollback guide",
|
|
2314
|
+
url: "https://docs.example.com/deployments/rollback",
|
|
2315
|
+
snippet: "Roll back from the Deployments page or with the CLI."
|
|
2316
|
+
}];
|
|
2317
|
+
const DEMO_TOOL = {
|
|
2318
|
+
name: "list_deployments",
|
|
2319
|
+
args: {
|
|
2320
|
+
project: "web-app",
|
|
2321
|
+
since: "7d"
|
|
2322
|
+
},
|
|
2323
|
+
result: {
|
|
2324
|
+
count: 12,
|
|
2325
|
+
succeeded: 11,
|
|
2326
|
+
rolledBack: 1,
|
|
2327
|
+
latest: "v2.14.0"
|
|
2328
|
+
},
|
|
2329
|
+
durationMs: 640
|
|
2330
|
+
};
|
|
2331
|
+
const DEMO_REPLY = `Here is the summary for **web-app** over the last 7 days:
|
|
2332
|
+
|
|
2333
|
+
| Status | Count |
|
|
2334
|
+
| --- | --- |
|
|
2335
|
+
| Succeeded | 11 |
|
|
2336
|
+
| Rolled back | 1 |
|
|
2337
|
+
|
|
2338
|
+
The rollback was \`v2.13.2\`: a health check failed in one region, so traffic went back to the previous build.
|
|
2339
|
+
|
|
2340
|
+
To pin a release from the CLI:
|
|
2341
|
+
|
|
2342
|
+
\`\`\`bash
|
|
2343
|
+
deploy pin --project web-app --version v2.14.0
|
|
2344
|
+
\`\`\`
|
|
2345
|
+
|
|
2346
|
+
See the [rollback guide](https://docs.example.com/deployments/rollback) for the full steps.`;
|
|
2347
|
+
/** Splits text into small chunks to imitate token streaming. */
|
|
2348
|
+
function chunk(text, size = 6) {
|
|
2349
|
+
const out = [];
|
|
2350
|
+
for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size));
|
|
2351
|
+
return out;
|
|
2352
|
+
}
|
|
2353
|
+
//#endregion
|
|
2354
|
+
//#region src/ChatDemo/ChatDemo.tsx
|
|
2355
|
+
/**
|
|
2356
|
+
* End-to-end example: suggestions, a user turn with attachments, a tool call, a streamed
|
|
2357
|
+
* markdown reply with sources, Stop, Regenerate and feedback, and a compact model picker. Streaming is mocked with timers that are cleared on
|
|
2358
|
+
* Stop and on unmount.
|
|
2359
|
+
*/
|
|
2360
|
+
function ChatDemo({ withPanel = true, tickMs = 30, className }) {
|
|
2361
|
+
const [messages, setMessages] = useState([]);
|
|
2362
|
+
const [streaming, setStreaming] = useState(false);
|
|
2363
|
+
const [model, setModel] = useState("balanced");
|
|
2364
|
+
const [files, setFiles] = useState([]);
|
|
2365
|
+
const nextId = useRef(1);
|
|
2366
|
+
const timers = useRef([]);
|
|
2367
|
+
const objectUrls = useRef([]);
|
|
2368
|
+
const clearTimers = () => {
|
|
2369
|
+
timers.current.forEach((t) => clearTimeout(t));
|
|
2370
|
+
timers.current = [];
|
|
2371
|
+
};
|
|
2372
|
+
useEffect(() => () => {
|
|
2373
|
+
clearTimers();
|
|
2374
|
+
objectUrls.current.forEach((u) => URL.revokeObjectURL(u));
|
|
2375
|
+
}, []);
|
|
2376
|
+
const attach = (picked) => {
|
|
2377
|
+
const added = picked.map((f) => {
|
|
2378
|
+
let previewUrl;
|
|
2379
|
+
if (f.type.startsWith("image/") && typeof URL.createObjectURL === "function") {
|
|
2380
|
+
previewUrl = URL.createObjectURL(f);
|
|
2381
|
+
objectUrls.current.push(previewUrl);
|
|
2382
|
+
}
|
|
2383
|
+
return {
|
|
2384
|
+
id: `file-${nextId.current++}`,
|
|
2385
|
+
name: f.name,
|
|
2386
|
+
size: f.size,
|
|
2387
|
+
type: f.type,
|
|
2388
|
+
previewUrl
|
|
2389
|
+
};
|
|
2390
|
+
});
|
|
2391
|
+
setFiles((all) => [...all, ...added]);
|
|
2392
|
+
};
|
|
2393
|
+
const patch = (id, fn) => setMessages((ms) => ms.map((m) => m.id === id ? fn(m) : m));
|
|
2394
|
+
const respond = () => {
|
|
2395
|
+
clearTimers();
|
|
2396
|
+
const toolId = nextId.current++;
|
|
2397
|
+
const replyId = nextId.current++;
|
|
2398
|
+
const now = /* @__PURE__ */ new Date();
|
|
2399
|
+
setStreaming(true);
|
|
2400
|
+
setMessages((ms) => [
|
|
2401
|
+
...ms,
|
|
2402
|
+
{
|
|
2403
|
+
id: toolId,
|
|
2404
|
+
role: "tool",
|
|
2405
|
+
text: "",
|
|
2406
|
+
time: now,
|
|
2407
|
+
tool: { status: "running" }
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
id: replyId,
|
|
2411
|
+
role: "assistant",
|
|
2412
|
+
text: "",
|
|
2413
|
+
time: now,
|
|
2414
|
+
streaming: true
|
|
2415
|
+
}
|
|
2416
|
+
]);
|
|
2417
|
+
const pieces = chunk(DEMO_REPLY);
|
|
2418
|
+
const startAt = DEMO_TOOL.durationMs;
|
|
2419
|
+
timers.current.push(setTimeout(() => patch(toolId, (m) => ({
|
|
2420
|
+
...m,
|
|
2421
|
+
tool: {
|
|
2422
|
+
status: "succeeded",
|
|
2423
|
+
durationMs: DEMO_TOOL.durationMs
|
|
2424
|
+
}
|
|
2425
|
+
})), startAt));
|
|
2426
|
+
pieces.forEach((piece, i) => {
|
|
2427
|
+
timers.current.push(setTimeout(() => patch(replyId, (m) => ({
|
|
2428
|
+
...m,
|
|
2429
|
+
text: m.text + piece
|
|
2430
|
+
})), startAt + (i + 1) * tickMs));
|
|
2431
|
+
});
|
|
2432
|
+
timers.current.push(setTimeout(() => {
|
|
2433
|
+
patch(replyId, (m) => ({
|
|
2434
|
+
...m,
|
|
2435
|
+
streaming: false,
|
|
2436
|
+
sources: true
|
|
2437
|
+
}));
|
|
2438
|
+
setStreaming(false);
|
|
2439
|
+
}, startAt + (pieces.length + 1) * tickMs));
|
|
2440
|
+
};
|
|
2441
|
+
const send = (text, attachments = []) => {
|
|
2442
|
+
setMessages((ms) => [...ms, {
|
|
2443
|
+
id: nextId.current++,
|
|
2444
|
+
role: "user",
|
|
2445
|
+
text,
|
|
2446
|
+
time: /* @__PURE__ */ new Date(),
|
|
2447
|
+
attachments
|
|
2448
|
+
}]);
|
|
2449
|
+
setFiles([]);
|
|
2450
|
+
respond();
|
|
2451
|
+
};
|
|
2452
|
+
const stop = () => {
|
|
2453
|
+
clearTimers();
|
|
2454
|
+
setMessages((ms) => ms.map((m) => m.streaming ? {
|
|
2455
|
+
...m,
|
|
2456
|
+
streaming: false
|
|
2457
|
+
} : m.tool?.status === "running" ? {
|
|
2458
|
+
...m,
|
|
2459
|
+
tool: {
|
|
2460
|
+
status: "failed",
|
|
2461
|
+
durationMs: 0
|
|
2462
|
+
}
|
|
2463
|
+
} : m));
|
|
2464
|
+
setStreaming(false);
|
|
2465
|
+
};
|
|
2466
|
+
const retry = (id) => {
|
|
2467
|
+
setMessages((ms) => {
|
|
2468
|
+
const at = ms.findIndex((m) => m.id === id);
|
|
2469
|
+
const cut = at > 0 && ms[at - 1]?.role === "tool" ? at - 1 : at;
|
|
2470
|
+
return ms.slice(0, cut);
|
|
2471
|
+
});
|
|
2472
|
+
respond();
|
|
2473
|
+
};
|
|
2474
|
+
const lastReply = [...messages].reverse().find((m) => m.role === "assistant" && !m.streaming)?.text;
|
|
2475
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx("div", {
|
|
2476
|
+
className: cn("h-[640px] overflow-hidden rounded-xl border border-border", className),
|
|
2477
|
+
children: /* @__PURE__ */ jsxs(ChatLayout, {
|
|
2478
|
+
header: /* @__PURE__ */ jsx("div", {
|
|
2479
|
+
className: "px-4 py-3 text-[13px] font-semibold text-foreground",
|
|
2480
|
+
children: "New conversation"
|
|
2481
|
+
}),
|
|
2482
|
+
panel: withPanel ? /* @__PURE__ */ jsxs("div", {
|
|
2483
|
+
className: "flex flex-col gap-4 p-4 text-[13px]",
|
|
2484
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
|
|
2485
|
+
className: "font-semibold text-foreground",
|
|
2486
|
+
children: "Details"
|
|
2487
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
2488
|
+
className: "mt-1 text-muted-foreground",
|
|
2489
|
+
children: "Workspace assistant with read access to projects, deployments and invoices."
|
|
2490
|
+
})] }), /* @__PURE__ */ jsx(CitationList, {
|
|
2491
|
+
sources: DEMO_SOURCES,
|
|
2492
|
+
heading: "Context sources"
|
|
2493
|
+
})]
|
|
2494
|
+
}) : void 0,
|
|
2495
|
+
composer: /* @__PURE__ */ jsx(ChatComposer, {
|
|
2496
|
+
onSubmit: send,
|
|
2497
|
+
streaming,
|
|
2498
|
+
onStop: stop,
|
|
2499
|
+
maxLength: 4e3,
|
|
2500
|
+
attachments: files,
|
|
2501
|
+
onAttach: attach,
|
|
2502
|
+
onRemoveAttachment: (id) => setFiles((all) => all.filter((f) => f.id !== id)),
|
|
2503
|
+
accept: ".pdf,.csv,.md,.txt,.json,image/*",
|
|
2504
|
+
maxSize: 10485760,
|
|
2505
|
+
maxFiles: 5,
|
|
2506
|
+
modelPicker: /* @__PURE__ */ jsx(ModelPicker, {
|
|
2507
|
+
compact: true,
|
|
2508
|
+
models: DEMO_MODELS,
|
|
2509
|
+
value: model,
|
|
2510
|
+
onValueChange: setModel
|
|
2511
|
+
})
|
|
2512
|
+
}),
|
|
2513
|
+
children: [
|
|
2514
|
+
messages.length === 0 && /* @__PURE__ */ jsxs("div", {
|
|
2515
|
+
className: "flex flex-col gap-5 pt-10",
|
|
2516
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
2517
|
+
className: "text-center text-[20px] font-semibold tracking-tight text-foreground",
|
|
2518
|
+
children: "How can I help?"
|
|
2519
|
+
}), /* @__PURE__ */ jsx(SuggestionChips, {
|
|
2520
|
+
suggestions: DEMO_SUGGESTIONS,
|
|
2521
|
+
onSelect: (text) => send(text)
|
|
2522
|
+
})]
|
|
2523
|
+
}),
|
|
2524
|
+
messages.map((m) => {
|
|
2525
|
+
if (m.role === "tool" && m.tool) return /* @__PURE__ */ jsx(ChatMessage, {
|
|
2526
|
+
role: "tool",
|
|
2527
|
+
author: "Tool",
|
|
2528
|
+
timestamp: m.time,
|
|
2529
|
+
children: /* @__PURE__ */ jsx(ToolCallCard, {
|
|
2530
|
+
name: DEMO_TOOL.name,
|
|
2531
|
+
status: m.tool.status,
|
|
2532
|
+
durationMs: m.tool.durationMs,
|
|
2533
|
+
args: DEMO_TOOL.args,
|
|
2534
|
+
result: m.tool.status === "succeeded" ? DEMO_TOOL.result : void 0,
|
|
2535
|
+
error: m.tool.status === "failed" ? "Stopped by the user." : void 0
|
|
2536
|
+
})
|
|
2537
|
+
}, m.id);
|
|
2538
|
+
if (m.role === "assistant") return /* @__PURE__ */ jsxs(ChatMessage, {
|
|
2539
|
+
role: "assistant",
|
|
2540
|
+
timestamp: m.time,
|
|
2541
|
+
streaming: m.streaming,
|
|
2542
|
+
alwaysShowActions: m.feedback?.rating === "down",
|
|
2543
|
+
actions: /* @__PURE__ */ jsx(MessageFeedback, {
|
|
2544
|
+
copyText: m.text,
|
|
2545
|
+
onRegenerate: streaming ? void 0 : () => retry(m.id),
|
|
2546
|
+
value: m.feedback ?? { rating: null },
|
|
2547
|
+
onFeedback: (f) => patch(m.id, (x) => ({
|
|
2548
|
+
...x,
|
|
2549
|
+
feedback: f
|
|
2550
|
+
}))
|
|
2551
|
+
}),
|
|
2552
|
+
children: [m.text && /* @__PURE__ */ jsx(Markdown, {
|
|
2553
|
+
streaming: m.streaming,
|
|
2554
|
+
children: m.text
|
|
2555
|
+
}), m.sources && /* @__PURE__ */ jsx(CitationList, {
|
|
2556
|
+
sources: DEMO_SOURCES,
|
|
2557
|
+
className: "mt-3"
|
|
2558
|
+
})]
|
|
2559
|
+
}, m.id);
|
|
2560
|
+
return /* @__PURE__ */ jsxs(ChatMessage, {
|
|
2561
|
+
role: m.role,
|
|
2562
|
+
timestamp: m.time,
|
|
2563
|
+
children: [m.text, m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx(AttachmentList, {
|
|
2564
|
+
attachments: m.attachments,
|
|
2565
|
+
variant: "tile",
|
|
2566
|
+
className: "mt-2"
|
|
2567
|
+
})]
|
|
2568
|
+
}, m.id);
|
|
2569
|
+
}),
|
|
2570
|
+
/* @__PURE__ */ jsx(ChatAnnouncer, {
|
|
2571
|
+
streaming,
|
|
2572
|
+
lastReply
|
|
2573
|
+
})
|
|
2574
|
+
]
|
|
2575
|
+
})
|
|
2576
|
+
}) });
|
|
2577
|
+
}
|
|
2578
|
+
//#endregion
|
|
2579
|
+
//#region src/ChatDemo/ChatDemo.doc.ts
|
|
2580
|
+
const doc$2 = {
|
|
2581
|
+
name: "ChatDemo",
|
|
2582
|
+
group: "Layout",
|
|
2583
|
+
status: "experimental",
|
|
2584
|
+
description: "Reference composition of the chat pack: suggestions in the empty state, attachments (pick, paste or drop), a compact ModelPicker in the composer, a tool call, a streamed markdown reply with sources (TypingIndicator, caret), Stop, MessageFeedback (copy, regenerate, thumbs with reasons), and a resizable details panel. Streaming is mocked with timers that are cleaned up.",
|
|
2585
|
+
keyboard: [["Enter", "In the composer: sends; on a suggestion: sends it"], ["Escape", "In the composer while streaming: stops the reply"]],
|
|
2586
|
+
tokens: [
|
|
2587
|
+
"background",
|
|
2588
|
+
"card",
|
|
2589
|
+
"border",
|
|
2590
|
+
"primary",
|
|
2591
|
+
"focus-ring"
|
|
2592
|
+
]
|
|
2593
|
+
};
|
|
2594
|
+
//#endregion
|
|
2595
|
+
export { AttachmentChip, AttachmentList, ChatAnnouncer, ChatComposer, ChatDemo, ChatLayout, ChatMessage, Citation, CitationList, CodeBlock, DEMO_MODELS, JsonView, Markdown, MessageFeedback, ModelPicker, SuggestionChips, ToolCallCard, TypingIndicator, doc as attachmentListDoc, attachmentStatus, attachmentStyles, doc$1 as chatComposerDoc, chatComposerStyles, doc$2 as chatDemoDoc, doc$3 as chatLayoutDoc, chatLayoutStyles, doc$4 as chatMessageDoc, chatMessageStyles, doc$5 as citationDoc, citationStyles, groupModels, doc$6 as markdownDoc, markdownStyles, matchesAccept, doc$7 as messageFeedbackDoc, messageFeedbackStyles, doc$8 as modelPickerDoc, modelPickerStyles, repairStreamingMarkdown, safeSourceUrl, safeUrl, sourceHost, streamingCaret, streamingCaretClass, doc$9 as suggestionChipsDoc, suggestionChipsStyles, doc$10 as toolCallCardDoc, toolCallCardStyles, doc$11 as typingIndicatorDoc, typingIndicatorStyles, useChatScroll, validateFiles };
|
|
2596
|
+
|
|
2597
|
+
//# sourceMappingURL=index.js.map
|