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