@particle-academy/react-fancy 5.70.1 → 5.71.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 (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -0
  3. package/dist/autocomplete.cjs +3 -3
  4. package/dist/autocomplete.js +2 -2
  5. package/dist/chat-thread.cjs +27 -0
  6. package/dist/chat-thread.cjs.map +1 -0
  7. package/dist/chat-thread.d.cts +260 -0
  8. package/dist/chat-thread.d.ts +260 -0
  9. package/dist/chat-thread.js +14 -0
  10. package/dist/chat-thread.js.map +1 -0
  11. package/dist/chunk-5IE65DIH.cjs +623 -0
  12. package/dist/chunk-5IE65DIH.cjs.map +1 -0
  13. package/dist/{chunk-6QAF35AJ.cjs → chunk-7JLKOVHX.cjs} +3 -3
  14. package/dist/{chunk-6QAF35AJ.cjs.map → chunk-7JLKOVHX.cjs.map} +1 -1
  15. package/dist/{chunk-TCMOD3WW.js → chunk-A6NYZ3F5.js} +3 -3
  16. package/dist/{chunk-TCMOD3WW.js.map → chunk-A6NYZ3F5.js.map} +1 -1
  17. package/dist/{chunk-6EA3DSPT.cjs → chunk-FGOFG2UZ.cjs} +5 -5
  18. package/dist/{chunk-6EA3DSPT.cjs.map → chunk-FGOFG2UZ.cjs.map} +1 -1
  19. package/dist/{chunk-FHRVSYJU.cjs → chunk-MP55W37V.cjs} +3 -3
  20. package/dist/{chunk-FHRVSYJU.cjs.map → chunk-MP55W37V.cjs.map} +1 -1
  21. package/dist/{chunk-EZL5DILH.js → chunk-OPDHSOUN.js} +3 -3
  22. package/dist/{chunk-EZL5DILH.js.map → chunk-OPDHSOUN.js.map} +1 -1
  23. package/dist/chunk-UWYSJARZ.js +620 -0
  24. package/dist/chunk-UWYSJARZ.js.map +1 -0
  25. package/dist/{chunk-M6UAVA2I.js → chunk-XIA2UAVO.js} +3 -3
  26. package/dist/{chunk-M6UAVA2I.js.map → chunk-XIA2UAVO.js.map} +1 -1
  27. package/dist/editor.cjs +3 -3
  28. package/dist/editor.js +3 -3
  29. package/dist/index.cjs +170 -161
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +26 -25
  34. package/dist/index.js.map +1 -1
  35. package/dist/input.cjs +1 -1
  36. package/dist/input.js +1 -1
  37. package/dist/inputs.cjs +7 -7
  38. package/dist/inputs.js +3 -3
  39. package/dist/json-editor.cjs +16 -16
  40. package/dist/json-editor.js +5 -5
  41. package/dist/prompt-input.cjs +1 -1
  42. package/dist/prompt-input.js +1 -1
  43. package/dist/select.cjs +3 -3
  44. package/dist/select.js +2 -2
  45. package/docs/ChatThread.md +318 -0
  46. package/package.json +11 -1
@@ -0,0 +1,623 @@
1
+ 'use strict';
2
+
3
+ var chunkR3EJNGJV_cjs = require('./chunk-R3EJNGJV.cjs');
4
+ var chunkBJL3VDRK_cjs = require('./chunk-BJL3VDRK.cjs');
5
+ var chunkMOU6O324_cjs = require('./chunk-MOU6O324.cjs');
6
+ var chunk3U7PUZLI_cjs = require('./chunk-3U7PUZLI.cjs');
7
+ var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
8
+ var react = require('react');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+
11
+ // src/components/ChatThread/chat-utils.ts
12
+ var DEFAULT_LABELS = {
13
+ log: "Conversation",
14
+ loadEarlier: "Load earlier messages",
15
+ loadingEarlier: "Loading earlier messages...",
16
+ newMessages: (n) => n === 1 ? "1 new message" : `${n} new messages`,
17
+ internal: "Internal note",
18
+ pending: "Sending...",
19
+ failed: "Not sent",
20
+ proposal: "Proposed",
21
+ accept: "Accept",
22
+ reject: "Reject",
23
+ accepted: "Accepted",
24
+ rejected: "Rejected",
25
+ withdrawn: "Withdrawn",
26
+ attachments: "Attachments",
27
+ roles: {
28
+ user: "You",
29
+ assistant: "Assistant",
30
+ system: "System",
31
+ agent: "Agent",
32
+ staff: "Staff",
33
+ customer: "Customer"
34
+ }
35
+ };
36
+ function mergeLabels(labels) {
37
+ if (!labels) return DEFAULT_LABELS;
38
+ return {
39
+ ...DEFAULT_LABELS,
40
+ ...labels,
41
+ roles: { ...DEFAULT_LABELS.roles, ...labels.roles }
42
+ };
43
+ }
44
+ function toDate(value) {
45
+ if (value === void 0 || value === null || value === "") return null;
46
+ const d = new Date(value);
47
+ return Number.isNaN(d.getTime()) ? null : d;
48
+ }
49
+ function dayKey(d) {
50
+ const p = (n) => String(n).padStart(2, "0");
51
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
52
+ }
53
+ function defaultFormatTime(d) {
54
+ return d.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
55
+ }
56
+ function defaultFormatDay(d) {
57
+ return d.toLocaleDateString(void 0, { dateStyle: "medium" });
58
+ }
59
+ function roleLabel(role, labels) {
60
+ const known = labels.roles[role];
61
+ if (known) return known;
62
+ return role ? role.charAt(0).toUpperCase() + role.slice(1) : "";
63
+ }
64
+ function initials(name) {
65
+ const parts = name.trim().split(/\s+/).filter(Boolean);
66
+ if (parts.length === 0) return "?";
67
+ const first = parts[0][0] ?? "";
68
+ const last = parts.length > 1 ? parts[parts.length - 1][0] ?? "" : "";
69
+ return (first + last).toUpperCase();
70
+ }
71
+ function isOwn(message, ownRoles) {
72
+ return message.own ?? ownRoles.includes(message.role);
73
+ }
74
+ var DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1e3;
75
+ function continuesGroup(prev, curr, windowMs) {
76
+ if (!prev) return false;
77
+ if (prev.role !== curr.role) return false;
78
+ if ((prev.author?.name ?? "") !== (curr.author?.name ?? "")) return false;
79
+ if ((prev.visibility ?? "public") !== (curr.visibility ?? "public")) return false;
80
+ if (prev.proposal || curr.proposal) return false;
81
+ if (curr.role === "system") return false;
82
+ const a = toDate(prev.createdAt);
83
+ const b = toDate(curr.createdAt);
84
+ if (!a || !b) return false;
85
+ const gap = b.getTime() - a.getTime();
86
+ return gap >= 0 && gap <= windowMs;
87
+ }
88
+ function formatBytes(n) {
89
+ if (n < 1024) return `${n} B`;
90
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
91
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
92
+ }
93
+ var BORDER = "border-[color:var(--fancy-border,var(--color-zinc-200))] dark:border-[color:var(--fancy-border-dark,var(--color-zinc-700))]";
94
+ var SURFACE = "bg-[color:var(--fancy-surface,var(--color-white))] dark:bg-[color:var(--fancy-surface-dark,var(--color-zinc-900))]";
95
+ var SURFACE_MUTED = "bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]";
96
+ var TEXT = "text-[color:var(--fancy-text,var(--color-zinc-900))] dark:text-[color:var(--fancy-text-dark,var(--color-zinc-100))]";
97
+ var TEXT_MUTED = "text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]";
98
+ function useAcknowledgedAction(onActionError) {
99
+ const [busy, setBusy] = react.useState(false);
100
+ const busyRef = react.useRef(false);
101
+ const mounted = react.useRef(true);
102
+ const onErrorRef = react.useRef(onActionError);
103
+ onErrorRef.current = onActionError;
104
+ react.useEffect(() => {
105
+ mounted.current = true;
106
+ return () => {
107
+ mounted.current = false;
108
+ };
109
+ }, []);
110
+ const run = react.useCallback((fn, ctx) => {
111
+ if (busyRef.current) return;
112
+ const result = fn();
113
+ if (!result || typeof result.then !== "function") return;
114
+ busyRef.current = true;
115
+ setBusy(true);
116
+ const settle = () => {
117
+ busyRef.current = false;
118
+ if (mounted.current) setBusy(false);
119
+ };
120
+ void Promise.resolve(result).then(settle, (error) => {
121
+ settle();
122
+ const onError = onErrorRef.current;
123
+ if (onError) onError(error, ctx);
124
+ else throw error;
125
+ });
126
+ }, []);
127
+ return [busy, run];
128
+ }
129
+ function AttachmentItem({
130
+ attachment,
131
+ message,
132
+ renderAttachment
133
+ }) {
134
+ let body;
135
+ if (renderAttachment) {
136
+ body = renderAttachment(attachment, message);
137
+ } else {
138
+ const href = chunk3U7PUZLI_cjs.sanitizeHref(attachment.url);
139
+ const size = typeof attachment.size === "number" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: chunkZU5Q77OT_cjs.cn("ml-1", TEXT_MUTED), children: [
140
+ "(",
141
+ formatBytes(attachment.size),
142
+ ")"
143
+ ] }) : null;
144
+ body = href ? /* @__PURE__ */ jsxRuntime.jsxs("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline underline-offset-2 break-all", children: [
145
+ attachment.name,
146
+ size
147
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "break-all", children: [
148
+ attachment.name,
149
+ size
150
+ ] });
151
+ }
152
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { "data-chat-attachment": attachment.id, className: "text-sm", children: body });
153
+ }
154
+ function ProposalFooter({
155
+ message,
156
+ labels,
157
+ busy,
158
+ onAccept,
159
+ onReject
160
+ }) {
161
+ const proposal = message.proposal;
162
+ const state = proposal.state ?? "pending";
163
+ if (state !== "pending") {
164
+ const text = state === "accepted" ? labels.accepted : state === "rejected" ? labels.rejected : labels.withdrawn;
165
+ const color = state === "accepted" ? "emerald" : state === "rejected" ? "red" : "zinc";
166
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(chunkMOU6O324_cjs.Badge, { color, variant: "soft", size: "sm", "data-chat-proposal-status": state, children: text }) });
167
+ }
168
+ if (!onAccept && !onReject) return null;
169
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2", children: [
170
+ onAccept && /* @__PURE__ */ jsxRuntime.jsx(
171
+ chunk3U7PUZLI_cjs.Button,
172
+ {
173
+ type: "button",
174
+ size: "sm",
175
+ color: "emerald",
176
+ disabled: busy,
177
+ "aria-busy": busy || void 0,
178
+ "data-chat-proposal-action": "accept",
179
+ onClick: onAccept,
180
+ children: proposal.acceptLabel ?? labels.accept
181
+ }
182
+ ),
183
+ onReject && /* @__PURE__ */ jsxRuntime.jsx(
184
+ chunk3U7PUZLI_cjs.Button,
185
+ {
186
+ type: "button",
187
+ size: "sm",
188
+ variant: "ghost",
189
+ disabled: busy,
190
+ "aria-busy": busy || void 0,
191
+ "data-chat-proposal-action": "reject",
192
+ onClick: onReject,
193
+ children: proposal.rejectLabel ?? labels.reject
194
+ }
195
+ )
196
+ ] });
197
+ }
198
+ function ChatMessage({
199
+ message,
200
+ actions,
201
+ grouped = false,
202
+ className,
203
+ ownRoles = ["user"],
204
+ streaming = true,
205
+ contentOptions,
206
+ labels: labelsProp,
207
+ formatTime = defaultFormatTime,
208
+ renderAttachment,
209
+ onProposalAccept,
210
+ onProposalReject,
211
+ onActivity,
212
+ onActionError,
213
+ classNames
214
+ }) {
215
+ const labels = mergeLabels(labelsProp);
216
+ const [busy, run] = useAcknowledgedAction(onActionError);
217
+ const status = message.status ?? "sent";
218
+ const visibility = message.visibility ?? "public";
219
+ const internal = visibility === "internal";
220
+ const own = isOwn(message, ownRoles);
221
+ const system = message.role === "system";
222
+ const proposal = message.proposal;
223
+ const date = toDate(message.createdAt);
224
+ const name = message.author?.name || roleLabel(message.role, labels);
225
+ const decide = (kind) => {
226
+ if (!proposal) return;
227
+ const cb = kind === "accept" ? onProposalAccept : onProposalReject;
228
+ if (!cb) return;
229
+ const ctx = {
230
+ type: kind === "accept" ? "proposal-accept" : "proposal-reject",
231
+ messageId: message.id,
232
+ proposalId: proposal.id
233
+ };
234
+ run(() => {
235
+ onActivity?.(ctx);
236
+ return cb(message);
237
+ }, ctx);
238
+ };
239
+ const body = message.format === "text" ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-content": "", className: "whitespace-pre-wrap break-words", children: message.content }) : /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-content": "", className: "min-w-0 break-words", children: /* @__PURE__ */ jsxRuntime.jsx(
240
+ chunkR3EJNGJV_cjs.ContentRenderer,
241
+ {
242
+ ...contentOptions,
243
+ value: message.content,
244
+ format: "markdown",
245
+ streaming: status === "streaming" ? streaming : false
246
+ }
247
+ ) });
248
+ const attachments = message.attachments && message.attachments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { "data-chat-attachments": "", "aria-label": labels.attachments, className: "mt-2 flex flex-col gap-1", children: message.attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsx(AttachmentItem, { attachment: a, message, renderAttachment }, a.id)) }) : null;
249
+ const statusText = status === "pending" ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-status-text": "pending", className: chunkZU5Q77OT_cjs.cn("mt-1 text-xs", TEXT_MUTED), children: labels.pending }) : status === "failed" ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-status-text": "failed", className: "mt-1 text-xs text-red-600 dark:text-red-400", children: labels.failed }) : null;
250
+ const actionsSlot = actions !== void 0 && actions !== null && actions !== false ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-message-actions": "", className: chunkZU5Q77OT_cjs.cn("mt-1 flex flex-wrap items-center gap-1", classNames?.actions), children: actions }) : null;
251
+ const rootAttrs = {
252
+ "data-chat-message": message.id,
253
+ "data-chat-role": message.role,
254
+ "data-chat-status": status,
255
+ "data-chat-visibility": visibility,
256
+ "data-chat-own": own ? "" : void 0,
257
+ "data-chat-grouped": grouped ? "" : void 0,
258
+ "aria-busy": status === "streaming" ? true : void 0
259
+ };
260
+ if (system && !proposal) {
261
+ return /* @__PURE__ */ jsxRuntime.jsxs(
262
+ "article",
263
+ {
264
+ ...rootAttrs,
265
+ className: chunkZU5Q77OT_cjs.cn("flex flex-col items-center px-4 py-1 text-center text-xs", TEXT_MUTED, className, classNames?.message),
266
+ children: [
267
+ internal && /* @__PURE__ */ jsxRuntime.jsx(chunkMOU6O324_cjs.Badge, { color: "amber", variant: "soft", size: "sm", "data-chat-internal-label": "", children: labels.internal }),
268
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkZU5Q77OT_cjs.cn("max-w-prose", classNames?.bubble), children: body }),
269
+ attachments,
270
+ actionsSlot
271
+ ]
272
+ }
273
+ );
274
+ }
275
+ const bubbleClass = chunkZU5Q77OT_cjs.cn(
276
+ "rounded-2xl px-3.5 py-2.5 text-sm",
277
+ TEXT,
278
+ internal ? "border border-dashed border-amber-400 bg-amber-50 dark:border-amber-500/60 dark:bg-amber-950/30" : proposal ? chunkZU5Q77OT_cjs.cn("border-2 border-violet-300 dark:border-violet-500/60", SURFACE) : own ? chunkZU5Q77OT_cjs.cn("border", BORDER, SURFACE) : SURFACE_MUTED,
279
+ status === "failed" && "ring-1 ring-red-400/70",
280
+ classNames?.bubble,
281
+ proposal && classNames?.proposal
282
+ );
283
+ const bubbleInner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
284
+ internal && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1.5", children: /* @__PURE__ */ jsxRuntime.jsx(chunkMOU6O324_cjs.Badge, { color: "amber", variant: "soft", size: "sm", "data-chat-internal-label": "", children: labels.internal }) }),
285
+ proposal && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-chat-proposal-header": "", className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMOU6O324_cjs.Badge, { color: "violet", variant: "soft", size: "sm", children: labels.proposal }),
287
+ proposal.title && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: proposal.title })
288
+ ] }),
289
+ body,
290
+ attachments,
291
+ proposal && /* @__PURE__ */ jsxRuntime.jsx(
292
+ ProposalFooter,
293
+ {
294
+ message,
295
+ labels,
296
+ busy,
297
+ onAccept: onProposalAccept ? () => decide("accept") : void 0,
298
+ onReject: onProposalReject ? () => decide("reject") : void 0
299
+ }
300
+ )
301
+ ] });
302
+ return /* @__PURE__ */ jsxRuntime.jsxs(
303
+ "article",
304
+ {
305
+ ...rootAttrs,
306
+ className: chunkZU5Q77OT_cjs.cn(
307
+ "flex gap-2.5 px-3",
308
+ grouped ? "pt-0.5" : "pt-3",
309
+ own && "flex-row-reverse",
310
+ className,
311
+ classNames?.message
312
+ ),
313
+ children: [
314
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 shrink-0", "aria-hidden": grouped || void 0, children: !grouped && /* @__PURE__ */ jsxRuntime.jsx(chunkBJL3VDRK_cjs.Avatar, { size: "sm", src: message.author?.avatar, alt: "", fallback: initials(name) }) }),
315
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkZU5Q77OT_cjs.cn("flex min-w-0 max-w-[85%] flex-col", own ? "items-end" : "items-start"), children: [
316
+ !grouped && /* @__PURE__ */ jsxRuntime.jsxs(
317
+ "div",
318
+ {
319
+ "data-chat-header": "",
320
+ className: chunkZU5Q77OT_cjs.cn("mb-1 flex items-baseline gap-2 text-xs", own && "flex-row-reverse", classNames?.header),
321
+ children: [
322
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-chat-author": "", className: chunkZU5Q77OT_cjs.cn("font-medium", TEXT), children: name }),
323
+ date && /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: date.toISOString(), className: TEXT_MUTED, children: formatTime(date) })
324
+ ]
325
+ }
326
+ ),
327
+ proposal ? /* @__PURE__ */ jsxRuntime.jsx(
328
+ "div",
329
+ {
330
+ "data-chat-proposal": proposal.id,
331
+ "data-chat-proposal-state": proposal.state ?? "pending",
332
+ "aria-busy": busy || void 0,
333
+ className: chunkZU5Q77OT_cjs.cn("min-w-0 max-w-full", bubbleClass),
334
+ children: bubbleInner
335
+ }
336
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { "data-chat-bubble": "", className: chunkZU5Q77OT_cjs.cn("min-w-0 max-w-full", bubbleClass), children: bubbleInner }),
337
+ statusText,
338
+ actionsSlot
339
+ ] })
340
+ ]
341
+ }
342
+ );
343
+ }
344
+ ChatMessage.displayName = "ChatMessage";
345
+ var TEXT_MUTED2 = "text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]";
346
+ var BORDER2 = "border-[color:var(--fancy-border,var(--color-zinc-200))] dark:border-[color:var(--fancy-border-dark,var(--color-zinc-700))]";
347
+ var useIsoLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
348
+ function ChatThread({
349
+ ref,
350
+ messages,
351
+ heading,
352
+ headingLevel = 2,
353
+ onLoadEarlier,
354
+ hasEarlier,
355
+ loadingEarlier = false,
356
+ stickToBottom = true,
357
+ bottomThreshold = 48,
358
+ onAtBottomChange,
359
+ separators = "none",
360
+ formatDay = defaultFormatDay,
361
+ groupConsecutive = false,
362
+ renderActions,
363
+ composer,
364
+ empty,
365
+ classNames,
366
+ className,
367
+ // message display
368
+ ownRoles = ["user"],
369
+ streaming = true,
370
+ contentOptions,
371
+ labels: labelsProp,
372
+ formatTime,
373
+ renderAttachment,
374
+ onProposalAccept,
375
+ onProposalReject,
376
+ onActivity,
377
+ onActionError,
378
+ onScroll: onScrollProp,
379
+ ...rest
380
+ }) {
381
+ const labels = mergeLabels(labelsProp);
382
+ const headingId = react.useId();
383
+ const logRef = react.useRef(null);
384
+ const contentRef = react.useRef(null);
385
+ const atBottomRef = react.useRef(true);
386
+ const seenRef = react.useRef(null);
387
+ const [atBottom, setAtBottom] = react.useState(true);
388
+ const [unseen, setUnseen] = react.useState(0);
389
+ const [loadBusy, runLoad] = useAcknowledgedAction(onActionError);
390
+ const onAtBottomChangeRef = react.useRef(onAtBottomChange);
391
+ onAtBottomChangeRef.current = onAtBottomChange;
392
+ const measure = react.useCallback(
393
+ (el) => el.scrollHeight - el.scrollTop - el.clientHeight <= bottomThreshold,
394
+ [bottomThreshold]
395
+ );
396
+ const commitAtBottom = react.useCallback((value) => {
397
+ const changed = atBottomRef.current !== value;
398
+ atBottomRef.current = value;
399
+ setAtBottom(value);
400
+ if (value) setUnseen(0);
401
+ if (changed) onAtBottomChangeRef.current?.(value);
402
+ }, []);
403
+ const scrollLogToBottom = react.useCallback(() => {
404
+ const el = logRef.current;
405
+ if (!el) return;
406
+ el.scrollTop = el.scrollHeight;
407
+ }, []);
408
+ useIsoLayoutEffect(() => {
409
+ const el = logRef.current;
410
+ if (!el) return;
411
+ const ids = messages.map((m) => m.id);
412
+ const first = ids[0];
413
+ const last = ids[ids.length - 1];
414
+ const prev = seenRef.current;
415
+ if (!prev) {
416
+ if (stickToBottom) scrollLogToBottom();
417
+ } else {
418
+ const prepended = prev.last !== void 0 && prev.last === last && prev.first !== void 0 && prev.first !== first && ids.includes(prev.first);
419
+ if (prepended) {
420
+ if (stickToBottom && atBottomRef.current) scrollLogToBottom();
421
+ else el.scrollTop = prev.scrollTop + (el.scrollHeight - prev.scrollHeight);
422
+ } else {
423
+ const lastIndex = prev.last === void 0 ? -1 : ids.indexOf(prev.last);
424
+ const appended = [];
425
+ for (let i = lastIndex + 1; i < messages.length; i++) {
426
+ if (!prev.ids.has(messages[i].id)) appended.push(messages[i]);
427
+ }
428
+ if (stickToBottom && atBottomRef.current) {
429
+ scrollLogToBottom();
430
+ } else if (stickToBottom && appended.some((m) => isOwn(m, ownRoles))) {
431
+ scrollLogToBottom();
432
+ } else if (appended.length > 0) {
433
+ setUnseen((n) => n + appended.length);
434
+ }
435
+ }
436
+ }
437
+ seenRef.current = { ids: new Set(ids), first, last, scrollHeight: el.scrollHeight, scrollTop: el.scrollTop };
438
+ const nowAtBottom = measure(el);
439
+ if (nowAtBottom !== atBottomRef.current || nowAtBottom) commitAtBottom(nowAtBottom);
440
+ }, [messages, stickToBottom, measure, commitAtBottom, scrollLogToBottom]);
441
+ react.useEffect(() => {
442
+ const content = contentRef.current;
443
+ if (!content || !stickToBottom || typeof ResizeObserver === "undefined") return;
444
+ const ro = new ResizeObserver(() => {
445
+ if (atBottomRef.current) scrollLogToBottom();
446
+ });
447
+ ro.observe(content);
448
+ return () => ro.disconnect();
449
+ }, [stickToBottom, scrollLogToBottom]);
450
+ const onScroll = react.useCallback(
451
+ (e) => {
452
+ onScrollProp?.(e);
453
+ if (seenRef.current) seenRef.current.scrollTop = e.currentTarget.scrollTop;
454
+ const now = measure(e.currentTarget);
455
+ if (now !== atBottomRef.current || now) commitAtBottom(now);
456
+ },
457
+ [measure, commitAtBottom, onScrollProp]
458
+ );
459
+ const jumpToLatest = react.useCallback(() => {
460
+ onActivity?.({ type: "jump-to-latest", unseen });
461
+ scrollLogToBottom();
462
+ commitAtBottom(true);
463
+ logRef.current?.focus({ preventScroll: true });
464
+ }, [onActivity, unseen, scrollLogToBottom, commitAtBottom]);
465
+ react.useImperativeHandle(
466
+ ref,
467
+ () => ({
468
+ scrollToLatest() {
469
+ scrollLogToBottom();
470
+ const el = logRef.current;
471
+ if (el) commitAtBottom(measure(el));
472
+ },
473
+ scrollToMessage(id) {
474
+ const el = logRef.current;
475
+ if (!el) return false;
476
+ const target = [...el.querySelectorAll("[data-chat-message]")].find(
477
+ (n) => n.getAttribute("data-chat-message") === id
478
+ );
479
+ if (!target) return false;
480
+ el.scrollTop = target.offsetTop;
481
+ commitAtBottom(measure(el));
482
+ return true;
483
+ },
484
+ get element() {
485
+ return logRef.current;
486
+ }
487
+ }),
488
+ [scrollLogToBottom, commitAtBottom, measure]
489
+ );
490
+ const showLoadEarlier = Boolean(onLoadEarlier) && (hasEarlier ?? true);
491
+ const earlierBusy = loadingEarlier || loadBusy;
492
+ const showJump = unseen > 0 && !atBottom;
493
+ const groupWindow = groupConsecutive === true ? DEFAULT_GROUP_WINDOW_MS : typeof groupConsecutive === "number" ? groupConsecutive : 0;
494
+ const items = [];
495
+ let prevDay;
496
+ messages.forEach((m, i) => {
497
+ const date = toDate(m.createdAt);
498
+ let separator = null;
499
+ if (separators === "day" && date) {
500
+ const key = dayKey(date);
501
+ if (key !== prevDay) {
502
+ separator = /* @__PURE__ */ jsxRuntime.jsxs(
503
+ "div",
504
+ {
505
+ "data-chat-day-separator": key,
506
+ className: chunkZU5Q77OT_cjs.cn("flex items-center gap-3 px-4 pt-4 text-xs", TEXT_MUTED2, classNames?.separator),
507
+ children: [
508
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: chunkZU5Q77OT_cjs.cn("h-px flex-1 border-t", BORDER2) }),
509
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatDay(date) }),
510
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: chunkZU5Q77OT_cjs.cn("h-px flex-1 border-t", BORDER2) })
511
+ ]
512
+ }
513
+ );
514
+ prevDay = key;
515
+ }
516
+ }
517
+ const grouped = groupWindow > 0 && !separator && continuesGroup(messages[i - 1], m, groupWindow);
518
+ items.push(
519
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
520
+ separator,
521
+ /* @__PURE__ */ jsxRuntime.jsx(
522
+ ChatMessage,
523
+ {
524
+ message: m,
525
+ grouped,
526
+ actions: renderActions?.(m),
527
+ ownRoles,
528
+ streaming,
529
+ contentOptions,
530
+ labels: labelsProp,
531
+ formatTime,
532
+ renderAttachment,
533
+ onProposalAccept,
534
+ onProposalReject,
535
+ onActivity,
536
+ onActionError,
537
+ classNames
538
+ }
539
+ )
540
+ ] }, m.id)
541
+ );
542
+ });
543
+ const H = `h${headingLevel}`;
544
+ return /* @__PURE__ */ jsxRuntime.jsxs(
545
+ "div",
546
+ {
547
+ "data-react-fancy-chat-thread": "",
548
+ ...rest,
549
+ className: chunkZU5Q77OT_cjs.cn("flex min-h-0 flex-col", className, classNames?.root),
550
+ children: [
551
+ heading !== void 0 && heading !== null && /* @__PURE__ */ jsxRuntime.jsx(
552
+ H,
553
+ {
554
+ id: headingId,
555
+ "data-react-fancy-chat-thread-heading": "",
556
+ className: chunkZU5Q77OT_cjs.cn("shrink-0 px-4 py-3 text-sm font-semibold", classNames?.heading),
557
+ children: heading
558
+ }
559
+ ),
560
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
561
+ /* @__PURE__ */ jsxRuntime.jsx(
562
+ "div",
563
+ {
564
+ ref: logRef,
565
+ "data-react-fancy-chat-thread-log": "",
566
+ role: "log",
567
+ "aria-live": "polite",
568
+ "aria-relevant": "additions",
569
+ "aria-label": heading ? void 0 : labels.log,
570
+ "aria-labelledby": heading ? headingId : void 0,
571
+ tabIndex: 0,
572
+ onScroll,
573
+ className: chunkZU5Q77OT_cjs.cn(
574
+ "relative min-h-0 flex-1 overflow-y-auto overscroll-contain focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--fancy-focus,var(--color-blue-500))]",
575
+ classNames?.log
576
+ ),
577
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: contentRef, className: "flex flex-col pb-3", children: [
578
+ showLoadEarlier && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center pt-3", children: /* @__PURE__ */ jsxRuntime.jsx(
579
+ chunk3U7PUZLI_cjs.Button,
580
+ {
581
+ type: "button",
582
+ size: "sm",
583
+ variant: "ghost",
584
+ "data-react-fancy-chat-thread-load-earlier": "",
585
+ disabled: earlierBusy,
586
+ "aria-busy": earlierBusy || void 0,
587
+ onClick: () => runLoad(() => {
588
+ onActivity?.({ type: "load-earlier" });
589
+ return onLoadEarlier();
590
+ }, { type: "load-earlier" }),
591
+ children: earlierBusy ? labels.loadingEarlier : labels.loadEarlier
592
+ }
593
+ ) }),
594
+ messages.length === 0 && empty !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-react-fancy-chat-thread-empty": "", className: chunkZU5Q77OT_cjs.cn("px-4 py-8 text-center text-sm", TEXT_MUTED2), children: empty }),
595
+ items
596
+ ] })
597
+ }
598
+ ),
599
+ showJump && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-3 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
600
+ chunk3U7PUZLI_cjs.Button,
601
+ {
602
+ type: "button",
603
+ size: "sm",
604
+ "data-react-fancy-chat-thread-jump": "",
605
+ "data-unseen": unseen,
606
+ iconTrailing: "chevron-down",
607
+ className: chunkZU5Q77OT_cjs.cn("pointer-events-auto shadow-md", classNames?.jump),
608
+ onClick: jumpToLatest,
609
+ children: labels.newMessages(unseen)
610
+ }
611
+ ) })
612
+ ] }),
613
+ composer !== void 0 && composer !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-react-fancy-chat-thread-composer": "", className: chunkZU5Q77OT_cjs.cn("shrink-0", classNames?.composer), children: composer })
614
+ ]
615
+ }
616
+ );
617
+ }
618
+ ChatThread.displayName = "ChatThread";
619
+
620
+ exports.ChatMessage = ChatMessage;
621
+ exports.ChatThread = ChatThread;
622
+ //# sourceMappingURL=chunk-5IE65DIH.cjs.map
623
+ //# sourceMappingURL=chunk-5IE65DIH.cjs.map