@djangocfg/ui-tools 2.1.334 → 2.1.336
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.
- package/README.md +68 -2
- package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
- package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
- package/dist/ChatRoot-PNNGQCYF.css +7 -0
- package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
- package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
- package/dist/ChatRoot-UUKTYM4N.cjs.map +1 -0
- package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
- package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
- package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
- package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
- package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
- package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
- package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
- package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
- package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
- package/dist/{JsonSchemaForm-OSPUUUHM.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
- package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
- package/dist/{JsonSchemaForm-TSLX2GRO.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
- package/dist/JsonTree-55625VVH.mjs +5 -0
- package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
- package/dist/JsonTree-DCM5QGWF.cjs +11 -0
- package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
- package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
- package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
- package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
- package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
- package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
- package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
- package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
- package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
- package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
- package/dist/Player-BRV7XTWR.mjs +4 -0
- package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
- package/dist/Player-PM7F7DD7.cjs +13 -0
- package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
- package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
- package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
- package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
- package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
- package/dist/TreeRoot-N72OYKXU.cjs +19 -0
- package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
- package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
- package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
- package/dist/chunk-2ZLKZ5VR.mjs +631 -0
- package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
- package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
- package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
- package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
- package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
- package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
- package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
- package/dist/chunk-B5AWZOHJ.cjs +649 -0
- package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
- package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
- package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
- package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
- package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
- package/dist/{chunk-4IW7GZFQ.cjs → chunk-FEN5S772.cjs} +74 -48
- package/dist/chunk-FEN5S772.cjs.map +1 -0
- package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
- package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
- package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
- package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
- package/dist/{chunk-EXGXUK2N.mjs → chunk-GYIO7W7M.mjs} +41 -15
- package/dist/chunk-GYIO7W7M.mjs.map +1 -0
- package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
- package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
- package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
- package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
- package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
- package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
- package/dist/chunk-KRETIZU6.mjs +2218 -0
- package/dist/chunk-KRETIZU6.mjs.map +1 -0
- package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
- package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
- package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
- package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
- package/dist/chunk-NRXYYO5V.cjs +2257 -0
- package/dist/chunk-NRXYYO5V.cjs.map +1 -0
- package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
- package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
- package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
- package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
- package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
- package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
- package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
- package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
- package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
- package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
- package/dist/chunk-QW4RBGHN.cjs +961 -0
- package/dist/chunk-QW4RBGHN.cjs.map +1 -0
- package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
- package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
- package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
- package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
- package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
- package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
- package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
- package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
- package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
- package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
- package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
- package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
- package/dist/components-EHOGXATG.cjs +22 -0
- package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
- package/dist/components-MQ6DR7TX.cjs +26 -0
- package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
- package/dist/components-XRX7QGLB.mjs +5 -0
- package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
- package/dist/components-YATKRWLH.mjs +5 -0
- package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
- package/dist/file-icon/index.cjs +6 -6
- package/dist/file-icon/index.mjs +1 -1
- package/dist/index.cjs +735 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +972 -39
- package/dist/index.d.ts +972 -39
- package/dist/index.mjs +387 -31
- package/dist/index.mjs.map +1 -1
- package/dist/tree/index.cjs +38 -38
- package/dist/tree/index.d.cts +2 -2
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.mjs +3 -3
- package/package.json +6 -6
- package/src/index.ts +5 -0
- package/src/stories/index.ts +3 -1
- package/src/tools/Chat/Chat.story.tsx +1006 -0
- package/src/tools/Chat/README.md +528 -0
- package/src/tools/Chat/components/Attachments.tsx +192 -0
- package/src/tools/Chat/components/ChatRoot.tsx +201 -0
- package/src/tools/Chat/components/Composer.tsx +134 -0
- package/src/tools/Chat/components/EmptyState.tsx +47 -0
- package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
- package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
- package/src/tools/Chat/components/MessageActions.tsx +72 -0
- package/src/tools/Chat/components/MessageBubble.tsx +228 -0
- package/src/tools/Chat/components/MessageList.tsx +82 -0
- package/src/tools/Chat/components/Sources.tsx +55 -0
- package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
- package/src/tools/Chat/components/ToolCalls.tsx +172 -0
- package/src/tools/Chat/components/index.ts +24 -0
- package/src/tools/Chat/config.ts +55 -0
- package/src/tools/Chat/context/ChatProvider.tsx +122 -0
- package/src/tools/Chat/context/index.ts +9 -0
- package/src/tools/Chat/core/audio/audioBus.ts +172 -0
- package/src/tools/Chat/core/audio/index.ts +8 -0
- package/src/tools/Chat/core/audio/preferences.ts +68 -0
- package/src/tools/Chat/core/audio/types.ts +49 -0
- package/src/tools/Chat/core/ids.ts +16 -0
- package/src/tools/Chat/core/index.ts +5 -0
- package/src/tools/Chat/core/markdown.ts +56 -0
- package/src/tools/Chat/core/payload-dispatch.ts +54 -0
- package/src/tools/Chat/core/persona.ts +35 -0
- package/src/tools/Chat/core/reducer.ts +335 -0
- package/src/tools/Chat/core/transport/http.ts +167 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mock.ts +134 -0
- package/src/tools/Chat/core/transport/sse.ts +116 -0
- package/src/tools/Chat/core/transport/types.ts +24 -0
- package/src/tools/Chat/hooks/index.ts +26 -0
- package/src/tools/Chat/hooks/useChat.ts +440 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
- package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
- package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
- package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
- package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
- package/src/tools/Chat/index.ts +158 -0
- package/src/tools/Chat/lazy.tsx +14 -0
- package/src/tools/Chat/types.ts +237 -0
- package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +32 -1
- package/src/tools/Map/README.md +384 -0
- package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
- package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
- package/dist/JsonSchemaForm-OSPUUUHM.cjs +0 -13
- package/dist/JsonSchemaForm-TSLX2GRO.mjs +0 -4
- package/dist/JsonTree-F27RMYSI.cjs +0 -11
- package/dist/JsonTree-QTJYSHCV.mjs +0 -5
- package/dist/Player-M3GC3VPE.mjs +0 -4
- package/dist/Player-ZL2X5LGG.cjs +0 -13
- package/dist/TreeRoot-A3J65L6F.mjs +0 -4
- package/dist/TreeRoot-DSK5JILT.cjs +0 -19
- package/dist/chunk-4IW7GZFQ.cjs.map +0 -1
- package/dist/chunk-62Y65TGK.mjs.map +0 -1
- package/dist/chunk-EXGXUK2N.mjs.map +0 -1
- package/dist/chunk-TKSFZHCG.cjs +0 -1597
- package/dist/chunk-TKSFZHCG.cjs.map +0 -1
- package/dist/components-5UXYNAKR.cjs +0 -22
- package/dist/components-CFXOEVPN.mjs +0 -5
- package/dist/components-WYEZL5TE.cjs +0 -26
- package/dist/components-ZAGG2PBO.mjs +0 -5
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseChatScrollOptions {
|
|
6
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
7
|
+
bottomRef: RefObject<HTMLElement | null>;
|
|
8
|
+
isStreaming?: boolean;
|
|
9
|
+
/** Distance from bottom (px) considered "at bottom". */
|
|
10
|
+
bottomThresholdPx?: number;
|
|
11
|
+
/** Bump key — increment when a new message arrives so the hook re-evaluates auto-scroll. */
|
|
12
|
+
messagesCount?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseChatScrollReturn {
|
|
16
|
+
isAtBottom: boolean;
|
|
17
|
+
unreadCount: number;
|
|
18
|
+
scrollToBottom: (smooth?: boolean) => void;
|
|
19
|
+
resetUnread: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useChatScroll(options: UseChatScrollOptions): UseChatScrollReturn {
|
|
23
|
+
const {
|
|
24
|
+
containerRef,
|
|
25
|
+
bottomRef,
|
|
26
|
+
isStreaming = false,
|
|
27
|
+
bottomThresholdPx = 80,
|
|
28
|
+
messagesCount = 0,
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
32
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
33
|
+
const lastCountRef = useRef(messagesCount);
|
|
34
|
+
const stickyRef = useRef(true);
|
|
35
|
+
const wasStreamingRef = useRef(isStreaming);
|
|
36
|
+
|
|
37
|
+
const scrollToBottom = useCallback(
|
|
38
|
+
(smooth = false) => {
|
|
39
|
+
const el = containerRef.current;
|
|
40
|
+
if (!el) return;
|
|
41
|
+
el.scrollTo({
|
|
42
|
+
top: el.scrollHeight,
|
|
43
|
+
behavior: smooth ? 'smooth' : 'auto',
|
|
44
|
+
});
|
|
45
|
+
stickyRef.current = true;
|
|
46
|
+
setIsAtBottom(true);
|
|
47
|
+
setUnreadCount(0);
|
|
48
|
+
},
|
|
49
|
+
[containerRef],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const resetUnread = useCallback(() => setUnreadCount(0), []);
|
|
53
|
+
|
|
54
|
+
// Track scroll position relative to bottom.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const el = containerRef.current;
|
|
57
|
+
if (!el) return;
|
|
58
|
+
const onScroll = () => {
|
|
59
|
+
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
60
|
+
const atBottom = distance <= bottomThresholdPx;
|
|
61
|
+
stickyRef.current = atBottom;
|
|
62
|
+
setIsAtBottom(atBottom);
|
|
63
|
+
if (atBottom) setUnreadCount(0);
|
|
64
|
+
};
|
|
65
|
+
onScroll();
|
|
66
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
67
|
+
return () => {
|
|
68
|
+
el.removeEventListener('scroll', onScroll);
|
|
69
|
+
};
|
|
70
|
+
}, [containerRef, bottomThresholdPx]);
|
|
71
|
+
|
|
72
|
+
// Stick to bottom while streaming, and one extra rAF after stream ends so
|
|
73
|
+
// the final layout (markdown re-render, sources/tool-call panels) doesn't
|
|
74
|
+
// push the latest content out of view.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const el = containerRef.current;
|
|
77
|
+
if (!el) return;
|
|
78
|
+
|
|
79
|
+
if (isStreaming) {
|
|
80
|
+
wasStreamingRef.current = true;
|
|
81
|
+
if (!stickyRef.current) return;
|
|
82
|
+
let raf = 0;
|
|
83
|
+
const tick = () => {
|
|
84
|
+
if (!stickyRef.current) return;
|
|
85
|
+
el.scrollTop = el.scrollHeight;
|
|
86
|
+
raf = requestAnimationFrame(tick);
|
|
87
|
+
};
|
|
88
|
+
raf = requestAnimationFrame(tick);
|
|
89
|
+
return () => cancelAnimationFrame(raf);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Stream just ended — flush one more scroll on the next two frames so
|
|
93
|
+
// both markdown swap and any post-stream panel insertions are reflected.
|
|
94
|
+
if (wasStreamingRef.current && stickyRef.current) {
|
|
95
|
+
wasStreamingRef.current = false;
|
|
96
|
+
let raf1 = 0;
|
|
97
|
+
let raf2 = 0;
|
|
98
|
+
raf1 = requestAnimationFrame(() => {
|
|
99
|
+
el.scrollTop = el.scrollHeight;
|
|
100
|
+
raf2 = requestAnimationFrame(() => {
|
|
101
|
+
el.scrollTop = el.scrollHeight;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return () => {
|
|
105
|
+
cancelAnimationFrame(raf1);
|
|
106
|
+
cancelAnimationFrame(raf2);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
wasStreamingRef.current = false;
|
|
110
|
+
return;
|
|
111
|
+
}, [containerRef, isStreaming]);
|
|
112
|
+
|
|
113
|
+
// On message count increase, decide whether to scroll or bump unread.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (messagesCount > lastCountRef.current) {
|
|
116
|
+
if (stickyRef.current) {
|
|
117
|
+
const el = containerRef.current;
|
|
118
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
119
|
+
} else {
|
|
120
|
+
setUnreadCount((n) => n + (messagesCount - lastCountRef.current));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lastCountRef.current = messagesCount;
|
|
124
|
+
}, [containerRef, messagesCount]);
|
|
125
|
+
|
|
126
|
+
// Watch bottom sentinel just for symmetry/future hooks.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
void bottomRef;
|
|
129
|
+
}, [bottomRef]);
|
|
130
|
+
|
|
131
|
+
return { isAtBottom, unreadCount, scrollToBottom, resetUnread };
|
|
132
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @djangocfg/ui-tools — Chat
|
|
5
|
+
*
|
|
6
|
+
* Decomposed, transport-agnostic chat. See @dev/@refactoring7-chat/ for design.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
ChatRole,
|
|
12
|
+
ChatMessage,
|
|
13
|
+
ChatPersona,
|
|
14
|
+
ChatToolCall,
|
|
15
|
+
ChatAttachment,
|
|
16
|
+
ChatSource,
|
|
17
|
+
ChatDisplayMode,
|
|
18
|
+
ChatUserContext,
|
|
19
|
+
ChatAssistantContext,
|
|
20
|
+
ChatPrefs,
|
|
21
|
+
ChatConfig,
|
|
22
|
+
ChatLabels,
|
|
23
|
+
ChatTransport,
|
|
24
|
+
ChatStreamEvent,
|
|
25
|
+
CreateSessionOptions,
|
|
26
|
+
SessionInfo,
|
|
27
|
+
HistoryPage,
|
|
28
|
+
StreamOptions,
|
|
29
|
+
SendOptions,
|
|
30
|
+
} from './types';
|
|
31
|
+
export { DEFAULT_LABELS } from './types';
|
|
32
|
+
|
|
33
|
+
// Config
|
|
34
|
+
export {
|
|
35
|
+
STORAGE_KEYS,
|
|
36
|
+
CSS_VARS,
|
|
37
|
+
DEFAULT_Z_INDEX,
|
|
38
|
+
LIMITS,
|
|
39
|
+
DEFAULT_SIDEBAR,
|
|
40
|
+
HOTKEYS,
|
|
41
|
+
CHAT_EVENT_NAME,
|
|
42
|
+
type ChatEventDetail,
|
|
43
|
+
} from './config';
|
|
44
|
+
|
|
45
|
+
// Core (pure)
|
|
46
|
+
export {
|
|
47
|
+
reducer,
|
|
48
|
+
initialState,
|
|
49
|
+
createId,
|
|
50
|
+
createTokenBuffer,
|
|
51
|
+
resolvePersona,
|
|
52
|
+
deriveInitials,
|
|
53
|
+
type ChatState,
|
|
54
|
+
type ChatAction,
|
|
55
|
+
type TokenBuffer,
|
|
56
|
+
} from './core';
|
|
57
|
+
|
|
58
|
+
// Transport
|
|
59
|
+
export {
|
|
60
|
+
createHttpTransport,
|
|
61
|
+
createMockTransport,
|
|
62
|
+
parseSSE,
|
|
63
|
+
TransportError,
|
|
64
|
+
type HttpTransportConfig,
|
|
65
|
+
type MockTransportOptions,
|
|
66
|
+
type ParseSSEOptions,
|
|
67
|
+
} from './core/transport';
|
|
68
|
+
|
|
69
|
+
// Hooks
|
|
70
|
+
export {
|
|
71
|
+
useChat,
|
|
72
|
+
useChatComposer,
|
|
73
|
+
useChatScroll,
|
|
74
|
+
useChatHistory,
|
|
75
|
+
useChatLayout,
|
|
76
|
+
useChatAudio,
|
|
77
|
+
type UseChatConfig,
|
|
78
|
+
type UseChatReturn,
|
|
79
|
+
type UseChatComposerOptions,
|
|
80
|
+
type UseChatComposerReturn,
|
|
81
|
+
type UseChatScrollOptions,
|
|
82
|
+
type UseChatScrollReturn,
|
|
83
|
+
type UseChatHistoryOptions,
|
|
84
|
+
type UseChatLayoutConfig,
|
|
85
|
+
type UseChatLayoutReturn,
|
|
86
|
+
} from './hooks';
|
|
87
|
+
|
|
88
|
+
// Audio
|
|
89
|
+
export type {
|
|
90
|
+
ChatAudioEvent,
|
|
91
|
+
ChatAudioSounds,
|
|
92
|
+
ChatAudioConfig,
|
|
93
|
+
UseChatAudioReturn,
|
|
94
|
+
} from './core/audio';
|
|
95
|
+
export { useChatAudioPrefs } from './core/audio';
|
|
96
|
+
|
|
97
|
+
// Tool-call payload dispatcher
|
|
98
|
+
export {
|
|
99
|
+
dispatchToolPayload,
|
|
100
|
+
isPlainObject,
|
|
101
|
+
isLatLng,
|
|
102
|
+
isGeoJSONFeatureCollection,
|
|
103
|
+
isStringValue,
|
|
104
|
+
type ToolPayloadMatcher,
|
|
105
|
+
type ToolPayloadFallback,
|
|
106
|
+
} from './core/payload-dispatch';
|
|
107
|
+
|
|
108
|
+
// Lightbox helpers
|
|
109
|
+
export { useChatLightbox, type UseChatLightboxReturn, type ChatLightboxState } from './hooks';
|
|
110
|
+
export { collectImageAttachments } from './utils/collectImageAttachments';
|
|
111
|
+
|
|
112
|
+
// Context
|
|
113
|
+
export {
|
|
114
|
+
ChatProvider,
|
|
115
|
+
useChatContext,
|
|
116
|
+
useChatContextOptional,
|
|
117
|
+
type ChatContextValue,
|
|
118
|
+
type ChatProviderProps,
|
|
119
|
+
} from './context';
|
|
120
|
+
|
|
121
|
+
// Components
|
|
122
|
+
export {
|
|
123
|
+
ChatRoot,
|
|
124
|
+
MessageList,
|
|
125
|
+
MessageBubble,
|
|
126
|
+
MessageActions,
|
|
127
|
+
Composer,
|
|
128
|
+
Sources,
|
|
129
|
+
ToolCalls,
|
|
130
|
+
Attachments,
|
|
131
|
+
AttachmentsGrid,
|
|
132
|
+
AttachmentsList,
|
|
133
|
+
EmptyState,
|
|
134
|
+
ErrorBanner,
|
|
135
|
+
JumpToLatest,
|
|
136
|
+
StreamingIndicator,
|
|
137
|
+
type ChatRootProps,
|
|
138
|
+
type MessageListProps,
|
|
139
|
+
type MessageBubbleProps,
|
|
140
|
+
type MessageActionsProps,
|
|
141
|
+
type ComposerProps,
|
|
142
|
+
type SourcesProps,
|
|
143
|
+
type ToolCallsProps,
|
|
144
|
+
type ToolPayloadKind,
|
|
145
|
+
type AttachmentsProps,
|
|
146
|
+
type AttachmentsGridProps,
|
|
147
|
+
type AttachmentsListProps,
|
|
148
|
+
type AttachmentRenderer,
|
|
149
|
+
type AttachmentRendererArgs,
|
|
150
|
+
type AttachmentRendererMap,
|
|
151
|
+
type EmptyStateProps,
|
|
152
|
+
type ErrorBannerProps,
|
|
153
|
+
type JumpToLatestProps,
|
|
154
|
+
type StreamingIndicatorProps,
|
|
155
|
+
} from './components';
|
|
156
|
+
|
|
157
|
+
// Lazy preset
|
|
158
|
+
export { LazyChat } from './lazy';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent, LoadingFallback } from '../../components/lazy-wrapper';
|
|
4
|
+
import type { ChatRootProps } from './components/ChatRoot';
|
|
5
|
+
|
|
6
|
+
export const LazyChat = createLazyComponent<ChatRootProps>(
|
|
7
|
+
() => import('./components/ChatRoot').then((m) => ({ default: m.ChatRoot })),
|
|
8
|
+
{
|
|
9
|
+
displayName: 'LazyChat',
|
|
10
|
+
fallback: <LoadingFallback minHeight={320} text="Loading chat…" />,
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export type { ChatRootProps };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the Chat tool.
|
|
3
|
+
*
|
|
4
|
+
* Shapes here are the contract between transport adapters, the reducer,
|
|
5
|
+
* the hooks layer and the components. Keep serializable (no Date, no Map).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ChatRole = 'user' | 'assistant' | 'system';
|
|
9
|
+
|
|
10
|
+
export interface ChatToolCall {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
input: unknown;
|
|
14
|
+
output?: unknown;
|
|
15
|
+
/** Live buffer accumulated during streaming. Replaced by `output` on completion. */
|
|
16
|
+
streamingText?: string;
|
|
17
|
+
status: 'running' | 'success' | 'error' | 'cancelled';
|
|
18
|
+
startedAt: number;
|
|
19
|
+
endedAt?: number;
|
|
20
|
+
/** Optional grouping label for parallel/fan-out execution. */
|
|
21
|
+
sourceHostname?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChatAttachment {
|
|
25
|
+
id: string;
|
|
26
|
+
type: 'image' | 'file' | 'audio' | 'video';
|
|
27
|
+
url: string;
|
|
28
|
+
thumbnailUrl?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
sizeBytes?: number;
|
|
32
|
+
status?: 'uploading' | 'ready' | 'error';
|
|
33
|
+
/** 0..1 while uploading. */
|
|
34
|
+
progress?: number;
|
|
35
|
+
/** Extracted text from images (OCR). */
|
|
36
|
+
ocrText?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatSource {
|
|
40
|
+
title: string;
|
|
41
|
+
url: string;
|
|
42
|
+
snippet?: string;
|
|
43
|
+
chunkIndex?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ChatPersona {
|
|
47
|
+
/** Display name. */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Avatar image URL. */
|
|
50
|
+
avatarUrl?: string;
|
|
51
|
+
/** Initials fallback when no avatar. Auto-derived from `name` if absent. */
|
|
52
|
+
initials?: string;
|
|
53
|
+
/** Tooltip / aria description. */
|
|
54
|
+
description?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ChatMessage {
|
|
58
|
+
id: string;
|
|
59
|
+
role: ChatRole;
|
|
60
|
+
content: string;
|
|
61
|
+
/** epoch ms — serializable. */
|
|
62
|
+
createdAt: number;
|
|
63
|
+
isStreaming?: boolean;
|
|
64
|
+
isError?: boolean;
|
|
65
|
+
/** Bumps on edit so memo keys invalidate. */
|
|
66
|
+
version?: number;
|
|
67
|
+
/** Per-message persona override (multi-user / multi-bot). Falls back to
|
|
68
|
+
* `ChatConfig.user` / `ChatConfig.assistant` when absent. */
|
|
69
|
+
sender?: ChatPersona;
|
|
70
|
+
/** Simple status indicator above message ("Searching knowledge base..."). */
|
|
71
|
+
toolActivity?: string;
|
|
72
|
+
toolCalls?: ChatToolCall[];
|
|
73
|
+
attachments?: ChatAttachment[];
|
|
74
|
+
sources?: ChatSource[];
|
|
75
|
+
tokensIn?: number;
|
|
76
|
+
tokensOut?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ChatDisplayMode = 'closed' | 'embedded' | 'floating' | 'sidebar' | 'fullscreen';
|
|
80
|
+
|
|
81
|
+
export interface ChatUserContext extends ChatPersona {
|
|
82
|
+
email?: string;
|
|
83
|
+
language?: string;
|
|
84
|
+
role?: string;
|
|
85
|
+
/** Free-form custom data forwarded to transport metadata. */
|
|
86
|
+
custom?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ChatAssistantContext extends ChatPersona {
|
|
90
|
+
/** Model identifier (gpt-4o, claude-opus, …). */
|
|
91
|
+
model?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ChatPrefs {
|
|
95
|
+
/** Submit hotkey for the composer. */
|
|
96
|
+
submitOn?: 'enter' | 'cmd+enter';
|
|
97
|
+
/** UI density. */
|
|
98
|
+
density?: 'comfortable' | 'compact';
|
|
99
|
+
/** Locale forwarded to transport metadata. */
|
|
100
|
+
locale?: string;
|
|
101
|
+
/** Show timestamps on each bubble. */
|
|
102
|
+
showTimestamps?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ChatConfig {
|
|
106
|
+
/** Window title / aria-label. */
|
|
107
|
+
title?: string;
|
|
108
|
+
/** Composer placeholder. */
|
|
109
|
+
placeholder?: string;
|
|
110
|
+
/** Empty-state greeting. */
|
|
111
|
+
greeting?: string;
|
|
112
|
+
/** Empty-state description. */
|
|
113
|
+
description?: string;
|
|
114
|
+
/** Suggested prompts shown on empty conversation. */
|
|
115
|
+
suggestions?: Array<{ label: string; prompt: string }>;
|
|
116
|
+
/** Locale forwarded to the transport via stream metadata. */
|
|
117
|
+
locale?: string;
|
|
118
|
+
/** Project / chat slug forwarded to the transport. */
|
|
119
|
+
slug?: string;
|
|
120
|
+
/** UI density. Use `prefs.density` for the same effect; this stays for
|
|
121
|
+
* backwards compatibility. */
|
|
122
|
+
density?: 'comfortable' | 'compact';
|
|
123
|
+
/** Identity of the human author. Renders avatar / name on user bubbles
|
|
124
|
+
* and gets stamped on outgoing messages as `message.sender`. */
|
|
125
|
+
user?: ChatUserContext;
|
|
126
|
+
/** Identity of the assistant. Renders avatar / name on assistant bubbles. */
|
|
127
|
+
assistant?: ChatAssistantContext;
|
|
128
|
+
/** UI preferences. */
|
|
129
|
+
prefs?: ChatPrefs;
|
|
130
|
+
/** Visual labels (i18n is the host's job). */
|
|
131
|
+
labels?: Partial<ChatLabels>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ChatLabels {
|
|
135
|
+
send: string;
|
|
136
|
+
cancel: string;
|
|
137
|
+
copy: string;
|
|
138
|
+
regenerate: string;
|
|
139
|
+
edit: string;
|
|
140
|
+
delete: string;
|
|
141
|
+
retry: string;
|
|
142
|
+
newChat: string;
|
|
143
|
+
loadMore: string;
|
|
144
|
+
jumpToLatest: string;
|
|
145
|
+
attach: string;
|
|
146
|
+
voice: string;
|
|
147
|
+
errorGeneric: string;
|
|
148
|
+
cancelledSuffix: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const DEFAULT_LABELS: ChatLabels = {
|
|
152
|
+
send: 'Send',
|
|
153
|
+
cancel: 'Stop',
|
|
154
|
+
copy: 'Copy',
|
|
155
|
+
regenerate: 'Regenerate',
|
|
156
|
+
edit: 'Edit',
|
|
157
|
+
delete: 'Delete',
|
|
158
|
+
retry: 'Retry',
|
|
159
|
+
newChat: 'New chat',
|
|
160
|
+
loadMore: 'Load more',
|
|
161
|
+
jumpToLatest: 'Jump to latest',
|
|
162
|
+
attach: 'Attach files',
|
|
163
|
+
voice: 'Voice input',
|
|
164
|
+
errorGeneric: 'Something went wrong',
|
|
165
|
+
cancelledSuffix: '[cancelled]',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ---- Transport ------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export interface CreateSessionOptions {
|
|
171
|
+
metadata?: Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SessionInfo {
|
|
175
|
+
sessionId: string;
|
|
176
|
+
/** If the server resumed an existing session, it may return prior messages. */
|
|
177
|
+
messages?: ChatMessage[];
|
|
178
|
+
hasMore?: boolean;
|
|
179
|
+
cursor?: string | null;
|
|
180
|
+
resumed?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface HistoryPage {
|
|
184
|
+
messages: ChatMessage[];
|
|
185
|
+
hasMore: boolean;
|
|
186
|
+
nextCursor: string | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface StreamOptions {
|
|
190
|
+
signal: AbortSignal;
|
|
191
|
+
attachments?: ChatAttachment[];
|
|
192
|
+
metadata?: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface SendOptions {
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
attachments?: ChatAttachment[];
|
|
198
|
+
metadata?: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type ChatStreamEvent =
|
|
202
|
+
| { type: 'message_start'; messageId: string; sessionId: string }
|
|
203
|
+
| { type: 'chunk'; delta: string }
|
|
204
|
+
| { type: 'tool_activity'; tool: string; status: string }
|
|
205
|
+
| {
|
|
206
|
+
type: 'tool_call_start';
|
|
207
|
+
toolId: string;
|
|
208
|
+
name: string;
|
|
209
|
+
input: unknown;
|
|
210
|
+
sourceHostname?: string;
|
|
211
|
+
}
|
|
212
|
+
| { type: 'tool_call_delta'; toolId: string; delta: string }
|
|
213
|
+
| {
|
|
214
|
+
type: 'tool_call_end';
|
|
215
|
+
toolId: string;
|
|
216
|
+
output: unknown;
|
|
217
|
+
status: 'success' | 'error';
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
type: 'message_end';
|
|
221
|
+
tokensIn?: number;
|
|
222
|
+
tokensOut?: number;
|
|
223
|
+
sources?: ChatSource[];
|
|
224
|
+
}
|
|
225
|
+
| { type: 'error'; code: string; message: string };
|
|
226
|
+
|
|
227
|
+
export interface ChatTransport {
|
|
228
|
+
createSession(opts?: CreateSessionOptions): Promise<SessionInfo>;
|
|
229
|
+
loadHistory(sessionId: string, cursor?: string | null, limit?: number): Promise<HistoryPage>;
|
|
230
|
+
stream(
|
|
231
|
+
sessionId: string,
|
|
232
|
+
content: string,
|
|
233
|
+
options: StreamOptions,
|
|
234
|
+
): AsyncGenerator<ChatStreamEvent, void, void>;
|
|
235
|
+
send(sessionId: string, content: string, options?: SendOptions): Promise<ChatMessage>;
|
|
236
|
+
closeSession(sessionId: string): Promise<void>;
|
|
237
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChatAttachment, ChatMessage } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Walk the conversation and collect image attachments in chronological order. */
|
|
4
|
+
export function collectImageAttachments(messages: ChatMessage[]): ChatAttachment[] {
|
|
5
|
+
const out: ChatAttachment[] = [];
|
|
6
|
+
for (const m of messages) {
|
|
7
|
+
if (!m.attachments) continue;
|
|
8
|
+
for (const a of m.attachments) {
|
|
9
|
+
if (a.type === 'image') out.push(a);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
@@ -121,6 +121,37 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
|
|
|
121
121
|
range: SliderWidget, // alias
|
|
122
122
|
}), []);
|
|
123
123
|
|
|
124
|
+
// Validate uiSchema widget references and strip unknown ones so RJSF
|
|
125
|
+
// doesn't crash with "No widget 'xxx' for type 'yyy'".
|
|
126
|
+
const safeUiSchema = useMemo(() => {
|
|
127
|
+
if (!uiSchema) return uiSchema;
|
|
128
|
+
const known = new Set(Object.keys(widgets));
|
|
129
|
+
let dirty = false;
|
|
130
|
+
|
|
131
|
+
function walk(node: unknown): unknown {
|
|
132
|
+
if (!node || typeof node !== 'object') return node;
|
|
133
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
134
|
+
const out: Record<string, unknown> = {};
|
|
135
|
+
for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
|
|
136
|
+
if (k === 'ui:widget' && typeof v === 'string' && !known.has(v)) {
|
|
137
|
+
dirty = true;
|
|
138
|
+
if (isDev) {
|
|
139
|
+
consola.error(
|
|
140
|
+
`[JsonSchemaForm] Unknown widget "${v}" — falling back to default. ` +
|
|
141
|
+
`Available: ${[...known].filter((w) => w[0]?.toLowerCase() === w[0]).join(', ')}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
continue; // drop unknown widget so RJSF picks the default
|
|
145
|
+
}
|
|
146
|
+
out[k] = walk(v);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cleaned = walk(uiSchema) as UiSchema | undefined;
|
|
152
|
+
return dirty ? cleaned : uiSchema;
|
|
153
|
+
}, [uiSchema, widgets]);
|
|
154
|
+
|
|
124
155
|
// Memoize templates to prevent recreation on every render
|
|
125
156
|
const templates = useMemo(() => ({
|
|
126
157
|
FieldTemplate,
|
|
@@ -181,7 +212,7 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
|
|
|
181
212
|
<div className={className} data-jsonform-density={density}>
|
|
182
213
|
<Form
|
|
183
214
|
schema={validatedSchema}
|
|
184
|
-
uiSchema={
|
|
215
|
+
uiSchema={safeUiSchema as UiSchema | undefined}
|
|
185
216
|
formData={normalizedFormData}
|
|
186
217
|
validator={validator}
|
|
187
218
|
widgets={widgets}
|