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