@epicbagel/saas-kit 1.19.0 → 1.21.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.
@@ -1,1071 +1,43 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import {
4
- Trace,
5
- TraceLine,
6
- TraceTitle
7
- } from "../chunk-Z45YKVAM.js";
8
- import {
9
- VoiceDictationButton
10
- } from "../chunk-ZGTPNSR7.js";
11
- import {
12
- VoiceSessionControls
13
- } from "../chunk-KF4X37DP.js";
14
- import {
15
- Suggestion,
16
- Suggestions
17
- } from "../chunk-IJG2B3Y2.js";
18
- import {
19
- Marker,
20
- MarkerContent
21
- } from "../chunk-IW4ZIXXG.js";
22
- import {
23
- MessageScroller,
24
- MessageScrollerButton,
25
- MessageScrollerContent,
26
- MessageScrollerItem,
27
- MessageScrollerProvider,
28
- MessageScrollerViewport
29
- } from "../chunk-OUZSIA5G.js";
4
+ Chat,
5
+ ChatCopyAction,
6
+ ChatInput,
7
+ ChatThread
8
+ } from "../chunk-C23Q4FRM.js";
9
+ import "../chunk-Z45YKVAM.js";
10
+ import "../chunk-ZGTPNSR7.js";
11
+ import "../chunk-KF4X37DP.js";
12
+ import "../chunk-IJG2B3Y2.js";
13
+ import "../chunk-IW4ZIXXG.js";
14
+ import "../chunk-OUZSIA5G.js";
30
15
  import "../chunk-36LNA4WM.js";
31
- import {
32
- Message,
33
- MessageAction,
34
- MessageActions,
35
- MessageContent
36
- } from "../chunk-H4XH2PPU.js";
16
+ import "../chunk-H4XH2PPU.js";
37
17
  import "../chunk-GA5SX7FE.js";
38
- import {
39
- InputGroup,
40
- InputGroupAddon,
41
- InputGroupTextarea
42
- } from "../chunk-QTPPNK6F.js";
18
+ import "../chunk-QTPPNK6F.js";
43
19
  import "../chunk-6FC65AZQ.js";
44
- import {
45
- FileDropArea,
46
- FileDropzone
47
- } from "../chunk-WHDHUYS6.js";
48
- import {
49
- LazyMarkdown
50
- } from "../chunk-VY5U4VGV.js";
20
+ import "../chunk-WHDHUYS6.js";
21
+ import "../chunk-VY5U4VGV.js";
51
22
  import "../chunk-INNGGLMZ.js";
52
- import {
53
- DropdownMenu,
54
- DropdownMenuContent,
55
- DropdownMenuItem,
56
- DropdownMenuTrigger
57
- } from "../chunk-V4LE2HWP.js";
23
+ import "../chunk-4HWCNRK4.js";
24
+ import "../chunk-V4LE2HWP.js";
58
25
  import "../chunk-ZRLUUZHK.js";
59
- import {
60
- Bubble,
61
- BubbleContent
62
- } from "../chunk-F366BHWH.js";
26
+ import "../chunk-F366BHWH.js";
63
27
  import "../chunk-EIY5LLSP.js";
64
28
  import "../chunk-NBUWGRQO.js";
65
- import {
66
- Attachment,
67
- AttachmentAction,
68
- AttachmentActions,
69
- AttachmentContent,
70
- AttachmentDescription,
71
- AttachmentGroup,
72
- AttachmentMedia,
73
- AttachmentTitle,
74
- AttachmentTrigger
75
- } from "../chunk-CYDICVH2.js";
76
- import {
77
- ActionDock
78
- } from "../chunk-42RTTHND.js";
29
+ import "../chunk-CYDICVH2.js";
30
+ import "../chunk-42RTTHND.js";
79
31
  import "../chunk-QMA3WVB7.js";
80
32
  import "../chunk-3OWGL4Q2.js";
81
33
  import "../chunk-5OF6D3GH.js";
82
- import {
83
- Button
84
- } from "../chunk-W326GKML.js";
85
- import {
86
- cn
87
- } from "../chunk-G6SRSWXB.js";
34
+ import "../chunk-W326GKML.js";
35
+ import "../chunk-G6SRSWXB.js";
88
36
  import "../chunk-GJ7S3L6R.js";
89
- import {
90
- useCopyToClipboard
91
- } from "../chunk-ZWZAAGQF.js";
37
+ import "../chunk-ZWZAAGQF.js";
92
38
  import "../chunk-RPSRCB5O.js";
93
- import {
94
- useIsMobile
95
- } from "../chunk-PIU5SSUV.js";
96
- import {
97
- formatRelativeLong
98
- } from "../chunk-CS7DM3NV.js";
99
-
100
- // src/primitives/chat.tsx
101
- import {
102
- forwardRef,
103
- useCallback,
104
- useEffect,
105
- useId,
106
- useImperativeHandle,
107
- useRef,
108
- useState
109
- } from "react";
110
- import {
111
- ArrowDown,
112
- ArrowRight,
113
- ArrowUp,
114
- ArrowUpRight,
115
- Check,
116
- Copy,
117
- Download,
118
- FileText,
119
- LockKeyhole,
120
- Paperclip,
121
- Plus,
122
- X
123
- } from "lucide-react";
124
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
125
- function offersActions(message) {
126
- if (message.status) return false;
127
- return message.actions ?? message.role === "assistant";
128
- }
129
- function useArrivingUserTurnId(messages) {
130
- const seen = useRef(null);
131
- seen.current ??= new Set(messages.map((m) => m.id));
132
- useEffect(() => {
133
- seen.current = new Set(messages.map((m) => m.id));
134
- }, [messages]);
135
- for (let i = messages.length - 1; i >= 0; i--) {
136
- const message = messages[i];
137
- if (message.role === "user" && message.status !== "running") {
138
- return seen.current.has(message.id) ? void 0 : message.id;
139
- }
140
- }
141
- return void 0;
142
- }
143
- function timelineMarkers(messages, now) {
144
- let previous = null;
145
- return messages.map((message) => {
146
- if (message.status === "running") return null;
147
- const relativeTime = formatRelativeLong(message.createdAt, now);
148
- const opens = relativeTime && relativeTime !== previous ? relativeTime : null;
149
- previous = relativeTime;
150
- return opens;
151
- });
152
- }
153
- function TimelineMarker({ label }) {
154
- return /* @__PURE__ */ jsx(Marker, { variant: "separator", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx(MarkerContent, { children: label }) });
155
- }
156
- function ChatRunningLine({
157
- label,
158
- align = "start",
159
- workspace
160
- }) {
161
- return /* @__PURE__ */ jsx(Message, { align, children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(Trace, { className: workspace ? void 0 : "px-3.5 py-2", children: /* @__PURE__ */ jsx(TraceLine, { status: "running", children: /* @__PURE__ */ jsx(TraceTitle, { children: label }) }) }) }) });
162
- }
163
- var ChatThread = forwardRef(
164
- function ChatThread2({
165
- messages,
166
- pending = false,
167
- pendingLabel = "Thinking\u2026",
168
- scrollButtonLabel = "Scroll to latest message",
169
- uploadingLabel,
170
- maxHeight = "46vh",
171
- appearance = "bubbles",
172
- footer,
173
- markdownComponents,
174
- onOpenAttachment,
175
- attachmentAction = "open",
176
- messageActions,
177
- className
178
- }, ref) {
179
- const markers = timelineMarkers(messages, /* @__PURE__ */ new Date());
180
- const anchorId = useArrivingUserTurnId(messages);
181
- const workspace = appearance === "workspace";
182
- return /* @__PURE__ */ jsx(MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ jsxs(
183
- MessageScroller,
184
- {
185
- ref,
186
- style: { maxHeight },
187
- className: cn("min-h-0", className),
188
- children: [
189
- /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(
190
- MessageScrollerContent,
191
- {
192
- className: cn(
193
- workspace ? "mx-auto w-full max-w-3xl gap-10 px-4 py-6 md:px-6 md:py-8" : "gap-2.5 px-px py-px"
194
- ),
195
- children: [
196
- messages.map((m, index) => {
197
- const isUser = m.role === "user";
198
- if (m.status === "running") {
199
- return /* @__PURE__ */ jsx(MessageScrollerItem, { children: /* @__PURE__ */ jsx(
200
- ChatRunningLine,
201
- {
202
- label: m.text || pendingLabel,
203
- align: isUser ? "end" : "start",
204
- workspace
205
- }
206
- ) }, m.id);
207
- }
208
- const isFullWidth = m.width === "full";
209
- const marker = markers[index];
210
- const actions = messageActions && offersActions(m) ? messageActions(m) : null;
211
- return /* @__PURE__ */ jsxs(
212
- MessageScrollerItem,
213
- {
214
- messageId: m.id,
215
- scrollAnchor: m.id === anchorId,
216
- className: cn("flex flex-col", workspace ? "gap-5" : "gap-3"),
217
- children: [
218
- marker ? /* @__PURE__ */ jsx(TimelineMarker, { label: marker }) : null,
219
- /* @__PURE__ */ jsx(Message, { align: isUser ? "end" : "start", children: /* @__PURE__ */ jsxs(MessageContent, { children: [
220
- (m.content || m.text) && /* @__PURE__ */ jsx(
221
- Bubble,
222
- {
223
- variant: workspace ? isUser ? "muted" : "ghost" : isUser ? "default" : "muted",
224
- align: isUser ? "end" : "start",
225
- className: isFullWidth ? "w-full" : void 0,
226
- children: /* @__PURE__ */ jsx(
227
- BubbleContent,
228
- {
229
- className: cn(
230
- isFullWidth && "w-full",
231
- workspace ? isUser ? "rounded-2xl px-4 py-2.5 text-[15px] leading-7" : "px-0 py-0 text-[15px] leading-7" : "px-3.5 py-2 text-sm leading-relaxed"
232
- ),
233
- children: m.content ?? (m.text ? /* @__PURE__ */ jsx(LazyMarkdown, { size: workspace ? "base" : "sm", components: markdownComponents, children: m.text }) : null)
234
- }
235
- )
236
- }
237
- ),
238
- m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx(
239
- ChatAttachmentList,
240
- {
241
- attachments: m.attachments,
242
- onOpen: onOpenAttachment,
243
- openAction: attachmentAction,
244
- uploadingLabel,
245
- className: "px-0 pt-0"
246
- }
247
- ),
248
- actions != null && actions !== false ? (
249
- // Unboxed workspace turns pull the row out by the
250
- // button's inset, so the first icon lines up with
251
- // the text above it.
252
- /* @__PURE__ */ jsx(
253
- MessageActions,
254
- {
255
- className: cn(
256
- workspace && (isUser ? "-me-2 pointer-coarse:-me-3.5" : "-ms-2 pointer-coarse:-ms-3.5")
257
- ),
258
- children: actions
259
- }
260
- )
261
- ) : null
262
- ] }) })
263
- ]
264
- },
265
- m.id
266
- );
267
- }),
268
- pending && /* @__PURE__ */ jsx(MessageScrollerItem, { children: /* @__PURE__ */ jsx(ChatRunningLine, { label: pendingLabel, workspace }) }),
269
- footer && // Always painted: content-visibility:auto would contain the
270
- // footer's paint and layout, clipping a ring or shadow at its
271
- // edge and re-parenting anything fixed inside it.
272
- /* @__PURE__ */ jsx(MessageScrollerItem, { className: "[content-visibility:visible]", children: /* @__PURE__ */ jsx(Message, { align: "start", children: /* @__PURE__ */ jsx(MessageContent, { children: footer }) }) })
273
- ]
274
- }
275
- ) }),
276
- /* @__PURE__ */ jsxs(MessageScrollerButton, { variant: "outline", className: "rounded-full shadow-sm", children: [
277
- /* @__PURE__ */ jsx(ArrowDown, { className: "h-4 w-4" }),
278
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: scrollButtonLabel })
279
- ] })
280
- ]
281
- }
282
- ) });
283
- }
284
- );
285
- function ChatCopyAction({
286
- message,
287
- label = "Copy",
288
- copiedLabel = "Copied",
289
- onCopy,
290
- resetAfter = 2e3
291
- }) {
292
- const { copied, copy } = useCopyToClipboard({ resetAfter });
293
- const text = message.text;
294
- if (!text) return null;
295
- return /* @__PURE__ */ jsx(
296
- MessageAction,
297
- {
298
- tooltip: copied ? copiedLabel : label,
299
- "data-copied": copied || void 0,
300
- onClick: async () => {
301
- const ok = await copy(text);
302
- onCopy?.(ok);
303
- },
304
- children: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {})
305
- }
306
- );
307
- }
308
- function formatFileSize(size) {
309
- if (size === void 0) return "File";
310
- if (size < 1024) return `${size} B`;
311
- if (size < 1048576) return `${Math.round(size / 1024)} KB`;
312
- return `${(size / 1048576).toFixed(1)} MB`;
313
- }
314
- var ATTACHMENT_ACTIONS = {
315
- open: { verb: "Open", icon: ArrowUpRight },
316
- download: { verb: "Download", icon: Download }
317
- };
318
- var COMPOSER_SIZES = {
319
- sm: { minHeight: "min-h-12", rows: 2 },
320
- md: { minHeight: "min-h-[4.5rem]", rows: 2 },
321
- lg: { minHeight: "min-h-32", rows: 3 }
322
- };
323
- function ChatAttachmentList({
324
- attachments,
325
- onRemove,
326
- onOpen,
327
- openAction = "open",
328
- uploadingLabel = "Uploading\u2026",
329
- className
330
- }) {
331
- const { verb, icon: ActionIcon } = ATTACHMENT_ACTIONS[openAction];
332
- return /* @__PURE__ */ jsx(AttachmentGroup, { className: cn("gap-2 px-3 pt-3", className), children: attachments.map((attachment) => {
333
- const state = attachment.state ?? "done";
334
- const opens = Boolean(onOpen) && state === "done";
335
- return /* @__PURE__ */ jsxs(
336
- Attachment,
337
- {
338
- state,
339
- size: "sm",
340
- className: "min-w-[12rem] rounded-xl p-2",
341
- children: [
342
- opens && /* @__PURE__ */ jsx(
343
- AttachmentTrigger,
344
- {
345
- "aria-label": `${verb} ${attachment.name}`,
346
- title: `${verb} ${attachment.name}`,
347
- onClick: () => onOpen?.(attachment),
348
- className: "rounded-xl focus-visible:ring-2 focus-visible:ring-ring/50"
349
- }
350
- ),
351
- /* @__PURE__ */ jsx(AttachmentMedia, { variant: "icon", className: "h-9 w-9 rounded-lg bg-muted", children: /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" }) }),
352
- /* @__PURE__ */ jsxs(AttachmentContent, { className: "px-2", children: [
353
- /* @__PURE__ */ jsx(AttachmentTitle, { className: "text-xs font-medium", children: attachment.name }),
354
- /* @__PURE__ */ jsx(AttachmentDescription, { className: "text-[11px]", children: state === "uploading" ? uploadingLabel : opens ? `${formatFileSize(attachment.size)} \xB7 ${verb}` : formatFileSize(attachment.size) })
355
- ] }),
356
- onRemove ? /* @__PURE__ */ jsx(AttachmentActions, { children: /* @__PURE__ */ jsx(
357
- AttachmentAction,
358
- {
359
- type: "button",
360
- "aria-label": `Remove ${attachment.name}`,
361
- onClick: () => onRemove(attachment),
362
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
363
- }
364
- ) }) : opens ? (
365
- // The trigger covers the whole file; the icon only says what it does.
366
- /* @__PURE__ */ jsx(AttachmentActions, { "aria-hidden": true, className: "pointer-events-none pr-1 text-muted-foreground", children: /* @__PURE__ */ jsx(ActionIcon, { className: "h-4 w-4" }) })
367
- ) : null
368
- ]
369
- },
370
- attachment.id
371
- );
372
- }) });
373
- }
374
- function ChatInput({
375
- value,
376
- onChange,
377
- onSend,
378
- pending = false,
379
- disabled = false,
380
- placeholder = "Type your message\u2026",
381
- size = "md",
382
- uploadingLabel,
383
- voice = true,
384
- voiceSession,
385
- voiceTitle,
386
- voiceLang,
387
- submitLabel,
388
- autoFocus = false,
389
- className,
390
- inputRef,
391
- attachments = [],
392
- onFilesSelected,
393
- onRemoveAttachment,
394
- accept,
395
- multiple = true,
396
- addFilesLabel = "Add files",
397
- appearance = "default",
398
- compactOnBlur = false,
399
- helperText,
400
- privacyNote
401
- }) {
402
- const internalRef = useRef(null);
403
- const ref = inputRef ?? internalRef;
404
- const fileInputRef = useRef(null);
405
- const [editing, setEditing] = useState(false);
406
- const focusVersion = useRef(0);
407
- const isDisabled = pending || disabled;
408
- const hasText = Boolean(value.trim());
409
- const attachmentsReady = attachments.length > 0 && attachments.every((item) => item.state !== "uploading");
410
- const canSubmit = !isDisabled && (hasText || attachmentsReady);
411
- const liveVoiceActive = voiceSession && voiceSession.state !== "disconnected" && voiceSession.state !== "error";
412
- const submit = () => {
413
- const trimmed = value.trim();
414
- if (!trimmed && !attachmentsReady || isDisabled) return;
415
- onSend(trimmed, attachments);
416
- };
417
- const spacious = appearance === "spacious";
418
- const composerSize = COMPOSER_SIZES[size];
419
- const compact = compactOnBlur && !editing && value.length === 0 && attachments.length === 0;
420
- return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-input-container", className: "w-full", children: [
421
- /* @__PURE__ */ jsxs(
422
- "form",
423
- {
424
- "data-slot": "chat-input",
425
- "data-appearance": appearance,
426
- "data-compact": compact || void 0,
427
- className: "w-full",
428
- onFocusCapture: (event) => {
429
- focusVersion.current += 1;
430
- if (event.nativeEvent.target === ref.current) setEditing(true);
431
- },
432
- onBlurCapture: (event) => {
433
- if (event.currentTarget.contains(event.relatedTarget)) return;
434
- const form = event.currentTarget;
435
- const blurredAt = ++focusVersion.current;
436
- queueMicrotask(() => {
437
- if (form.isConnected && focusVersion.current === blurredAt) setEditing(false);
438
- });
439
- },
440
- onSubmit: (event) => {
441
- if (event.target !== event.currentTarget) return;
442
- event.preventDefault();
443
- submit();
444
- },
445
- children: [
446
- onFilesSelected && /* @__PURE__ */ jsx(
447
- "input",
448
- {
449
- ref: fileInputRef,
450
- type: "file",
451
- accept,
452
- multiple,
453
- hidden: true,
454
- onChange: (event) => {
455
- const files = Array.from(event.currentTarget.files ?? []);
456
- if (files.length > 0) void onFilesSelected(files);
457
- event.currentTarget.value = "";
458
- }
459
- }
460
- ),
461
- /* @__PURE__ */ jsxs(
462
- InputGroup,
463
- {
464
- className: cn(
465
- // Opaque in both modes: the input group's own dark:bg-input/30
466
- // would let the thread show through the composer.
467
- "overflow-hidden bg-background dark:bg-background",
468
- spacious ? "rounded-3xl" : "rounded-lg",
469
- className
470
- ),
471
- children: [
472
- liveVoiceActive && /* @__PURE__ */ jsx(InputGroupAddon, { align: "block-start", className: "w-full cursor-default border-b border-border p-0 [.border-b]:pb-0", children: /* @__PURE__ */ jsx(VoiceSessionControls, { ...voiceSession }) }),
473
- attachments.length > 0 && /* @__PURE__ */ jsx(
474
- ChatAttachmentList,
475
- {
476
- attachments,
477
- onRemove: onRemoveAttachment,
478
- uploadingLabel,
479
- className: "w-full"
480
- }
481
- ),
482
- /* @__PURE__ */ jsx(
483
- InputGroupTextarea,
484
- {
485
- ref,
486
- value,
487
- onChange: (e) => onChange(e.target.value),
488
- onKeyDown: (e) => {
489
- if (e.key !== "Enter" || e.shiftKey) return;
490
- if (e.nativeEvent.isComposing || e.keyCode === 229) return;
491
- e.preventDefault();
492
- submit();
493
- },
494
- placeholder,
495
- rows: compact ? 1 : composerSize.rows,
496
- disabled: isDisabled,
497
- autoFocus,
498
- className: cn(
499
- compact ? "min-h-[44px] max-h-24 min-w-0" : composerSize.minHeight,
500
- // Grows with the text, then scrolls rather than pushing the thread away.
501
- "leading-relaxed disabled:opacity-70 placeholder:text-sm",
502
- !compact && "max-h-48",
503
- compact ? "px-4 py-2" : spacious ? "px-6 pb-2 pt-6" : "px-4 py-3.5"
504
- )
505
- }
506
- ),
507
- /* @__PURE__ */ jsxs(
508
- InputGroupAddon,
509
- {
510
- align: "block-end",
511
- "data-slot": "chat-input-actions",
512
- onClick: (event) => {
513
- if (!event.currentTarget.contains(event.target)) return;
514
- if (!event.target.closest("button")) ref.current?.focus();
515
- },
516
- className: cn(
517
- compact ? "gap-1 px-2 pb-2 pt-0" : "gap-2",
518
- !compact && (spacious ? "px-5 pb-5 pt-1" : "border-t border-border px-3 py-2.5")
519
- ),
520
- children: [
521
- onFilesSelected && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
522
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
523
- Button,
524
- {
525
- type: "button",
526
- size: spacious && !compact ? "icon-lg" : "icon",
527
- variant: "ghost",
528
- "aria-label": "Add to conversation",
529
- className: compactOnBlur ? "min-h-[44px] min-w-[44px] shrink-0" : void 0,
530
- children: /* @__PURE__ */ jsx(Plus, { className: cn(spacious && "h-5 w-5") })
531
- }
532
- ) }),
533
- /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => fileInputRef.current?.click(), children: [
534
- /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" }),
535
- addFilesLabel
536
- ] }) })
537
- ] }),
538
- helperText ? /* @__PURE__ */ jsx(
539
- "span",
540
- {
541
- "data-slot": "chat-input-helper",
542
- className: cn("min-w-0 flex-1 truncate px-1 text-center text-xs font-normal text-muted-foreground sm:px-2 sm:text-sm", compact && "hidden"),
543
- children: helperText
544
- }
545
- ) : null,
546
- (!helperText || compact) && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
547
- voice && /* @__PURE__ */ jsx(
548
- VoiceDictationButton,
549
- {
550
- size: "icon",
551
- title: voiceTitle,
552
- lang: voiceLang,
553
- disabled: Boolean(isDisabled || liveVoiceActive),
554
- onCommit: (text) => onSend(text),
555
- className: cn(
556
- spacious && "border-transparent bg-transparent shadow-none hover:bg-muted",
557
- spacious && !compact && "h-12 w-12",
558
- compactOnBlur && "min-h-[44px] min-w-[44px] shrink-0"
559
- )
560
- }
561
- ),
562
- voiceSession && !liveVoiceActive && !hasText && attachments.length === 0 ? /* @__PURE__ */ jsx(VoiceSessionControls, { ...voiceSession, disabled: voiceSession.disabled || isDisabled }) : submitLabel ? /* @__PURE__ */ jsxs(
563
- Button,
564
- {
565
- type: "submit",
566
- disabled: !canSubmit,
567
- "data-chat-action": "submit",
568
- className: cn(spacious && "rounded-xl", compactOnBlur && "min-h-[44px] min-w-[44px] shrink-0"),
569
- children: [
570
- submitLabel,
571
- /* @__PURE__ */ jsx(ArrowRight, { className: "ml-2 h-4 w-4" })
572
- ]
573
- }
574
- ) : /* @__PURE__ */ jsx(
575
- Button,
576
- {
577
- type: "submit",
578
- size: !voiceSession && spacious && !compact ? "icon-lg" : "icon",
579
- shape: voiceSession ? "circle" : void 0,
580
- disabled: !canSubmit,
581
- "aria-label": "Send",
582
- "data-chat-action": "submit",
583
- className: cn(
584
- spacious && !voiceSession && "rounded-xl",
585
- (voiceSession || compactOnBlur) && "min-h-[44px] min-w-[44px] shrink-0"
586
- ),
587
- children: /* @__PURE__ */ jsx(ArrowUp, { className: cn(spacious && "h-5 w-5") })
588
- }
589
- )
590
- ]
591
- }
592
- )
593
- ]
594
- }
595
- )
596
- ]
597
- }
598
- ),
599
- privacyNote ? /* @__PURE__ */ jsxs(
600
- "div",
601
- {
602
- "data-slot": "chat-input-privacy",
603
- className: "mt-3 flex items-center justify-center gap-2 px-4 text-center text-xs text-muted-foreground sm:text-sm",
604
- children: [
605
- /* @__PURE__ */ jsx(LockKeyhole, { "aria-hidden": true, className: "h-4 w-4 shrink-0" }),
606
- /* @__PURE__ */ jsx("span", { children: privacyNote })
607
- ]
608
- }
609
- ) : null
610
- ] });
611
- }
612
- function VoiceHero({
613
- onSubmit,
614
- voice,
615
- voiceSession,
616
- voiceTitle,
617
- voiceLang,
618
- onSwitchToText,
619
- label,
620
- switchLabel
621
- }) {
622
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-5 py-6", children: [
623
- voiceSession ? /* @__PURE__ */ jsxs(Fragment, { children: [
624
- /* @__PURE__ */ jsx(VoiceSessionControls, { ...voiceSession, size: "hero" }),
625
- voice && /* @__PURE__ */ jsx(
626
- VoiceDictationButton,
627
- {
628
- size: "icon",
629
- title: voiceTitle,
630
- lang: voiceLang,
631
- onCommit: onSubmit,
632
- disabled: Boolean(voiceSession.disabled || voiceSession.state !== "disconnected" && voiceSession.state !== "error")
633
- }
634
- )
635
- ] }) : /* @__PURE__ */ jsx(
636
- VoiceDictationButton,
637
- {
638
- size: "lg",
639
- title: voiceTitle,
640
- lang: voiceLang,
641
- onCommit: onSubmit,
642
- className: "h-24 w-24 rounded-full p-0 shadow-lg transition-transform hover:scale-105 active:scale-95 [&_svg]:size-7"
643
- }
644
- ),
645
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1.5", children: [
646
- /* @__PURE__ */ jsx("span", { className: "text-base font-medium text-foreground", children: label }),
647
- /* @__PURE__ */ jsx(
648
- "button",
649
- {
650
- type: "button",
651
- onClick: onSwitchToText,
652
- className: "text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-4 decoration-muted-foreground/40",
653
- children: switchLabel
654
- }
655
- )
656
- ] })
657
- ] });
658
- }
659
- var EXAMPLE_PROMPT_LAYOUTS = {
660
- chips: "wrap",
661
- row: "row",
662
- cards: "grid"
663
- };
664
- function ChatSuggestions({
665
- slot,
666
- items,
667
- onSelect,
668
- label,
669
- showLabel = true,
670
- variant = "chips",
671
- disabled = false,
672
- className
673
- }) {
674
- const labelId = useId();
675
- if (items.length === 0) return null;
676
- const cards = variant === "cards";
677
- return /* @__PURE__ */ jsxs("div", { "data-slot": slot, "data-variant": variant, className, children: [
678
- showLabel && /* @__PURE__ */ jsx("p", { id: labelId, "data-slot": `${slot}-label`, className: "mb-2 text-left text-xs text-muted-foreground", children: label }),
679
- /* @__PURE__ */ jsx(
680
- Suggestions,
681
- {
682
- role: "group",
683
- "aria-labelledby": showLabel ? labelId : void 0,
684
- "aria-label": showLabel ? void 0 : label,
685
- layout: EXAMPLE_PROMPT_LAYOUTS[variant],
686
- children: items.map((item) => /* @__PURE__ */ jsx(
687
- Suggestion,
688
- {
689
- suggestion: item,
690
- onClick: onSelect,
691
- disabled,
692
- className: cards ? "h-auto min-h-12 px-3 py-2.5 text-xs" : void 0
693
- },
694
- item
695
- ))
696
- }
697
- )
698
- ] });
699
- }
700
- function ExamplePromptsLoading({ label, className }) {
701
- const list = Array.isArray(label) ? label : [label];
702
- const [idx, setIdx] = useState(0);
703
- useEffect(() => {
704
- if (list.length <= 1) return;
705
- const id = setInterval(() => setIdx((i) => (i + 1) % list.length), 1800);
706
- return () => clearInterval(id);
707
- }, [list.length]);
708
- return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-examples", "data-state": "loading", "aria-busy": "true", className: cn("mt-3", className), children: [
709
- /* @__PURE__ */ jsx(Trace, { className: "mb-2", children: /* @__PURE__ */ jsx(TraceLine, { status: "running", className: "text-xs", children: /* @__PURE__ */ jsx(TraceTitle, { className: "animate-in fade-in duration-500", children: list[idx] }, idx) }) }),
710
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "flex gap-2 overflow-hidden", children: [36, 44, 28, 40].map((w, i) => /* @__PURE__ */ jsx(
711
- "span",
712
- {
713
- className: "h-11 shrink-0 rounded-md bg-muted animate-pulse",
714
- style: {
715
- width: `${w * 4}px`,
716
- animationDelay: `${i * 120}ms`
717
- }
718
- },
719
- i
720
- )) })
721
- ] });
722
- }
723
- function ChatError({ children, className }) {
724
- return /* @__PURE__ */ jsx("p", { "data-slot": "chat-error", role: "alert", className: cn("mt-2 text-xs text-destructive", className), children });
725
- }
726
- var Chat = forwardRef(function Chat2({
727
- messages,
728
- onSend,
729
- onSendError,
730
- pending = false,
731
- pendingLabel,
732
- emptyPendingContent,
733
- error = null,
734
- emptyTitle,
735
- emptySubtitle,
736
- examplePrompts = [],
737
- examplePromptsLabel = "Try",
738
- examplePromptsVariant = "chips",
739
- examplePromptsLoading = false,
740
- examplePromptsLoadingLabel = "Loading suggestions\u2026",
741
- replySuggestions = [],
742
- replySuggestionsLabel = "Suggested replies",
743
- compactEmptyState = false,
744
- defaultMode,
745
- placeholder = "Type your message\u2026",
746
- activePlaceholder = "Reply\u2026",
747
- submitLabel,
748
- attachments = [],
749
- onFilesSelected,
750
- onRemoveAttachment,
751
- attachmentAccept,
752
- addFilesLabel,
753
- uploadingLabel,
754
- composerAppearance = "default",
755
- mobileCompactComposer = false,
756
- composerHelperText,
757
- composerPrivacyNote,
758
- emptyFileDropzone = false,
759
- emptyFileDropzoneLabel = "Drop files here, or choose them",
760
- emptyFileDropzoneDescription,
761
- fileDrop = "conversation",
762
- fileDropLabel,
763
- fileDropDescription,
764
- voice = true,
765
- voiceSession,
766
- voiceTitle,
767
- activeVoiceTitle,
768
- voiceLang,
769
- voiceHeroLabel = "Tap to speak",
770
- voiceHeroSwitchLabel = "or type instead",
771
- threadMaxHeight = "46vh",
772
- scrollButtonLabel,
773
- emptyAlign = "start",
774
- emptyIcon,
775
- emptyFooter,
776
- layout = "embedded",
777
- threadFooter,
778
- markdownComponents,
779
- onOpenAttachment,
780
- attachmentAction,
781
- messageActions,
782
- className
783
- }, ref) {
784
- const isMobile = useIsMobile();
785
- const [input, setInput] = useState("");
786
- const inputRef = useRef(null);
787
- const conversationStarted = messages.length > 0;
788
- const [userMode, setUserMode] = useState(null);
789
- const mode = !voice && !voiceSession ? "text" : userMode ?? defaultMode ?? (isMobile ? "voice" : "text");
790
- const reportSendError = useCallback(
791
- (cause) => {
792
- if (onSendError) onSendError(cause);
793
- else if (typeof globalThis.reportError === "function") globalThis.reportError(cause);
794
- else console.error(cause);
795
- },
796
- [onSendError]
797
- );
798
- const send = useCallback(
799
- async (text, sentAttachments = attachments) => {
800
- const trimmed = text.trim();
801
- if (!trimmed && sentAttachments.length === 0 || pending) return;
802
- setInput("");
803
- try {
804
- await onSend(trimmed, sentAttachments);
805
- } catch (cause) {
806
- if (trimmed) {
807
- setInput((current) => current ? current : trimmed);
808
- if (mode === "voice") setUserMode("text");
809
- }
810
- reportSendError(cause);
811
- }
812
- window.setTimeout(() => inputRef.current?.focus(), 0);
813
- },
814
- [attachments, mode, onSend, pending, reportSendError]
815
- );
816
- const sendSuggestion = useCallback(
817
- async (text) => {
818
- const trimmed = text.trim();
819
- if (!trimmed || pending) return;
820
- try {
821
- await onSend(trimmed, []);
822
- } catch (cause) {
823
- reportSendError(cause);
824
- }
825
- window.setTimeout(() => inputRef.current?.focus(), 0);
826
- },
827
- [onSend, pending, reportSendError]
828
- );
829
- useImperativeHandle(
830
- ref,
831
- () => ({
832
- focusInput: () => inputRef.current?.focus(),
833
- clearInput: () => setInput("")
834
- }),
835
- []
836
- );
837
- const dropFiles = useCallback(
838
- (files) => {
839
- if (!conversationStarted && mode === "voice") setUserMode("text");
840
- return onFilesSelected?.(files);
841
- },
842
- [conversationStarted, mode, onFilesSelected]
843
- );
844
- const application = layout === "application";
845
- const workspace = layout === "workspace";
846
- const showEmptyFileDropzone = emptyFileDropzone && Boolean(onFilesSelected) && attachments.length === 0;
847
- const liveVoiceActive = voiceSession && voiceSession.state !== "disconnected" && voiceSession.state !== "error";
848
- const showEmptyPending = !conversationStarted && pending && Boolean(emptyPendingContent) && !liveVoiceActive;
849
- return /* @__PURE__ */ jsx(
850
- FileDropArea,
851
- {
852
- "data-slot": "chat",
853
- "data-layout": layout,
854
- onFiles: onFilesSelected ? dropFiles : void 0,
855
- disabled: fileDrop === false || showEmptyPending,
856
- scope: fileDrop === "page" ? "page" : "area",
857
- label: fileDropLabel,
858
- description: fileDropDescription,
859
- className: cn(
860
- "w-full",
861
- application && "flex min-h-[calc(100svh-4rem)] flex-1 flex-col md:block md:min-h-0",
862
- workspace && "flex h-full min-h-0 flex-1 flex-col overflow-hidden",
863
- className
864
- ),
865
- children: showEmptyPending ? /* @__PURE__ */ jsx(
866
- "div",
867
- {
868
- "data-slot": "chat-empty-pending",
869
- className: "flex min-h-0 flex-1 items-center justify-center px-4 py-8",
870
- children: emptyPendingContent
871
- }
872
- ) : !conversationStarted ? /* @__PURE__ */ jsxs(
873
- "div",
874
- {
875
- "data-slot": "chat-empty",
876
- className: cn(
877
- emptyAlign === "center" && "mx-auto flex w-full max-w-3xl flex-col items-center text-center",
878
- application && "min-h-0 max-w-none flex-1 md:max-w-3xl",
879
- workspace && "min-h-0 max-w-none flex-1 overflow-y-auto overscroll-contain px-4 pb-20"
880
- ),
881
- children: [
882
- /* @__PURE__ */ jsxs(
883
- "div",
884
- {
885
- "data-slot": "chat-empty-intro",
886
- className: cn(
887
- application && "flex flex-1 flex-col items-center justify-center px-6 py-8 md:contents",
888
- workspace && "mt-auto w-full flex-none px-0 py-0"
889
- ),
890
- children: [
891
- !compactEmptyState && emptyIcon && /* @__PURE__ */ jsx("div", { "data-slot": "chat-empty-icon", className: "mb-4", children: emptyIcon }),
892
- emptyTitle && !compactEmptyState && /* @__PURE__ */ jsx("h2", { "data-slot": "chat-empty-title", className: "text-3xl md:text-4xl font-medium tracking-tight leading-[1.1] mt-2 mb-3", children: emptyTitle }),
893
- emptySubtitle && !compactEmptyState && /* @__PURE__ */ jsx(
894
- "p",
895
- {
896
- "data-slot": "chat-empty-subtitle",
897
- className: cn(
898
- "text-base text-muted-foreground leading-relaxed mb-6 max-w-md",
899
- emptyAlign === "center" && "mx-auto"
900
- ),
901
- children: emptySubtitle
902
- }
903
- )
904
- ]
905
- }
906
- ),
907
- /* @__PURE__ */ jsxs(
908
- ActionDock,
909
- {
910
- shrink: false,
911
- className: cn(
912
- application && "px-4 py-3 md:border-0 md:bg-transparent md:p-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
913
- !application && "border-0 bg-transparent p-0 backdrop-blur-none",
914
- workspace && "mb-auto mt-2 w-full max-w-3xl border-0 bg-transparent p-0 backdrop-blur-none supports-[backdrop-filter]:bg-transparent"
915
- ),
916
- children: [
917
- showEmptyFileDropzone && onFilesSelected && /* @__PURE__ */ jsx(
918
- FileDropzone,
919
- {
920
- multiple: true,
921
- onFiles: onFilesSelected,
922
- accept: attachmentAccept,
923
- label: emptyFileDropzoneLabel,
924
- description: emptyFileDropzoneDescription,
925
- icon: /* @__PURE__ */ jsx(FileText, {}),
926
- className: "mb-4 rounded-lg bg-muted/20 py-8"
927
- }
928
- ),
929
- mode === "voice" ? /* @__PURE__ */ jsx(
930
- VoiceHero,
931
- {
932
- onSubmit: (text) => void send(text),
933
- voice,
934
- voiceSession: voiceSession ? { ...voiceSession, disabled: voiceSession.disabled || pending } : void 0,
935
- voiceTitle,
936
- voiceLang,
937
- label: voiceHeroLabel,
938
- switchLabel: voiceHeroSwitchLabel,
939
- onSwitchToText: () => setUserMode("text")
940
- }
941
- ) : /* @__PURE__ */ jsx(
942
- ChatInput,
943
- {
944
- value: input,
945
- onChange: setInput,
946
- onSend: (text) => void send(text),
947
- pending,
948
- placeholder,
949
- size: showEmptyFileDropzone || application && isMobile ? "md" : "lg",
950
- voice,
951
- voiceSession,
952
- voiceTitle,
953
- voiceLang,
954
- submitLabel,
955
- autoFocus: !isMobile,
956
- inputRef,
957
- attachments,
958
- onFilesSelected,
959
- onRemoveAttachment,
960
- accept: attachmentAccept,
961
- addFilesLabel,
962
- uploadingLabel,
963
- appearance: composerAppearance,
964
- helperText: composerHelperText,
965
- privacyNote: composerPrivacyNote,
966
- className: workspace ? "rounded-3xl shadow-sm" : void 0
967
- }
968
- ),
969
- error && /* @__PURE__ */ jsx(ChatError, { children: error }),
970
- !pending && examplePromptsLoading && /* @__PURE__ */ jsx(ExamplePromptsLoading, { label: examplePromptsLoadingLabel }),
971
- !pending && !examplePromptsLoading && examplePrompts.length > 0 && /* @__PURE__ */ jsx(
972
- ChatSuggestions,
973
- {
974
- slot: "chat-examples",
975
- items: examplePrompts,
976
- onSelect: (prompt) => void sendSuggestion(prompt),
977
- label: examplePromptsLabel,
978
- variant: examplePromptsVariant,
979
- className: examplePromptsVariant === "cards" ? "mt-4" : "mt-3"
980
- }
981
- ),
982
- emptyFooter && /* @__PURE__ */ jsx("div", { "data-slot": "chat-empty-footer", className: "mt-5", children: emptyFooter })
983
- ]
984
- }
985
- )
986
- ]
987
- }
988
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
989
- /* @__PURE__ */ jsx(
990
- ChatThread,
991
- {
992
- messages,
993
- pending,
994
- pendingLabel,
995
- scrollButtonLabel,
996
- uploadingLabel,
997
- maxHeight: workspace || application && isMobile ? "none" : threadMaxHeight,
998
- appearance: workspace ? "workspace" : "bubbles",
999
- footer: threadFooter,
1000
- markdownComponents,
1001
- onOpenAttachment,
1002
- attachmentAction,
1003
- messageActions,
1004
- className: cn(
1005
- "mb-3",
1006
- application && "min-h-0 flex-1 px-4 py-4 md:px-0 md:py-0",
1007
- workspace && "mb-0 min-h-0 flex-1"
1008
- )
1009
- }
1010
- ),
1011
- /* @__PURE__ */ jsxs(
1012
- ActionDock,
1013
- {
1014
- shrink: false,
1015
- className: cn(
1016
- "pt-3",
1017
- application && "px-4 py-3 md:bg-transparent md:px-0 md:pb-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
1018
- workspace && "border-0 bg-gradient-to-t from-background via-background to-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-8 backdrop-blur-none supports-[backdrop-filter]:bg-gradient-to-t",
1019
- workspace && mobileCompactComposer && "max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))] max-md:pt-3"
1020
- ),
1021
- children: [
1022
- /* @__PURE__ */ jsx(
1023
- ChatSuggestions,
1024
- {
1025
- slot: "chat-replies",
1026
- items: replySuggestions,
1027
- onSelect: (reply) => void sendSuggestion(reply),
1028
- label: replySuggestionsLabel,
1029
- showLabel: false,
1030
- disabled: pending,
1031
- className: cn("mb-3", workspace && "mx-auto max-w-3xl")
1032
- }
1033
- ),
1034
- /* @__PURE__ */ jsx(
1035
- ChatInput,
1036
- {
1037
- value: input,
1038
- onChange: setInput,
1039
- onSend: (text) => void send(text),
1040
- pending,
1041
- placeholder: activePlaceholder,
1042
- size: composerAppearance === "spacious" ? "lg" : "sm",
1043
- voice,
1044
- voiceSession,
1045
- voiceTitle: activeVoiceTitle ?? voiceTitle,
1046
- voiceLang,
1047
- inputRef,
1048
- attachments,
1049
- onFilesSelected,
1050
- onRemoveAttachment,
1051
- accept: attachmentAccept,
1052
- addFilesLabel,
1053
- uploadingLabel,
1054
- appearance: composerAppearance,
1055
- compactOnBlur: mobileCompactComposer && isMobile,
1056
- helperText: composerHelperText,
1057
- privacyNote: composerPrivacyNote,
1058
- className: workspace ? "mx-auto max-w-3xl rounded-3xl shadow-sm" : void 0
1059
- }
1060
- ),
1061
- error && /* @__PURE__ */ jsx(ChatError, { className: workspace ? "mx-auto max-w-3xl px-2" : void 0, children: error })
1062
- ]
1063
- }
1064
- )
1065
- ] })
1066
- }
1067
- );
1068
- });
39
+ import "../chunk-PIU5SSUV.js";
40
+ import "../chunk-CS7DM3NV.js";
1069
41
  export {
1070
42
  Chat,
1071
43
  ChatCopyAction,