@djangocfg/ui-tools 2.1.368 → 2.1.369

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { MarkdownMessage } from './chunk-NWUT327A.mjs';
2
2
  import { __name } from './chunk-N2XQF2OL.mjs';
3
- import { createContext, forwardRef, memo, useCallback, useReducer, useRef, useEffect, useState, useMemo, useSyncExternalStore, useContext } from 'react';
3
+ import { createContext, forwardRef, memo, useRef, useImperativeHandle, useCallback, useMemo, useReducer, useEffect, useState, useSyncExternalStore, useContext } from 'react';
4
4
  import { cn, isDev } from '@djangocfg/ui-core/lib';
5
5
  import { consola } from 'consola';
6
6
  import { useLocalStorage, useMediaQuery } from '@djangocfg/ui-core/hooks';
@@ -9,6 +9,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
9
9
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
10
  import { Paperclip, Square, Send, File, X, Sparkles, AlertCircle, RefreshCw, ArrowDown, ExternalLink, ChevronDown, ChevronRight, Loader2, Copy, Pencil, Trash } from 'lucide-react';
11
11
  import { Button, Textarea, Spinner, Avatar, AvatarImage, AvatarFallback } from '@djangocfg/ui-core/components';
12
+ import { Virtuoso } from 'react-virtuoso';
12
13
 
13
14
  // src/tools/Chat/types.ts
14
15
  var DEFAULT_LABELS = {
@@ -692,13 +693,21 @@ function useChat(config) {
692
693
  };
693
694
  dispatch({ type: "MESSAGE_USER_ADD", message: userMsg });
694
695
  config.onMessageSent?.(userMsg);
696
+ let outbound = content;
697
+ if (config.onBeforeSend) {
698
+ try {
699
+ outbound = await config.onBeforeSend(content);
700
+ } catch (err) {
701
+ log.error.error("onBeforeSend threw \u2014 falling back to original content", err);
702
+ }
703
+ }
695
704
  if (streaming) {
696
- await consumeStream(sessionId, content, attachments);
705
+ await consumeStream(sessionId, outbound, attachments);
697
706
  } else {
698
- await consumeBuffered(sessionId, content, attachments);
707
+ await consumeBuffered(sessionId, outbound, attachments);
699
708
  }
700
709
  },
701
- [streaming, consumeStream, consumeBuffered, config, awaitSession]
710
+ [streaming, consumeStream, consumeBuffered, config, awaitSession, log]
702
711
  );
703
712
  const cancelStream = useCallback(() => {
704
713
  abortRef.current?.abort();
@@ -1198,6 +1207,7 @@ function ChatProvider({
1198
1207
  streaming,
1199
1208
  audio,
1200
1209
  debug,
1210
+ onBeforeSend,
1201
1211
  children
1202
1212
  }) {
1203
1213
  const audioApi = useChatAudio(audio ?? {});
@@ -1221,7 +1231,8 @@ function ChatProvider({
1221
1231
  onMessageSent,
1222
1232
  onMessageEnd,
1223
1233
  onStreamStart,
1224
- onError
1234
+ onError,
1235
+ onBeforeSend
1225
1236
  });
1226
1237
  const layout = useChatLayout({ defaultMode: "embedded" });
1227
1238
  const rootRef = useRef(null);
@@ -1277,9 +1288,28 @@ function useChatComposer(options) {
1277
1288
  disabled = false,
1278
1289
  submitOn = "enter",
1279
1290
  history = { enabled: true, size: LIMITS.composerHistorySize },
1280
- onPasteFiles
1291
+ onPasteFiles,
1292
+ persistKey
1281
1293
  } = options;
1282
- const [value, setValueState] = useState(initialValue);
1294
+ const initialFromStorage = (() => {
1295
+ if (!persistKey || typeof window === "undefined") return initialValue;
1296
+ try {
1297
+ const stored = window.sessionStorage.getItem(`chat:draft:${persistKey}`);
1298
+ return stored && stored.length > 0 ? stored : initialValue;
1299
+ } catch {
1300
+ return initialValue;
1301
+ }
1302
+ })();
1303
+ const [value, setValueState] = useState(initialFromStorage);
1304
+ useEffect(() => {
1305
+ if (!persistKey || typeof window === "undefined") return;
1306
+ try {
1307
+ const k = `chat:draft:${persistKey}`;
1308
+ if (value.length > 0) window.sessionStorage.setItem(k, value);
1309
+ else window.sessionStorage.removeItem(k);
1310
+ } catch {
1311
+ }
1312
+ }, [value, persistKey]);
1283
1313
  const [attachments, setAttachments] = useState([]);
1284
1314
  const [isSubmitting, setIsSubmitting] = useState(false);
1285
1315
  const textareaRef = useRef(null);
@@ -1424,138 +1454,6 @@ function useChatComposer(options) {
1424
1454
  };
1425
1455
  }
1426
1456
  __name(useChatComposer, "useChatComposer");
1427
- function useChatScroll(options) {
1428
- const {
1429
- containerRef,
1430
- bottomRef,
1431
- isStreaming = false,
1432
- bottomThresholdPx = 80,
1433
- messagesCount = 0
1434
- } = options;
1435
- const [isAtBottom, setIsAtBottom] = useState(true);
1436
- const [unreadCount, setUnreadCount] = useState(0);
1437
- const lastCountRef = useRef(messagesCount);
1438
- const stickyRef = useRef(true);
1439
- const wasStreamingRef = useRef(isStreaming);
1440
- const scrollToBottom = useCallback(
1441
- (smooth = false) => {
1442
- const el = containerRef.current;
1443
- if (!el) return;
1444
- el.scrollTo({
1445
- top: el.scrollHeight,
1446
- behavior: smooth ? "smooth" : "auto"
1447
- });
1448
- stickyRef.current = true;
1449
- setIsAtBottom(true);
1450
- setUnreadCount(0);
1451
- },
1452
- [containerRef]
1453
- );
1454
- const resetUnread = useCallback(() => setUnreadCount(0), []);
1455
- useEffect(() => {
1456
- const el = containerRef.current;
1457
- if (!el) return;
1458
- const onScroll = /* @__PURE__ */ __name(() => {
1459
- const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
1460
- const atBottom = distance <= bottomThresholdPx;
1461
- stickyRef.current = atBottom;
1462
- setIsAtBottom(atBottom);
1463
- if (atBottom) setUnreadCount(0);
1464
- }, "onScroll");
1465
- onScroll();
1466
- el.addEventListener("scroll", onScroll, { passive: true });
1467
- return () => {
1468
- el.removeEventListener("scroll", onScroll);
1469
- };
1470
- }, [containerRef, bottomThresholdPx]);
1471
- useEffect(() => {
1472
- const el = containerRef.current;
1473
- if (!el) return;
1474
- if (isStreaming) {
1475
- wasStreamingRef.current = true;
1476
- if (!stickyRef.current) return;
1477
- let raf = 0;
1478
- const tick = /* @__PURE__ */ __name(() => {
1479
- if (!stickyRef.current) return;
1480
- el.scrollTop = el.scrollHeight;
1481
- raf = requestAnimationFrame(tick);
1482
- }, "tick");
1483
- raf = requestAnimationFrame(tick);
1484
- return () => cancelAnimationFrame(raf);
1485
- }
1486
- if (wasStreamingRef.current && stickyRef.current) {
1487
- wasStreamingRef.current = false;
1488
- let raf1 = 0;
1489
- let raf2 = 0;
1490
- raf1 = requestAnimationFrame(() => {
1491
- el.scrollTop = el.scrollHeight;
1492
- raf2 = requestAnimationFrame(() => {
1493
- el.scrollTop = el.scrollHeight;
1494
- });
1495
- });
1496
- return () => {
1497
- cancelAnimationFrame(raf1);
1498
- cancelAnimationFrame(raf2);
1499
- };
1500
- }
1501
- wasStreamingRef.current = false;
1502
- return;
1503
- }, [containerRef, isStreaming]);
1504
- useEffect(() => {
1505
- if (messagesCount > lastCountRef.current) {
1506
- if (stickyRef.current) {
1507
- const el = containerRef.current;
1508
- if (el) el.scrollTop = el.scrollHeight;
1509
- } else {
1510
- setUnreadCount((n) => n + (messagesCount - lastCountRef.current));
1511
- }
1512
- }
1513
- lastCountRef.current = messagesCount;
1514
- }, [containerRef, messagesCount]);
1515
- useEffect(() => {
1516
- }, [bottomRef]);
1517
- return { isAtBottom, unreadCount, scrollToBottom, resetUnread };
1518
- }
1519
- __name(useChatScroll, "useChatScroll");
1520
- function useChatHistory(options) {
1521
- const { enabled = true, containerRef, topSentinelRef, hasMore, isLoadingMore, loadMore } = options;
1522
- const heightBeforeRef = useRef(null);
1523
- useEffect(() => {
1524
- if (heightBeforeRef.current == null) return;
1525
- const el = containerRef.current;
1526
- if (!el) {
1527
- heightBeforeRef.current = null;
1528
- return;
1529
- }
1530
- if (!isLoadingMore) {
1531
- const delta = el.scrollHeight - heightBeforeRef.current;
1532
- if (delta > 0) {
1533
- el.scrollTop += delta;
1534
- }
1535
- heightBeforeRef.current = null;
1536
- }
1537
- }, [containerRef, isLoadingMore]);
1538
- useEffect(() => {
1539
- if (!enabled || !hasMore) return;
1540
- const sentinel = topSentinelRef.current;
1541
- const root = containerRef.current;
1542
- if (!sentinel || !root) return;
1543
- const observer = new IntersectionObserver(
1544
- (entries) => {
1545
- const entry = entries[0];
1546
- if (!entry?.isIntersecting) return;
1547
- if (isLoadingMore) return;
1548
- const el = containerRef.current;
1549
- if (el) heightBeforeRef.current = el.scrollHeight;
1550
- void loadMore();
1551
- },
1552
- { root, threshold: 0, rootMargin: "200px 0px 0px 0px" }
1553
- );
1554
- observer.observe(sentinel);
1555
- return () => observer.disconnect();
1556
- }, [enabled, hasMore, isLoadingMore, containerRef, topSentinelRef, loadMore]);
1557
- }
1558
- __name(useChatHistory, "useChatHistory");
1559
1457
  function AttachmentsGrid({
1560
1458
  attachments,
1561
1459
  maxVisible,
@@ -2135,7 +2033,9 @@ var MessageBubbleInner = /* @__PURE__ */ __name(({
2135
2033
  onCopy,
2136
2034
  onRegenerate,
2137
2035
  onEdit,
2138
- onDelete
2036
+ onDelete,
2037
+ messageActionsExtra,
2038
+ streamingIndicator
2139
2039
  }) => {
2140
2040
  const isUser = isUserProp ?? message.role === "user";
2141
2041
  const isStreaming = !!message.isStreaming;
@@ -2198,7 +2098,7 @@ var MessageBubbleInner = /* @__PURE__ */ __name(({
2198
2098
  isUser ? "bg-primary text-primary-foreground rounded-tr-md" : isErr ? "bg-destructive/10 text-destructive rounded-tl-md border border-destructive/30" : "bg-muted text-foreground rounded-tl-md"
2199
2099
  ),
2200
2100
  children: [
2201
- isStreaming && message.toolActivity ? /* @__PURE__ */ jsx("div", { className: "mb-1.5", children: /* @__PURE__ */ jsx(StreamingIndicator, { label: message.toolActivity }) }) : null,
2101
+ isStreaming && message.toolActivity ? /* @__PURE__ */ jsx("div", { className: "mb-1.5", children: streamingIndicator ? streamingIndicator(message) : /* @__PURE__ */ jsx(StreamingIndicator, { label: message.toolActivity }) }) : null,
2202
2102
  message.content || !isStreaming ? /* @__PURE__ */ jsx(
2203
2103
  MarkdownMessage,
2204
2104
  {
@@ -2207,22 +2107,25 @@ var MessageBubbleInner = /* @__PURE__ */ __name(({
2207
2107
  isCompact,
2208
2108
  plainText: isStreaming
2209
2109
  }
2210
- ) : /* @__PURE__ */ jsx(StreamingIndicator, {})
2110
+ ) : streamingIndicator ? streamingIndicator(message) : /* @__PURE__ */ jsx(StreamingIndicator, {})
2211
2111
  ]
2212
2112
  }
2213
2113
  ),
2214
2114
  message.toolCalls?.length ? toolCallsRenderer ? toolCallsRenderer(message.toolCalls) : /* @__PURE__ */ jsx(ToolCalls, { calls: message.toolCalls, ...toolCallsProps }) : null,
2215
2115
  message.sources?.length && !isStreaming ? sourcesRenderer ? sourcesRenderer(message.sources) : /* @__PURE__ */ jsx(Sources, { sources: message.sources }) : null,
2216
- showActions && !isStreaming ? /* @__PURE__ */ jsx(
2217
- MessageActions,
2218
- {
2219
- role: message.role,
2220
- onCopy,
2221
- onRegenerate,
2222
- onEdit,
2223
- onDelete
2224
- }
2225
- ) : null,
2116
+ showActions && !isStreaming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
2117
+ /* @__PURE__ */ jsx(
2118
+ MessageActions,
2119
+ {
2120
+ role: message.role,
2121
+ onCopy,
2122
+ onRegenerate,
2123
+ onEdit,
2124
+ onDelete
2125
+ }
2126
+ ),
2127
+ messageActionsExtra ? messageActionsExtra(message) : null
2128
+ ] }) : null,
2226
2129
  showTimestamp ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-[10px] text-muted-foreground", children: new Date(message.createdAt).toLocaleTimeString() }) : null,
2227
2130
  afterContent
2228
2131
  ] })
@@ -2241,14 +2144,36 @@ var MessageList = forwardRef(/* @__PURE__ */ __name(function MessageList2({
2241
2144
  renderItem,
2242
2145
  renderEmpty,
2243
2146
  isLoadingMore: isLoadingMoreProp,
2244
- topSentinelRef,
2245
- bottomRef,
2147
+ onStartReached,
2246
2148
  className,
2247
- itemClassName
2149
+ itemClassName,
2150
+ noVirtualize = false,
2151
+ defaultItemHeight = 120,
2152
+ onAtBottomChange
2248
2153
  }, ref) {
2249
2154
  const ctx = useChatContextOptional();
2250
2155
  const messages = messagesProp ?? ctx?.messages ?? [];
2251
2156
  const isLoadingMore = isLoadingMoreProp ?? ctx?.isLoadingMore ?? false;
2157
+ const virtuosoRef = useRef(null);
2158
+ useImperativeHandle(
2159
+ ref,
2160
+ () => ({
2161
+ scrollToBottom: /* @__PURE__ */ __name((smooth = false) => {
2162
+ virtuosoRef.current?.scrollToIndex({
2163
+ index: "LAST",
2164
+ behavior: smooth ? "smooth" : "auto",
2165
+ align: "end"
2166
+ });
2167
+ }, "scrollToBottom"),
2168
+ scrollToIndex: /* @__PURE__ */ __name((index, smooth = false) => {
2169
+ virtuosoRef.current?.scrollToIndex({
2170
+ index,
2171
+ behavior: smooth ? "smooth" : "auto"
2172
+ });
2173
+ }, "scrollToIndex")
2174
+ }),
2175
+ []
2176
+ );
2252
2177
  const defaultRenderItem = useCallback(
2253
2178
  (m) => /* @__PURE__ */ jsx("div", { className: itemClassName, children: /* @__PURE__ */ jsx(
2254
2179
  MessageBubble,
@@ -2258,24 +2183,73 @@ var MessageList = forwardRef(/* @__PURE__ */ __name(function MessageList2({
2258
2183
  onRegenerate: ctx ? () => void ctx.regenerate(m.id) : void 0,
2259
2184
  onDelete: ctx ? () => ctx.deleteMessage(m.id) : void 0
2260
2185
  }
2261
- ) }, m.id),
2186
+ ) }),
2262
2187
  [itemClassName, ctx]
2263
2188
  );
2264
2189
  const itemRenderer = renderItem ?? defaultRenderItem;
2265
- return /* @__PURE__ */ jsxs(
2266
- "div",
2190
+ const computeItemKey = useCallback((index, m) => m.id ?? index, []);
2191
+ const startReachedHandler = useMemo(() => {
2192
+ if (!onStartReached) return void 0;
2193
+ let inFlight = false;
2194
+ return () => {
2195
+ if (inFlight || isLoadingMore) return;
2196
+ inFlight = true;
2197
+ try {
2198
+ onStartReached();
2199
+ } finally {
2200
+ queueMicrotask(() => {
2201
+ inFlight = false;
2202
+ });
2203
+ }
2204
+ };
2205
+ }, [onStartReached, isLoadingMore]);
2206
+ if (messages.length === 0) {
2207
+ return /* @__PURE__ */ jsx(
2208
+ "div",
2209
+ {
2210
+ role: "log",
2211
+ "aria-live": "polite",
2212
+ "aria-atomic": "false",
2213
+ className: cn("flex-1 overflow-y-auto", className),
2214
+ children: renderEmpty?.() ?? null
2215
+ }
2216
+ );
2217
+ }
2218
+ if (noVirtualize) {
2219
+ return /* @__PURE__ */ jsxs(
2220
+ "div",
2221
+ {
2222
+ role: "log",
2223
+ "aria-live": "polite",
2224
+ "aria-atomic": "false",
2225
+ className: cn("flex-1 overflow-y-auto", className),
2226
+ children: [
2227
+ isLoadingMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-muted-foreground" }) }) : null,
2228
+ messages.map((m, i) => /* @__PURE__ */ jsx("div", { children: itemRenderer(m, i) }, m.id ?? i))
2229
+ ]
2230
+ }
2231
+ );
2232
+ }
2233
+ return /* @__PURE__ */ jsx(
2234
+ Virtuoso,
2267
2235
  {
2268
- ref,
2236
+ ref: virtuosoRef,
2269
2237
  role: "log",
2270
2238
  "aria-live": "polite",
2271
2239
  "aria-atomic": "false",
2272
- className: cn("flex-1 overflow-y-auto", className),
2273
- children: [
2274
- /* @__PURE__ */ jsx("div", { ref: topSentinelRef, "aria-hidden": true }),
2275
- isLoadingMore ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-muted-foreground" }) }) : null,
2276
- messages.length === 0 ? renderEmpty?.() ?? null : messages.map((m, i) => itemRenderer(m, i)),
2277
- /* @__PURE__ */ jsx("div", { ref: bottomRef, "aria-hidden": true })
2278
- ]
2240
+ className: cn("flex-1", className),
2241
+ data: messages,
2242
+ computeItemKey,
2243
+ itemContent: (index, m) => itemRenderer(m, index),
2244
+ defaultItemHeight,
2245
+ followOutput: (isAtBottom) => isAtBottom ? "auto" : false,
2246
+ atBottomStateChange: onAtBottomChange,
2247
+ alignToBottom: true,
2248
+ startReached: startReachedHandler,
2249
+ components: isLoadingMore ? {
2250
+ Header: /* @__PURE__ */ __name(() => /* @__PURE__ */ jsx("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx(Spinner, { className: "size-4 text-muted-foreground" }) }), "Header")
2251
+ } : void 0,
2252
+ increaseViewportBy: { top: 200, bottom: 400 }
2279
2253
  }
2280
2254
  );
2281
2255
  }, "MessageList"));
@@ -2308,22 +2282,9 @@ function ChatRootShell({ className, listClassName, slots }) {
2308
2282
  onSubmit: /* @__PURE__ */ __name((content, attachments) => chat.sendMessage(content, attachments), "onSubmit"),
2309
2283
  disabled: chat.isStreaming
2310
2284
  });
2311
- const containerRef = useRef(null);
2312
- const bottomRef = useRef(null);
2313
- const topRef = useRef(null);
2314
- const scroll = useChatScroll({
2315
- containerRef,
2316
- bottomRef,
2317
- isStreaming: chat.isStreaming,
2318
- messagesCount: chat.messages.length
2319
- });
2320
- useChatHistory({
2321
- containerRef,
2322
- topSentinelRef: topRef,
2323
- hasMore: chat.hasMore,
2324
- isLoadingMore: chat.isLoadingMore,
2325
- loadMore: chat.loadMore
2326
- });
2285
+ const listRef = useRef(null);
2286
+ const [isAtBottom, setIsAtBottom] = useState(true);
2287
+ const handleStartReached = chat.hasMore && !chat.isLoadingMore ? () => void chat.loadMore() : void 0;
2327
2288
  const greeting = chat.config.greeting ?? "How can I help?";
2328
2289
  const description = chat.config.description;
2329
2290
  const suggestions = chat.config.suggestions;
@@ -2368,20 +2329,19 @@ function ChatRootShell({ className, listClassName, slots }) {
2368
2329
  /* @__PURE__ */ jsx(
2369
2330
  MessageList,
2370
2331
  {
2371
- ref: containerRef,
2372
- topSentinelRef: topRef,
2373
- bottomRef,
2332
+ ref: listRef,
2374
2333
  renderItem,
2375
2334
  renderEmpty: () => /* @__PURE__ */ jsx(Fragment, { children: emptyNode }),
2376
- className: listClassName
2335
+ className: listClassName,
2336
+ onStartReached: handleStartReached,
2337
+ onAtBottomChange: setIsAtBottom
2377
2338
  }
2378
2339
  ),
2379
2340
  /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-2 flex justify-center", children: slots.jumpToLatest ?? /* @__PURE__ */ jsx(
2380
2341
  JumpToLatest,
2381
2342
  {
2382
- visible: !scroll.isAtBottom,
2383
- unreadCount: scroll.unreadCount,
2384
- onClick: () => scroll.scrollToBottom(true)
2343
+ visible: !isAtBottom,
2344
+ onClick: () => listRef.current?.scrollToBottom(true)
2385
2345
  }
2386
2346
  ) })
2387
2347
  ] }),
@@ -2409,6 +2369,6 @@ function copy2(text) {
2409
2369
  }
2410
2370
  __name(copy2, "copy");
2411
2371
 
2412
- export { Attachments, AttachmentsGrid, AttachmentsList, CHAT_EVENT_NAME, CSS_VARS, ChatProvider, ChatRoot, Composer, DEFAULT_LABELS, DEFAULT_SIDEBAR, DEFAULT_Z_INDEX, EmptyState, ErrorBanner, HOTKEYS, JumpToLatest, LIMITS, MessageActions, MessageBubble, MessageList, STORAGE_KEYS, Sources, StreamingIndicator, ToolCalls, createId, createTokenBuffer, deriveInitials, getChatLogger, initialState, reducer, resolvePersona, useChat, useChatAudio, useChatAudioPrefs, useChatComposer, useChatContext, useChatContextOptional, useChatHistory, useChatLayout, useChatScroll };
2413
- //# sourceMappingURL=chunk-WGU5BEZX.mjs.map
2414
- //# sourceMappingURL=chunk-WGU5BEZX.mjs.map
2372
+ export { Attachments, AttachmentsGrid, AttachmentsList, CHAT_EVENT_NAME, CSS_VARS, ChatProvider, ChatRoot, Composer, DEFAULT_LABELS, DEFAULT_SIDEBAR, DEFAULT_Z_INDEX, EmptyState, ErrorBanner, HOTKEYS, JumpToLatest, LIMITS, MessageActions, MessageBubble, MessageList, STORAGE_KEYS, Sources, StreamingIndicator, ToolCalls, createId, createTokenBuffer, deriveInitials, getChatLogger, initialState, reducer, resolvePersona, useChat, useChatAudio, useChatAudioPrefs, useChatComposer, useChatContext, useChatContextOptional, useChatLayout };
2373
+ //# sourceMappingURL=chunk-YLIYXSUO.mjs.map
2374
+ //# sourceMappingURL=chunk-YLIYXSUO.mjs.map