@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.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