@epicbagel/saas-kit 1.0.0 → 1.1.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.
Files changed (120) hide show
  1. package/README.md +38 -1
  2. package/THIRD_PARTY_NOTICES.md +13 -7
  3. package/dist/chunk-2AU7ZRGX.cjs +96 -0
  4. package/dist/chunk-2BTDBZYN.cjs +154 -0
  5. package/dist/chunk-2HV2KKOE.cjs +38 -0
  6. package/dist/chunk-2KFZQJH5.cjs +29 -0
  7. package/dist/chunk-32Q4XOTU.cjs +204 -0
  8. package/dist/chunk-42RTTHND.js +29 -0
  9. package/dist/chunk-6FC65AZQ.js +38 -0
  10. package/dist/chunk-6V4XMQ6D.js +118 -0
  11. package/dist/chunk-B765DSRG.cjs +66 -0
  12. package/dist/chunk-ELPV44S6.cjs +44 -0
  13. package/dist/chunk-EOUO3ORO.cjs +244 -0
  14. package/dist/chunk-EZCKWTH7.js +204 -0
  15. package/dist/chunk-F366BHWH.js +116 -0
  16. package/dist/chunk-GFG2R5BZ.js +132 -0
  17. package/dist/chunk-HAC32ALJ.js +244 -0
  18. package/dist/chunk-I5UQSBBZ.cjs +118 -0
  19. package/dist/chunk-INNGGLMZ.js +44 -0
  20. package/dist/chunk-IW4ZIXXG.js +66 -0
  21. package/dist/chunk-JZOBIPFC.js +96 -0
  22. package/dist/chunk-KENBU3NV.cjs +27 -0
  23. package/dist/chunk-MHYAVXOE.js +159 -0
  24. package/dist/chunk-RIWLAMME.cjs +159 -0
  25. package/dist/chunk-SUB4BAYE.cjs +73 -0
  26. package/dist/chunk-THWIJG5C.js +73 -0
  27. package/dist/chunk-V4YWTRLD.cjs +116 -0
  28. package/dist/chunk-VY5U4VGV.js +27 -0
  29. package/dist/chunk-XABXUP6L.cjs +132 -0
  30. package/dist/chunk-Z45YKVAM.js +154 -0
  31. package/dist/primitives/action-dock.cjs +8 -0
  32. package/dist/primitives/action-dock.d.cts +10 -0
  33. package/dist/primitives/action-dock.d.ts +10 -0
  34. package/dist/primitives/action-dock.js +8 -0
  35. package/dist/primitives/alert.d.cts +1 -1
  36. package/dist/primitives/alert.d.ts +1 -1
  37. package/dist/primitives/attachment.cjs +25 -203
  38. package/dist/primitives/attachment.d.cts +1 -1
  39. package/dist/primitives/attachment.d.ts +1 -1
  40. package/dist/primitives/attachment.js +12 -190
  41. package/dist/primitives/badge.d.cts +1 -1
  42. package/dist/primitives/badge.d.ts +1 -1
  43. package/dist/primitives/board-view.cjs +3 -3
  44. package/dist/primitives/board-view.js +2 -2
  45. package/dist/primitives/bubble.cjs +14 -0
  46. package/dist/primitives/bubble.d.cts +21 -0
  47. package/dist/primitives/bubble.d.ts +21 -0
  48. package/dist/primitives/bubble.js +14 -0
  49. package/dist/primitives/button.d.cts +2 -2
  50. package/dist/primitives/button.d.ts +2 -2
  51. package/dist/primitives/chat.cjs +864 -0
  52. package/dist/primitives/chat.d.cts +326 -0
  53. package/dist/primitives/chat.d.ts +326 -0
  54. package/dist/primitives/chat.js +864 -0
  55. package/dist/primitives/connection.cjs +6 -6
  56. package/dist/primitives/connection.js +7 -7
  57. package/dist/primitives/file-dropzone.cjs +7 -113
  58. package/dist/primitives/file-dropzone.js +6 -112
  59. package/dist/primitives/input-group.cjs +21 -0
  60. package/dist/primitives/input-group.d.cts +20 -0
  61. package/dist/primitives/input-group.d.ts +20 -0
  62. package/dist/primitives/input-group.js +21 -0
  63. package/dist/primitives/item.cjs +197 -0
  64. package/dist/primitives/item.d.cts +28 -0
  65. package/dist/primitives/item.d.ts +28 -0
  66. package/dist/primitives/item.js +197 -0
  67. package/dist/primitives/lazy-markdown.cjs +10 -0
  68. package/dist/primitives/lazy-markdown.d.cts +20 -0
  69. package/dist/primitives/lazy-markdown.d.ts +20 -0
  70. package/dist/primitives/lazy-markdown.js +10 -0
  71. package/dist/primitives/markdown.cjs +17 -0
  72. package/dist/primitives/markdown.d.cts +32 -0
  73. package/dist/primitives/markdown.d.ts +32 -0
  74. package/dist/primitives/markdown.js +17 -0
  75. package/dist/primitives/marker.cjs +14 -0
  76. package/dist/primitives/marker.d.cts +15 -0
  77. package/dist/primitives/marker.d.ts +15 -0
  78. package/dist/primitives/marker.js +14 -0
  79. package/dist/primitives/message-scroller.cjs +26 -0
  80. package/dist/primitives/message-scroller.d.cts +16 -0
  81. package/dist/primitives/message-scroller.d.ts +16 -0
  82. package/dist/primitives/message-scroller.js +26 -0
  83. package/dist/primitives/message.cjs +7 -84
  84. package/dist/primitives/message.js +8 -85
  85. package/dist/primitives/resource-list.cjs +3 -3
  86. package/dist/primitives/resource-list.js +2 -2
  87. package/dist/primitives/rich-text.cjs +3 -38
  88. package/dist/primitives/rich-text.js +3 -38
  89. package/dist/primitives/section-header.d.cts +1 -1
  90. package/dist/primitives/section-header.d.ts +1 -1
  91. package/dist/primitives/spinner.cjs +23 -0
  92. package/dist/primitives/spinner.d.cts +6 -0
  93. package/dist/primitives/spinner.d.ts +6 -0
  94. package/dist/primitives/spinner.js +23 -0
  95. package/dist/primitives/suggestion.cjs +11 -0
  96. package/dist/primitives/suggestion.d.cts +19 -0
  97. package/dist/primitives/suggestion.d.ts +19 -0
  98. package/dist/primitives/suggestion.js +11 -0
  99. package/dist/primitives/switch-field.cjs +2 -2
  100. package/dist/primitives/switch-field.js +3 -3
  101. package/dist/primitives/textarea.cjs +4 -33
  102. package/dist/primitives/textarea.js +3 -32
  103. package/dist/primitives/toggle-group.d.cts +2 -2
  104. package/dist/primitives/toggle-group.d.ts +2 -2
  105. package/dist/primitives/toggle.d.cts +2 -2
  106. package/dist/primitives/toggle.d.ts +2 -2
  107. package/dist/primitives/trace.cjs +23 -0
  108. package/dist/primitives/trace.d.cts +22 -0
  109. package/dist/primitives/trace.d.ts +22 -0
  110. package/dist/primitives/trace.js +23 -0
  111. package/dist/primitives/voice-dictation-button.cjs +5 -239
  112. package/dist/primitives/voice-dictation-button.js +4 -238
  113. package/package.json +125 -2
  114. package/src/styles/components/bubble.css +81 -0
  115. package/src/styles/components/marker.css +27 -0
  116. package/src/styles/components/message-scroller.css +11 -0
  117. package/src/styles/components.css +3 -0
  118. package/src/styles/utilities.css +3 -2
  119. package/dist/{chunk-UGF2K6NR.js → chunk-HRLP4BHJ.js} +6 -6
  120. package/dist/{chunk-RWWOFWLP.cjs → chunk-IH3COANJ.cjs} +3 -3
@@ -0,0 +1,864 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ Trace,
5
+ TraceLine,
6
+ TraceTitle
7
+ } from "../chunk-Z45YKVAM.js";
8
+ import {
9
+ VoiceDictationButton
10
+ } from "../chunk-HAC32ALJ.js";
11
+ import {
12
+ Suggestion,
13
+ Suggestions
14
+ } from "../chunk-THWIJG5C.js";
15
+ import {
16
+ Marker,
17
+ MarkerContent
18
+ } from "../chunk-IW4ZIXXG.js";
19
+ import {
20
+ MessageScroller,
21
+ MessageScrollerButton,
22
+ MessageScrollerContent,
23
+ MessageScrollerItem,
24
+ MessageScrollerProvider,
25
+ MessageScrollerViewport
26
+ } from "../chunk-GFG2R5BZ.js";
27
+ import {
28
+ Message,
29
+ MessageContent
30
+ } from "../chunk-JZOBIPFC.js";
31
+ import {
32
+ InputGroup,
33
+ InputGroupAddon,
34
+ InputGroupTextarea
35
+ } from "../chunk-MHYAVXOE.js";
36
+ import "../chunk-6FC65AZQ.js";
37
+ import {
38
+ LazyMarkdown
39
+ } from "../chunk-VY5U4VGV.js";
40
+ import "../chunk-INNGGLMZ.js";
41
+ import {
42
+ FileDropzone
43
+ } from "../chunk-6V4XMQ6D.js";
44
+ import {
45
+ DropdownMenu,
46
+ DropdownMenuContent,
47
+ DropdownMenuItem,
48
+ DropdownMenuTrigger
49
+ } from "../chunk-V4LE2HWP.js";
50
+ import {
51
+ Bubble,
52
+ BubbleContent
53
+ } from "../chunk-F366BHWH.js";
54
+ import "../chunk-EIY5LLSP.js";
55
+ import "../chunk-NBUWGRQO.js";
56
+ import {
57
+ Attachment,
58
+ AttachmentAction,
59
+ AttachmentActions,
60
+ AttachmentContent,
61
+ AttachmentDescription,
62
+ AttachmentGroup,
63
+ AttachmentMedia,
64
+ AttachmentTitle,
65
+ AttachmentTrigger
66
+ } from "../chunk-EZCKWTH7.js";
67
+ import "../chunk-ZRLUUZHK.js";
68
+ import {
69
+ ActionDock
70
+ } from "../chunk-42RTTHND.js";
71
+ import "../chunk-QMA3WVB7.js";
72
+ import {
73
+ Button
74
+ } from "../chunk-HQRWRRZJ.js";
75
+ import {
76
+ useIsMobile
77
+ } from "../chunk-PIU5SSUV.js";
78
+ import {
79
+ formatRelativeLong
80
+ } from "../chunk-CS7DM3NV.js";
81
+ import {
82
+ cn
83
+ } from "../chunk-G6SRSWXB.js";
84
+
85
+ // src/primitives/chat.tsx
86
+ import {
87
+ forwardRef,
88
+ useCallback,
89
+ useEffect,
90
+ useId,
91
+ useImperativeHandle,
92
+ useRef,
93
+ useState
94
+ } from "react";
95
+ import {
96
+ ArrowDown,
97
+ ArrowRight,
98
+ ArrowUp,
99
+ ArrowUpRight,
100
+ Download,
101
+ FileText,
102
+ LockKeyhole,
103
+ Paperclip,
104
+ Plus,
105
+ X
106
+ } from "lucide-react";
107
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
108
+ function useArrivingUserTurnId(messages) {
109
+ const seen = useRef(null);
110
+ seen.current ??= new Set(messages.map((m) => m.id));
111
+ useEffect(() => {
112
+ seen.current = new Set(messages.map((m) => m.id));
113
+ }, [messages]);
114
+ for (let i = messages.length - 1; i >= 0; i--) {
115
+ const message = messages[i];
116
+ if (message.role === "user") return seen.current.has(message.id) ? void 0 : message.id;
117
+ }
118
+ return void 0;
119
+ }
120
+ function TimelineMarker({ label }) {
121
+ return /* @__PURE__ */ jsx(Marker, { variant: "separator", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx(MarkerContent, { children: label }) });
122
+ }
123
+ var ChatThread = forwardRef(
124
+ function ChatThread2({
125
+ messages,
126
+ pending = false,
127
+ pendingLabel = "Thinking\u2026",
128
+ scrollButtonLabel = "Scroll to latest message",
129
+ uploadingLabel,
130
+ maxHeight = "46vh",
131
+ appearance = "bubbles",
132
+ footer,
133
+ markdownComponents,
134
+ onOpenAttachment,
135
+ attachmentAction = "open",
136
+ className
137
+ }, ref) {
138
+ const timelineNow = /* @__PURE__ */ new Date();
139
+ const anchorId = useArrivingUserTurnId(messages);
140
+ const workspace = appearance === "workspace";
141
+ return /* @__PURE__ */ jsx(MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ jsxs(
142
+ MessageScroller,
143
+ {
144
+ ref,
145
+ style: { maxHeight },
146
+ className: cn("min-h-0", className),
147
+ children: [
148
+ /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(
149
+ MessageScrollerContent,
150
+ {
151
+ className: cn(
152
+ workspace ? "mx-auto w-full max-w-3xl gap-7 px-4 py-6 md:px-6 md:py-8" : "gap-2.5 px-px py-px"
153
+ ),
154
+ children: [
155
+ messages.map((m, index) => {
156
+ const isUser = m.role === "user";
157
+ const isFullWidth = m.width === "full";
158
+ const relativeTime = formatRelativeLong(m.createdAt, timelineNow);
159
+ const previousRelativeTime = formatRelativeLong(messages[index - 1]?.createdAt, timelineNow);
160
+ const startsTimelineSection = relativeTime && relativeTime !== previousRelativeTime;
161
+ return /* @__PURE__ */ jsxs(
162
+ MessageScrollerItem,
163
+ {
164
+ messageId: m.id,
165
+ scrollAnchor: m.id === anchorId,
166
+ className: "flex flex-col gap-3",
167
+ children: [
168
+ startsTimelineSection ? /* @__PURE__ */ jsx(TimelineMarker, { label: relativeTime }) : null,
169
+ /* @__PURE__ */ jsx(Message, { align: isUser ? "end" : "start", children: /* @__PURE__ */ jsxs(MessageContent, { children: [
170
+ (m.content || m.text) && /* @__PURE__ */ jsx(
171
+ Bubble,
172
+ {
173
+ variant: workspace ? isUser ? "muted" : "ghost" : isUser ? "default" : "muted",
174
+ align: isUser ? "end" : "start",
175
+ className: isFullWidth ? "w-full" : void 0,
176
+ children: /* @__PURE__ */ jsx(
177
+ BubbleContent,
178
+ {
179
+ className: cn(
180
+ isFullWidth && "w-full",
181
+ 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"
182
+ ),
183
+ children: m.content ?? (m.text ? /* @__PURE__ */ jsx(LazyMarkdown, { size: workspace ? "base" : "sm", components: markdownComponents, children: m.text }) : null)
184
+ }
185
+ )
186
+ }
187
+ ),
188
+ m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx(
189
+ ChatAttachmentList,
190
+ {
191
+ attachments: m.attachments,
192
+ onOpen: onOpenAttachment,
193
+ openAction: attachmentAction,
194
+ uploadingLabel,
195
+ className: "px-0 pt-0"
196
+ }
197
+ )
198
+ ] }) })
199
+ ]
200
+ },
201
+ m.id
202
+ );
203
+ }),
204
+ pending && /* @__PURE__ */ jsx(MessageScrollerItem, { children: /* @__PURE__ */ jsx(Message, { align: "start", 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: pendingLabel }) }) }) }) }) }),
205
+ footer && // Always painted: content-visibility:auto would contain the
206
+ // footer's paint and layout, clipping a ring or shadow at its
207
+ // edge and re-parenting anything fixed inside it.
208
+ /* @__PURE__ */ jsx(MessageScrollerItem, { className: "[content-visibility:visible]", children: /* @__PURE__ */ jsx(Message, { align: "start", children: /* @__PURE__ */ jsx(MessageContent, { children: footer }) }) })
209
+ ]
210
+ }
211
+ ) }),
212
+ /* @__PURE__ */ jsxs(MessageScrollerButton, { variant: "outline", className: "rounded-full shadow-sm", children: [
213
+ /* @__PURE__ */ jsx(ArrowDown, { className: "h-4 w-4" }),
214
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: scrollButtonLabel })
215
+ ] })
216
+ ]
217
+ }
218
+ ) });
219
+ }
220
+ );
221
+ function formatFileSize(size) {
222
+ if (size === void 0) return "File";
223
+ if (size < 1024) return `${size} B`;
224
+ if (size < 1048576) return `${Math.round(size / 1024)} KB`;
225
+ return `${(size / 1048576).toFixed(1)} MB`;
226
+ }
227
+ var ATTACHMENT_ACTIONS = {
228
+ open: { verb: "Open", icon: ArrowUpRight },
229
+ download: { verb: "Download", icon: Download }
230
+ };
231
+ var COMPOSER_SIZES = {
232
+ sm: { minHeight: "min-h-12", rows: 2 },
233
+ md: { minHeight: "min-h-[4.5rem]", rows: 2 },
234
+ lg: { minHeight: "min-h-32", rows: 3 }
235
+ };
236
+ function ChatAttachmentList({
237
+ attachments,
238
+ onRemove,
239
+ onOpen,
240
+ openAction = "open",
241
+ uploadingLabel = "Uploading\u2026",
242
+ className
243
+ }) {
244
+ const { verb, icon: ActionIcon } = ATTACHMENT_ACTIONS[openAction];
245
+ return /* @__PURE__ */ jsx(AttachmentGroup, { className: cn("gap-2 px-3 pt-3", className), children: attachments.map((attachment) => {
246
+ const state = attachment.state ?? "done";
247
+ const opens = Boolean(onOpen) && state === "done";
248
+ return /* @__PURE__ */ jsxs(
249
+ Attachment,
250
+ {
251
+ state,
252
+ size: "sm",
253
+ className: "min-w-[12rem] rounded-xl p-2",
254
+ children: [
255
+ opens && /* @__PURE__ */ jsx(
256
+ AttachmentTrigger,
257
+ {
258
+ "aria-label": `${verb} ${attachment.name}`,
259
+ title: `${verb} ${attachment.name}`,
260
+ onClick: () => onOpen?.(attachment),
261
+ className: "rounded-xl focus-visible:ring-2 focus-visible:ring-ring/50"
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx(AttachmentMedia, { variant: "icon", className: "h-9 w-9 rounded-lg bg-muted", children: /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" }) }),
265
+ /* @__PURE__ */ jsxs(AttachmentContent, { className: "px-2", children: [
266
+ /* @__PURE__ */ jsx(AttachmentTitle, { className: "text-xs font-medium", children: attachment.name }),
267
+ /* @__PURE__ */ jsx(AttachmentDescription, { className: "text-[11px]", children: state === "uploading" ? uploadingLabel : opens ? `${formatFileSize(attachment.size)} \xB7 ${verb}` : formatFileSize(attachment.size) })
268
+ ] }),
269
+ onRemove ? /* @__PURE__ */ jsx(AttachmentActions, { children: /* @__PURE__ */ jsx(
270
+ AttachmentAction,
271
+ {
272
+ type: "button",
273
+ "aria-label": `Remove ${attachment.name}`,
274
+ onClick: () => onRemove(attachment),
275
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
276
+ }
277
+ ) }) : opens ? (
278
+ // The trigger covers the whole file; the icon only says what it does.
279
+ /* @__PURE__ */ jsx(AttachmentActions, { "aria-hidden": true, className: "pointer-events-none pr-1 text-muted-foreground", children: /* @__PURE__ */ jsx(ActionIcon, { className: "h-4 w-4" }) })
280
+ ) : null
281
+ ]
282
+ },
283
+ attachment.id
284
+ );
285
+ }) });
286
+ }
287
+ function ChatInput({
288
+ value,
289
+ onChange,
290
+ onSend,
291
+ pending = false,
292
+ disabled = false,
293
+ placeholder = "Type your message\u2026",
294
+ size = "md",
295
+ uploadingLabel,
296
+ voice = true,
297
+ voiceTitle,
298
+ voiceLang,
299
+ submitLabel,
300
+ autoFocus = false,
301
+ className,
302
+ inputRef,
303
+ attachments = [],
304
+ onFilesSelected,
305
+ onRemoveAttachment,
306
+ accept,
307
+ multiple = true,
308
+ addFilesLabel = "Add files",
309
+ appearance = "default",
310
+ helperText,
311
+ privacyNote
312
+ }) {
313
+ const internalRef = useRef(null);
314
+ const ref = inputRef ?? internalRef;
315
+ const fileInputRef = useRef(null);
316
+ const isDisabled = pending || disabled;
317
+ const attachmentsReady = attachments.length > 0 && attachments.every((item) => item.state !== "uploading");
318
+ const canSubmit = !isDisabled && (Boolean(value.trim()) || attachmentsReady);
319
+ const submit = () => {
320
+ const trimmed = value.trim();
321
+ if (!trimmed && !attachmentsReady || isDisabled) return;
322
+ onSend(trimmed, attachments);
323
+ };
324
+ const spacious = appearance === "spacious";
325
+ const composerSize = COMPOSER_SIZES[size];
326
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-input-container", className: "w-full", children: [
327
+ /* @__PURE__ */ jsxs(
328
+ "form",
329
+ {
330
+ "data-slot": "chat-input",
331
+ "data-appearance": appearance,
332
+ className: "w-full",
333
+ onSubmit: (event) => {
334
+ if (event.target !== event.currentTarget) return;
335
+ event.preventDefault();
336
+ submit();
337
+ },
338
+ children: [
339
+ onFilesSelected && /* @__PURE__ */ jsx(
340
+ "input",
341
+ {
342
+ ref: fileInputRef,
343
+ type: "file",
344
+ accept,
345
+ multiple,
346
+ hidden: true,
347
+ onChange: (event) => {
348
+ const files = Array.from(event.currentTarget.files ?? []);
349
+ if (files.length > 0) void onFilesSelected(files);
350
+ event.currentTarget.value = "";
351
+ }
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsxs(
355
+ InputGroup,
356
+ {
357
+ className: cn(
358
+ // Opaque in both modes: the input group's own dark:bg-input/30
359
+ // would let the thread show through the composer.
360
+ "overflow-hidden bg-background dark:bg-background",
361
+ spacious ? "rounded-3xl" : "rounded-lg",
362
+ className
363
+ ),
364
+ children: [
365
+ attachments.length > 0 && /* @__PURE__ */ jsx(
366
+ ChatAttachmentList,
367
+ {
368
+ attachments,
369
+ onRemove: onRemoveAttachment,
370
+ uploadingLabel,
371
+ className: "w-full"
372
+ }
373
+ ),
374
+ /* @__PURE__ */ jsx(
375
+ InputGroupTextarea,
376
+ {
377
+ ref,
378
+ value,
379
+ onChange: (e) => onChange(e.target.value),
380
+ onKeyDown: (e) => {
381
+ if (e.key !== "Enter" || e.shiftKey) return;
382
+ if (e.nativeEvent.isComposing || e.keyCode === 229) return;
383
+ e.preventDefault();
384
+ submit();
385
+ },
386
+ placeholder,
387
+ rows: composerSize.rows,
388
+ disabled: isDisabled,
389
+ autoFocus,
390
+ className: cn(
391
+ composerSize.minHeight,
392
+ // Grows with the text, then scrolls rather than pushing the thread away.
393
+ "max-h-48 leading-relaxed disabled:opacity-70 placeholder:text-sm",
394
+ spacious ? "px-6 pb-2 pt-6" : "px-4 py-3.5"
395
+ )
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsxs(
399
+ InputGroupAddon,
400
+ {
401
+ align: "block-end",
402
+ "data-slot": "chat-input-actions",
403
+ onClick: (event) => {
404
+ if (!event.currentTarget.contains(event.target)) return;
405
+ if (!event.target.closest("button")) ref.current?.focus();
406
+ },
407
+ className: cn(
408
+ "gap-2",
409
+ spacious ? "px-5 pb-5 pt-1" : "border-t border-border px-3 py-2.5"
410
+ ),
411
+ children: [
412
+ onFilesSelected && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
413
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
414
+ Button,
415
+ {
416
+ type: "button",
417
+ size: spacious ? "icon-lg" : "icon",
418
+ variant: "ghost",
419
+ "aria-label": "Add to conversation",
420
+ children: /* @__PURE__ */ jsx(Plus, { className: cn(spacious && "h-5 w-5") })
421
+ }
422
+ ) }),
423
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => fileInputRef.current?.click(), children: [
424
+ /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" }),
425
+ addFilesLabel
426
+ ] }) })
427
+ ] }),
428
+ voice && /* @__PURE__ */ jsx(
429
+ VoiceDictationButton,
430
+ {
431
+ size: "icon",
432
+ title: voiceTitle,
433
+ lang: voiceLang,
434
+ onCommit: (text) => onSend(text),
435
+ className: spacious ? "h-12 w-12 border-transparent bg-transparent shadow-none hover:bg-muted" : void 0
436
+ }
437
+ ),
438
+ helperText ? /* @__PURE__ */ jsx(
439
+ "span",
440
+ {
441
+ "data-slot": "chat-input-helper",
442
+ className: "min-w-0 flex-1 truncate px-1 text-center text-xs font-normal text-muted-foreground sm:px-2 sm:text-sm",
443
+ children: helperText
444
+ }
445
+ ) : /* @__PURE__ */ jsx("div", { className: "flex-1" }),
446
+ submitLabel ? /* @__PURE__ */ jsxs(
447
+ Button,
448
+ {
449
+ type: "submit",
450
+ disabled: !canSubmit,
451
+ "data-chat-action": "submit",
452
+ className: spacious ? "rounded-xl" : void 0,
453
+ children: [
454
+ submitLabel,
455
+ /* @__PURE__ */ jsx(ArrowRight, { className: "ml-2 h-4 w-4" })
456
+ ]
457
+ }
458
+ ) : /* @__PURE__ */ jsx(
459
+ Button,
460
+ {
461
+ type: "submit",
462
+ size: spacious ? "icon-lg" : "icon",
463
+ disabled: !canSubmit,
464
+ "aria-label": "Send",
465
+ "data-chat-action": "submit",
466
+ className: spacious ? "rounded-xl" : void 0,
467
+ children: /* @__PURE__ */ jsx(ArrowUp, { className: cn(spacious && "h-5 w-5") })
468
+ }
469
+ )
470
+ ]
471
+ }
472
+ )
473
+ ]
474
+ }
475
+ )
476
+ ]
477
+ }
478
+ ),
479
+ privacyNote ? /* @__PURE__ */ jsxs(
480
+ "div",
481
+ {
482
+ "data-slot": "chat-input-privacy",
483
+ className: "mt-3 flex items-center justify-center gap-2 px-4 text-center text-xs text-muted-foreground sm:text-sm",
484
+ children: [
485
+ /* @__PURE__ */ jsx(LockKeyhole, { "aria-hidden": true, className: "h-4 w-4 shrink-0" }),
486
+ /* @__PURE__ */ jsx("span", { children: privacyNote })
487
+ ]
488
+ }
489
+ ) : null
490
+ ] });
491
+ }
492
+ function VoiceHero({
493
+ onSubmit,
494
+ voiceTitle,
495
+ voiceLang,
496
+ onSwitchToText,
497
+ label,
498
+ switchLabel
499
+ }) {
500
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-5 py-6", children: [
501
+ /* @__PURE__ */ jsx(
502
+ VoiceDictationButton,
503
+ {
504
+ size: "lg",
505
+ title: voiceTitle,
506
+ lang: voiceLang,
507
+ onCommit: onSubmit,
508
+ className: "h-24 w-24 rounded-full p-0 shadow-lg transition-transform hover:scale-105 active:scale-95 [&_svg]:size-7"
509
+ }
510
+ ),
511
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1.5", children: [
512
+ /* @__PURE__ */ jsx("span", { className: "text-base font-medium text-foreground", children: label }),
513
+ /* @__PURE__ */ jsx(
514
+ "button",
515
+ {
516
+ type: "button",
517
+ onClick: onSwitchToText,
518
+ className: "text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-4 decoration-muted-foreground/40",
519
+ children: switchLabel
520
+ }
521
+ )
522
+ ] })
523
+ ] });
524
+ }
525
+ var EXAMPLE_PROMPT_LAYOUTS = {
526
+ chips: "wrap",
527
+ row: "row",
528
+ cards: "grid"
529
+ };
530
+ function ExamplePrompts({
531
+ prompts,
532
+ onSelect,
533
+ label = "Try",
534
+ variant = "chips",
535
+ className
536
+ }) {
537
+ const labelId = useId();
538
+ if (prompts.length === 0) return null;
539
+ const cards = variant === "cards";
540
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-examples", "data-variant": variant, className: cn(cards ? "mt-4" : "mt-3", className), children: [
541
+ /* @__PURE__ */ jsx("p", { id: labelId, "data-slot": "chat-examples-label", className: "mb-2 text-left text-xs text-muted-foreground", children: label }),
542
+ /* @__PURE__ */ jsx(Suggestions, { role: "group", "aria-labelledby": labelId, layout: EXAMPLE_PROMPT_LAYOUTS[variant], children: prompts.map((prompt) => /* @__PURE__ */ jsx(
543
+ Suggestion,
544
+ {
545
+ suggestion: prompt,
546
+ onClick: onSelect,
547
+ className: cards ? "h-auto min-h-12 px-3 py-2.5 text-xs" : void 0
548
+ },
549
+ prompt
550
+ )) })
551
+ ] });
552
+ }
553
+ function ExamplePromptsLoading({ label, className }) {
554
+ const list = Array.isArray(label) ? label : [label];
555
+ const [idx, setIdx] = useState(0);
556
+ useEffect(() => {
557
+ if (list.length <= 1) return;
558
+ const id = setInterval(() => setIdx((i) => (i + 1) % list.length), 1800);
559
+ return () => clearInterval(id);
560
+ }, [list.length]);
561
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-examples", "data-state": "loading", "aria-busy": "true", className: cn("mt-3", className), children: [
562
+ /* @__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) }) }),
563
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "flex gap-2 overflow-hidden", children: [36, 44, 28, 40].map((w, i) => /* @__PURE__ */ jsx(
564
+ "span",
565
+ {
566
+ className: "h-11 shrink-0 rounded-md bg-muted animate-pulse",
567
+ style: {
568
+ width: `${w * 4}px`,
569
+ animationDelay: `${i * 120}ms`
570
+ }
571
+ },
572
+ i
573
+ )) })
574
+ ] });
575
+ }
576
+ function ChatError({ children, className }) {
577
+ return /* @__PURE__ */ jsx("p", { "data-slot": "chat-error", role: "alert", className: cn("mt-2 text-xs text-destructive", className), children });
578
+ }
579
+ var Chat = forwardRef(function Chat2({
580
+ messages,
581
+ onSend,
582
+ onSendError,
583
+ pending = false,
584
+ pendingLabel,
585
+ emptyPendingContent,
586
+ error = null,
587
+ emptyTitle,
588
+ emptySubtitle,
589
+ examplePrompts = [],
590
+ examplePromptsLabel = "Try",
591
+ examplePromptsVariant = "chips",
592
+ examplePromptsLoading = false,
593
+ examplePromptsLoadingLabel = "Loading suggestions\u2026",
594
+ compactEmptyState = false,
595
+ defaultMode,
596
+ placeholder = "Type your message\u2026",
597
+ activePlaceholder = "Reply\u2026",
598
+ submitLabel,
599
+ attachments = [],
600
+ onFilesSelected,
601
+ onRemoveAttachment,
602
+ attachmentAccept,
603
+ addFilesLabel,
604
+ uploadingLabel,
605
+ composerAppearance = "default",
606
+ composerHelperText,
607
+ composerPrivacyNote,
608
+ emptyFileDropzone = false,
609
+ emptyFileDropzoneLabel = "Drop files here, or choose them",
610
+ emptyFileDropzoneDescription,
611
+ voice = true,
612
+ voiceTitle,
613
+ activeVoiceTitle,
614
+ voiceLang,
615
+ voiceHeroLabel = "Tap to speak",
616
+ voiceHeroSwitchLabel = "or type instead",
617
+ threadMaxHeight = "46vh",
618
+ scrollButtonLabel,
619
+ emptyAlign = "start",
620
+ emptyIcon,
621
+ emptyFooter,
622
+ layout = "embedded",
623
+ threadFooter,
624
+ markdownComponents,
625
+ onOpenAttachment,
626
+ attachmentAction,
627
+ className
628
+ }, ref) {
629
+ const isMobile = useIsMobile();
630
+ const [input, setInput] = useState("");
631
+ const inputRef = useRef(null);
632
+ const conversationStarted = messages.length > 0;
633
+ const [userMode, setUserMode] = useState(null);
634
+ const mode = userMode ?? defaultMode ?? (isMobile ? "voice" : "text");
635
+ const send = useCallback(
636
+ async (text, sentAttachments = attachments) => {
637
+ const trimmed = text.trim();
638
+ if (!trimmed && sentAttachments.length === 0 || pending) return;
639
+ setInput("");
640
+ try {
641
+ await onSend(trimmed, sentAttachments);
642
+ } catch (cause) {
643
+ if (trimmed) {
644
+ setInput((current) => current ? current : trimmed);
645
+ if (mode === "voice") setUserMode("text");
646
+ }
647
+ if (onSendError) onSendError(cause);
648
+ else if (typeof globalThis.reportError === "function") globalThis.reportError(cause);
649
+ else console.error(cause);
650
+ }
651
+ window.setTimeout(() => inputRef.current?.focus(), 0);
652
+ },
653
+ [attachments, mode, onSend, onSendError, pending]
654
+ );
655
+ useImperativeHandle(
656
+ ref,
657
+ () => ({
658
+ focusInput: () => inputRef.current?.focus(),
659
+ clearInput: () => setInput("")
660
+ }),
661
+ []
662
+ );
663
+ const application = layout === "application";
664
+ const workspace = layout === "workspace";
665
+ const showEmptyFileDropzone = emptyFileDropzone && Boolean(onFilesSelected) && attachments.length === 0;
666
+ return /* @__PURE__ */ jsx(
667
+ "div",
668
+ {
669
+ "data-slot": "chat",
670
+ "data-layout": layout,
671
+ className: cn(
672
+ "w-full",
673
+ application && "flex min-h-[calc(100svh-4rem)] flex-1 flex-col md:block md:min-h-0",
674
+ workspace && "flex h-full min-h-0 flex-1 flex-col overflow-hidden",
675
+ className
676
+ ),
677
+ children: !conversationStarted && pending && emptyPendingContent ? /* @__PURE__ */ jsx(
678
+ "div",
679
+ {
680
+ "data-slot": "chat-empty-pending",
681
+ className: "flex min-h-0 flex-1 items-center justify-center px-4 py-8",
682
+ children: emptyPendingContent
683
+ }
684
+ ) : !conversationStarted ? /* @__PURE__ */ jsxs(
685
+ "div",
686
+ {
687
+ "data-slot": "chat-empty",
688
+ className: cn(
689
+ emptyAlign === "center" && "mx-auto flex w-full max-w-3xl flex-col items-center text-center",
690
+ application && "min-h-0 max-w-none flex-1 md:max-w-3xl",
691
+ workspace && "min-h-0 max-w-none flex-1 overflow-y-auto overscroll-contain px-4 pb-20"
692
+ ),
693
+ children: [
694
+ /* @__PURE__ */ jsxs(
695
+ "div",
696
+ {
697
+ "data-slot": "chat-empty-intro",
698
+ className: cn(
699
+ application && "flex flex-1 flex-col items-center justify-center px-6 py-8 md:contents",
700
+ workspace && "mt-auto w-full flex-none px-0 py-0"
701
+ ),
702
+ children: [
703
+ !compactEmptyState && emptyIcon && /* @__PURE__ */ jsx("div", { "data-slot": "chat-empty-icon", className: "mb-4", children: emptyIcon }),
704
+ 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 }),
705
+ emptySubtitle && !compactEmptyState && /* @__PURE__ */ jsx(
706
+ "p",
707
+ {
708
+ "data-slot": "chat-empty-subtitle",
709
+ className: cn(
710
+ "text-base text-muted-foreground leading-relaxed mb-6 max-w-md",
711
+ emptyAlign === "center" && "mx-auto"
712
+ ),
713
+ children: emptySubtitle
714
+ }
715
+ )
716
+ ]
717
+ }
718
+ ),
719
+ /* @__PURE__ */ jsxs(
720
+ ActionDock,
721
+ {
722
+ shrink: false,
723
+ className: cn(
724
+ application && "px-4 py-3 md:border-0 md:bg-transparent md:p-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
725
+ !application && "border-0 bg-transparent p-0 backdrop-blur-none",
726
+ workspace && "mb-auto mt-2 w-full max-w-3xl border-0 bg-transparent p-0 backdrop-blur-none supports-[backdrop-filter]:bg-transparent"
727
+ ),
728
+ children: [
729
+ showEmptyFileDropzone && onFilesSelected && /* @__PURE__ */ jsx(
730
+ FileDropzone,
731
+ {
732
+ multiple: true,
733
+ onFiles: onFilesSelected,
734
+ accept: attachmentAccept,
735
+ label: emptyFileDropzoneLabel,
736
+ description: emptyFileDropzoneDescription,
737
+ icon: /* @__PURE__ */ jsx(FileText, {}),
738
+ className: "mb-4 rounded-lg bg-muted/20 py-8"
739
+ }
740
+ ),
741
+ mode === "voice" ? /* @__PURE__ */ jsx(
742
+ VoiceHero,
743
+ {
744
+ onSubmit: (text) => void send(text),
745
+ voiceTitle,
746
+ voiceLang,
747
+ label: voiceHeroLabel,
748
+ switchLabel: voiceHeroSwitchLabel,
749
+ onSwitchToText: () => setUserMode("text")
750
+ }
751
+ ) : /* @__PURE__ */ jsx(
752
+ ChatInput,
753
+ {
754
+ value: input,
755
+ onChange: setInput,
756
+ onSend: (text) => void send(text),
757
+ pending,
758
+ placeholder,
759
+ size: showEmptyFileDropzone || application && isMobile ? "md" : "lg",
760
+ voice,
761
+ voiceTitle,
762
+ voiceLang,
763
+ submitLabel,
764
+ autoFocus: !isMobile,
765
+ inputRef,
766
+ attachments,
767
+ onFilesSelected,
768
+ onRemoveAttachment,
769
+ accept: attachmentAccept,
770
+ addFilesLabel,
771
+ uploadingLabel,
772
+ appearance: composerAppearance,
773
+ helperText: composerHelperText,
774
+ privacyNote: composerPrivacyNote,
775
+ className: workspace ? "rounded-3xl shadow-sm" : void 0
776
+ }
777
+ ),
778
+ error && /* @__PURE__ */ jsx(ChatError, { children: error }),
779
+ !pending && examplePromptsLoading && /* @__PURE__ */ jsx(ExamplePromptsLoading, { label: examplePromptsLoadingLabel }),
780
+ !pending && !examplePromptsLoading && examplePrompts.length > 0 && /* @__PURE__ */ jsx(
781
+ ExamplePrompts,
782
+ {
783
+ prompts: examplePrompts,
784
+ onSelect: (p) => void send(p),
785
+ label: examplePromptsLabel,
786
+ variant: examplePromptsVariant
787
+ }
788
+ ),
789
+ emptyFooter && /* @__PURE__ */ jsx("div", { "data-slot": "chat-empty-footer", className: "mt-5", children: emptyFooter })
790
+ ]
791
+ }
792
+ )
793
+ ]
794
+ }
795
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
796
+ /* @__PURE__ */ jsx(
797
+ ChatThread,
798
+ {
799
+ messages,
800
+ pending,
801
+ pendingLabel,
802
+ scrollButtonLabel,
803
+ uploadingLabel,
804
+ maxHeight: workspace || application && isMobile ? "none" : threadMaxHeight,
805
+ appearance: workspace ? "workspace" : "bubbles",
806
+ footer: threadFooter,
807
+ markdownComponents,
808
+ onOpenAttachment,
809
+ attachmentAction,
810
+ className: cn(
811
+ "mb-3",
812
+ application && "min-h-0 flex-1 px-4 py-4 md:px-0 md:py-0",
813
+ workspace && "mb-0 min-h-0 flex-1"
814
+ )
815
+ }
816
+ ),
817
+ /* @__PURE__ */ jsxs(
818
+ ActionDock,
819
+ {
820
+ shrink: false,
821
+ className: cn(
822
+ "pt-3",
823
+ application && "px-4 py-3 md:bg-transparent md:px-0 md:pb-0 md:backdrop-blur-none md:supports-backdrop-filter:bg-transparent",
824
+ 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"
825
+ ),
826
+ children: [
827
+ /* @__PURE__ */ jsx(
828
+ ChatInput,
829
+ {
830
+ value: input,
831
+ onChange: setInput,
832
+ onSend: (text) => void send(text),
833
+ pending,
834
+ placeholder: activePlaceholder,
835
+ size: composerAppearance === "spacious" ? "lg" : "sm",
836
+ voice,
837
+ voiceTitle: activeVoiceTitle ?? voiceTitle,
838
+ voiceLang,
839
+ inputRef,
840
+ attachments,
841
+ onFilesSelected,
842
+ onRemoveAttachment,
843
+ accept: attachmentAccept,
844
+ addFilesLabel,
845
+ uploadingLabel,
846
+ appearance: composerAppearance,
847
+ helperText: composerHelperText,
848
+ privacyNote: composerPrivacyNote,
849
+ className: workspace ? "mx-auto max-w-3xl rounded-3xl shadow-sm" : void 0
850
+ }
851
+ ),
852
+ error && /* @__PURE__ */ jsx(ChatError, { className: workspace ? "mx-auto max-w-3xl px-2" : void 0, children: error })
853
+ ]
854
+ }
855
+ )
856
+ ] })
857
+ }
858
+ );
859
+ });
860
+ export {
861
+ Chat,
862
+ ChatInput,
863
+ ChatThread
864
+ };