@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Sent Events parser as an AsyncGenerator.
|
|
3
|
+
*
|
|
4
|
+
* Yields parsed events from a `Response` body. Handles the split-read case
|
|
5
|
+
* where `event:` and `data:` arrive in separate TCP packets. Skips malformed
|
|
6
|
+
* JSON gracefully. Honors AbortSignal (caller passes one to fetch).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ChatStreamEvent } from '../../types';
|
|
10
|
+
import { LIMITS } from '../../config';
|
|
11
|
+
|
|
12
|
+
interface RawEvent {
|
|
13
|
+
event?: string;
|
|
14
|
+
data?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ParseSSEOptions {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Map a raw SSE event to a ChatStreamEvent. Default: parse `data` as JSON
|
|
20
|
+
* and assume the JSON shape already matches `ChatStreamEvent`. */
|
|
21
|
+
map?: (raw: RawEvent) => ChatStreamEvent | null;
|
|
22
|
+
idleTimeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_MAP = (raw: RawEvent): ChatStreamEvent | null => {
|
|
26
|
+
if (!raw.data) return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw.data) as ChatStreamEvent;
|
|
29
|
+
if (raw.event && !('type' in parsed)) {
|
|
30
|
+
return { ...(parsed as object), type: raw.event } as ChatStreamEvent;
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function* parseSSE(
|
|
39
|
+
response: Response,
|
|
40
|
+
options: ParseSSEOptions = {},
|
|
41
|
+
): AsyncGenerator<ChatStreamEvent, void, void> {
|
|
42
|
+
if (!response.body) {
|
|
43
|
+
throw new Error('SSE response has no body');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const map = options.map ?? DEFAULT_MAP;
|
|
47
|
+
const idleMs = options.idleTimeoutMs ?? LIMITS.sseIdleMs;
|
|
48
|
+
const reader = response.body.getReader();
|
|
49
|
+
const decoder = new TextDecoder();
|
|
50
|
+
let buffer = '';
|
|
51
|
+
let lastChunkAt = Date.now();
|
|
52
|
+
|
|
53
|
+
const idleCheck = () => {
|
|
54
|
+
if (Date.now() - lastChunkAt > idleMs) {
|
|
55
|
+
throw new Error(`SSE idle timeout (${idleMs}ms)`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
while (true) {
|
|
61
|
+
if (options.signal?.aborted) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { value, done } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
|
|
67
|
+
lastChunkAt = Date.now();
|
|
68
|
+
buffer += decoder.decode(value, { stream: true });
|
|
69
|
+
|
|
70
|
+
// Split on blank line which delimits SSE events.
|
|
71
|
+
let separator = buffer.indexOf('\n\n');
|
|
72
|
+
while (separator !== -1) {
|
|
73
|
+
const rawBlock = buffer.slice(0, separator);
|
|
74
|
+
buffer = buffer.slice(separator + 2);
|
|
75
|
+
|
|
76
|
+
const raw = parseEventBlock(rawBlock);
|
|
77
|
+
const evt = map(raw);
|
|
78
|
+
if (evt) yield evt;
|
|
79
|
+
|
|
80
|
+
separator = buffer.indexOf('\n\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
idleCheck();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Flush any trailing partial event.
|
|
87
|
+
if (buffer.trim()) {
|
|
88
|
+
const raw = parseEventBlock(buffer);
|
|
89
|
+
const evt = map(raw);
|
|
90
|
+
if (evt) yield evt;
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
try {
|
|
94
|
+
reader.releaseLock();
|
|
95
|
+
} catch {
|
|
96
|
+
/* ignore */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseEventBlock(block: string): RawEvent {
|
|
102
|
+
const out: RawEvent = {};
|
|
103
|
+
const lines = block.split(/\r?\n/);
|
|
104
|
+
const dataLines: string[] = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (!line || line.startsWith(':')) continue;
|
|
107
|
+
const colon = line.indexOf(':');
|
|
108
|
+
if (colon === -1) continue;
|
|
109
|
+
const field = line.slice(0, colon).trim();
|
|
110
|
+
const value = line.slice(colon + 1).replace(/^ /, '');
|
|
111
|
+
if (field === 'event') out.event = value;
|
|
112
|
+
else if (field === 'data') dataLines.push(value);
|
|
113
|
+
}
|
|
114
|
+
if (dataLines.length) out.data = dataLines.join('\n');
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport surface re-export. Lives in core so transport implementations
|
|
3
|
+
* never need to reach into the public types module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
ChatTransport,
|
|
8
|
+
ChatStreamEvent,
|
|
9
|
+
CreateSessionOptions,
|
|
10
|
+
SessionInfo,
|
|
11
|
+
HistoryPage,
|
|
12
|
+
StreamOptions,
|
|
13
|
+
SendOptions,
|
|
14
|
+
} from '../../types';
|
|
15
|
+
|
|
16
|
+
export class TransportError extends Error {
|
|
17
|
+
code: string;
|
|
18
|
+
|
|
19
|
+
constructor(message: string, code = 'transport_error') {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'TransportError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { useChat, type UseChatConfig, type UseChatReturn } from './useChat';
|
|
4
|
+
export {
|
|
5
|
+
useChatComposer,
|
|
6
|
+
type UseChatComposerOptions,
|
|
7
|
+
type UseChatComposerReturn,
|
|
8
|
+
} from './useChatComposer';
|
|
9
|
+
export {
|
|
10
|
+
useChatScroll,
|
|
11
|
+
type UseChatScrollOptions,
|
|
12
|
+
type UseChatScrollReturn,
|
|
13
|
+
} from './useChatScroll';
|
|
14
|
+
export { useChatHistory, type UseChatHistoryOptions } from './useChatHistory';
|
|
15
|
+
export {
|
|
16
|
+
useChatLayout,
|
|
17
|
+
type UseChatLayoutConfig,
|
|
18
|
+
type UseChatLayoutReturn,
|
|
19
|
+
} from './useChatLayout';
|
|
20
|
+
export { useChatAudio } from './useChatAudio';
|
|
21
|
+
export {
|
|
22
|
+
useChatLightbox,
|
|
23
|
+
type UseChatLightboxReturn,
|
|
24
|
+
type ChatLightboxState,
|
|
25
|
+
type ChatLightboxScope,
|
|
26
|
+
} from './useChatLightbox';
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ChatAttachment,
|
|
7
|
+
ChatMessage,
|
|
8
|
+
ChatPersona,
|
|
9
|
+
ChatStreamEvent,
|
|
10
|
+
ChatTransport,
|
|
11
|
+
ChatToolCall,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import { LIMITS } from '../config';
|
|
14
|
+
import {
|
|
15
|
+
type ChatState,
|
|
16
|
+
initialState,
|
|
17
|
+
reducer,
|
|
18
|
+
type ChatAction,
|
|
19
|
+
} from '../core/reducer';
|
|
20
|
+
import { createId } from '../core/ids';
|
|
21
|
+
import { createTokenBuffer } from '../core/markdown';
|
|
22
|
+
|
|
23
|
+
export interface UseChatConfig {
|
|
24
|
+
transport: ChatTransport;
|
|
25
|
+
initialSessionId?: string;
|
|
26
|
+
autoCreateSession?: boolean;
|
|
27
|
+
streaming?: boolean;
|
|
28
|
+
pageSize?: number;
|
|
29
|
+
onError?: (err: Error) => void;
|
|
30
|
+
/** Fires once an assistant message finishes streaming (or buffered send returns). */
|
|
31
|
+
onMessageEnd?: (msg: ChatMessage) => void;
|
|
32
|
+
/** Fires after a user message is added to the state (right before streaming starts). */
|
|
33
|
+
onMessageSent?: (msg: ChatMessage) => void;
|
|
34
|
+
/** Fires when the assistant placeholder is created (first byte / pre-stream). */
|
|
35
|
+
onStreamStart?: (assistantMessageId: string) => void;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
/** Stamped on outgoing user messages as `message.sender`. */
|
|
38
|
+
userPersona?: ChatPersona;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UseChatReturn extends ChatState {
|
|
42
|
+
sendMessage: (content: string, attachments?: ChatAttachment[]) => Promise<void>;
|
|
43
|
+
cancelStream: () => void;
|
|
44
|
+
regenerate: (messageId?: string) => Promise<void>;
|
|
45
|
+
editMessage: (id: string, content: string) => Promise<void>;
|
|
46
|
+
deleteMessage: (id: string) => void;
|
|
47
|
+
clearMessages: () => void;
|
|
48
|
+
loadMore: () => Promise<void>;
|
|
49
|
+
newSession: () => Promise<void>;
|
|
50
|
+
lastError: Error | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useChat(config: UseChatConfig): UseChatReturn {
|
|
54
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
55
|
+
const stateRef = useRef(state);
|
|
56
|
+
stateRef.current = state;
|
|
57
|
+
|
|
58
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
59
|
+
const lastErrorRef = useRef<Error | null>(null);
|
|
60
|
+
const initRef = useRef(false);
|
|
61
|
+
const streamingMsgIdRef = useRef<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const { transport, autoCreateSession = true, streaming = true, pageSize = LIMITS.pageSize } =
|
|
64
|
+
config;
|
|
65
|
+
|
|
66
|
+
// Initial session bootstrap.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (initRef.current) return;
|
|
69
|
+
initRef.current = true;
|
|
70
|
+
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
const run = async () => {
|
|
73
|
+
try {
|
|
74
|
+
if (config.initialSessionId) {
|
|
75
|
+
dispatch({
|
|
76
|
+
type: 'SESSION_SET',
|
|
77
|
+
sessionId: config.initialSessionId,
|
|
78
|
+
});
|
|
79
|
+
dispatch({ type: 'HISTORY_LOAD_START' });
|
|
80
|
+
const page = await transport.loadHistory(config.initialSessionId, null, pageSize);
|
|
81
|
+
if (cancelled) return;
|
|
82
|
+
dispatch({
|
|
83
|
+
type: 'HISTORY_LOAD_DONE',
|
|
84
|
+
messages: page.messages,
|
|
85
|
+
hasMore: page.hasMore,
|
|
86
|
+
cursor: page.nextCursor,
|
|
87
|
+
});
|
|
88
|
+
} else if (autoCreateSession) {
|
|
89
|
+
const info = await transport.createSession({ metadata: config.metadata });
|
|
90
|
+
if (cancelled) return;
|
|
91
|
+
dispatch({
|
|
92
|
+
type: 'SESSION_SET',
|
|
93
|
+
sessionId: info.sessionId,
|
|
94
|
+
messages: info.messages ?? [],
|
|
95
|
+
hasMore: info.hasMore ?? false,
|
|
96
|
+
cursor: info.cursor ?? null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
101
|
+
lastErrorRef.current = e;
|
|
102
|
+
dispatch({ type: 'ERROR_SET', error: e.message });
|
|
103
|
+
config.onError?.(e);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
void run();
|
|
107
|
+
return () => {
|
|
108
|
+
cancelled = true;
|
|
109
|
+
};
|
|
110
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const consumeStream = useCallback(
|
|
114
|
+
async (
|
|
115
|
+
sessionId: string,
|
|
116
|
+
content: string,
|
|
117
|
+
attachments?: ChatAttachment[],
|
|
118
|
+
): Promise<void> => {
|
|
119
|
+
const ctrl = new AbortController();
|
|
120
|
+
abortRef.current = ctrl;
|
|
121
|
+
const assistantId = createId('a');
|
|
122
|
+
streamingMsgIdRef.current = assistantId;
|
|
123
|
+
|
|
124
|
+
dispatch({ type: 'STREAM_START', id: assistantId });
|
|
125
|
+
config.onStreamStart?.(assistantId);
|
|
126
|
+
|
|
127
|
+
const tokenBuffer = createTokenBuffer((delta) =>
|
|
128
|
+
dispatch({ type: 'STREAM_CHUNK', delta }),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
let serverMessageId: string | null = null;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const iterator = transport.stream(sessionId, content, {
|
|
135
|
+
signal: ctrl.signal,
|
|
136
|
+
attachments,
|
|
137
|
+
metadata: config.metadata,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
for await (const ev of iterator) {
|
|
141
|
+
if (ctrl.signal.aborted) break;
|
|
142
|
+
handleEvent(ev);
|
|
143
|
+
}
|
|
144
|
+
tokenBuffer.flush();
|
|
145
|
+
|
|
146
|
+
// If transport never emitted message_end, finalize manually.
|
|
147
|
+
if (stateRef.current.isStreaming) {
|
|
148
|
+
dispatch({ type: 'STREAM_DONE', id: assistantId });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const finalMsg = stateRef.current.messages.find((m) => m.id === assistantId);
|
|
152
|
+
if (finalMsg) config.onMessageEnd?.(finalMsg);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
tokenBuffer.close();
|
|
155
|
+
if (ctrl.signal.aborted) {
|
|
156
|
+
const partial =
|
|
157
|
+
stateRef.current.messages.find((m) => m.id === assistantId)?.content ?? '';
|
|
158
|
+
dispatch({ type: 'STREAM_CANCELLED', id: assistantId, partialText: partial });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
lastErrorRef.current = e;
|
|
163
|
+
dispatch({ type: 'STREAM_ERROR', id: assistantId, message: e.message });
|
|
164
|
+
config.onError?.(e);
|
|
165
|
+
} finally {
|
|
166
|
+
tokenBuffer.close();
|
|
167
|
+
if (abortRef.current === ctrl) abortRef.current = null;
|
|
168
|
+
streamingMsgIdRef.current = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleEvent(ev: ChatStreamEvent) {
|
|
172
|
+
switch (ev.type) {
|
|
173
|
+
case 'message_start':
|
|
174
|
+
serverMessageId = ev.messageId;
|
|
175
|
+
return;
|
|
176
|
+
case 'chunk':
|
|
177
|
+
tokenBuffer.push(ev.delta);
|
|
178
|
+
return;
|
|
179
|
+
case 'tool_activity':
|
|
180
|
+
tokenBuffer.flush();
|
|
181
|
+
dispatch({ type: 'STREAM_TOOL_ACTIVITY', tool: ev.tool });
|
|
182
|
+
return;
|
|
183
|
+
case 'tool_call_start': {
|
|
184
|
+
tokenBuffer.flush();
|
|
185
|
+
const toolCall: ChatToolCall = {
|
|
186
|
+
id: ev.toolId,
|
|
187
|
+
name: ev.name,
|
|
188
|
+
input: ev.input,
|
|
189
|
+
status: 'running',
|
|
190
|
+
startedAt: Date.now(),
|
|
191
|
+
sourceHostname: ev.sourceHostname,
|
|
192
|
+
};
|
|
193
|
+
dispatch({
|
|
194
|
+
type: 'TOOL_CALL_START',
|
|
195
|
+
messageId: assistantId,
|
|
196
|
+
toolCall,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
case 'tool_call_delta':
|
|
201
|
+
dispatch({
|
|
202
|
+
type: 'TOOL_CALL_DELTA',
|
|
203
|
+
messageId: assistantId,
|
|
204
|
+
toolId: ev.toolId,
|
|
205
|
+
delta: ev.delta,
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
case 'tool_call_end':
|
|
209
|
+
dispatch({
|
|
210
|
+
type: 'TOOL_CALL_END',
|
|
211
|
+
messageId: assistantId,
|
|
212
|
+
toolId: ev.toolId,
|
|
213
|
+
output: ev.output,
|
|
214
|
+
status: ev.status,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
case 'message_end':
|
|
218
|
+
tokenBuffer.flush();
|
|
219
|
+
dispatch({
|
|
220
|
+
type: 'STREAM_DONE',
|
|
221
|
+
id: assistantId,
|
|
222
|
+
tokensIn: ev.tokensIn,
|
|
223
|
+
tokensOut: ev.tokensOut,
|
|
224
|
+
sources: ev.sources,
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
case 'error':
|
|
228
|
+
tokenBuffer.flush();
|
|
229
|
+
dispatch({
|
|
230
|
+
type: 'STREAM_ERROR',
|
|
231
|
+
id: assistantId,
|
|
232
|
+
message: ev.message,
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// unreachable; prevents unused-var on serverMessageId
|
|
237
|
+
void serverMessageId;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[transport, config],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const consumeBuffered = useCallback(
|
|
244
|
+
async (sessionId: string, content: string, attachments?: ChatAttachment[]): Promise<void> => {
|
|
245
|
+
const ctrl = new AbortController();
|
|
246
|
+
abortRef.current = ctrl;
|
|
247
|
+
try {
|
|
248
|
+
const reply = await transport.send(sessionId, content, {
|
|
249
|
+
signal: ctrl.signal,
|
|
250
|
+
attachments,
|
|
251
|
+
metadata: config.metadata,
|
|
252
|
+
});
|
|
253
|
+
const placeholderId = createId('a');
|
|
254
|
+
dispatch({ type: 'STREAM_START', id: placeholderId });
|
|
255
|
+
config.onStreamStart?.(placeholderId);
|
|
256
|
+
dispatch({ type: 'STREAM_CHUNK', delta: reply.content });
|
|
257
|
+
dispatch({ type: 'STREAM_DONE', id: placeholderId });
|
|
258
|
+
config.onMessageEnd?.(reply);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
261
|
+
lastErrorRef.current = e;
|
|
262
|
+
dispatch({ type: 'STREAM_ERROR', message: e.message });
|
|
263
|
+
config.onError?.(e);
|
|
264
|
+
} finally {
|
|
265
|
+
if (abortRef.current === ctrl) abortRef.current = null;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[transport, config],
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const sendMessage = useCallback(
|
|
272
|
+
async (content: string, attachments?: ChatAttachment[]) => {
|
|
273
|
+
const sessionId = stateRef.current.sessionId;
|
|
274
|
+
if (!sessionId) {
|
|
275
|
+
const e = new Error('No active session');
|
|
276
|
+
lastErrorRef.current = e;
|
|
277
|
+
dispatch({ type: 'ERROR_SET', error: e.message });
|
|
278
|
+
config.onError?.(e);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!content.trim() && !(attachments && attachments.length > 0)) return;
|
|
282
|
+
if (stateRef.current.isStreaming) return;
|
|
283
|
+
|
|
284
|
+
const userMsg: ChatMessage = {
|
|
285
|
+
id: createId('u'),
|
|
286
|
+
role: 'user',
|
|
287
|
+
content,
|
|
288
|
+
createdAt: Date.now(),
|
|
289
|
+
attachments,
|
|
290
|
+
sender: config.userPersona,
|
|
291
|
+
};
|
|
292
|
+
dispatch({ type: 'MESSAGE_USER_ADD', message: userMsg });
|
|
293
|
+
config.onMessageSent?.(userMsg);
|
|
294
|
+
|
|
295
|
+
if (streaming) {
|
|
296
|
+
await consumeStream(sessionId, content, attachments);
|
|
297
|
+
} else {
|
|
298
|
+
await consumeBuffered(sessionId, content, attachments);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
[streaming, consumeStream, consumeBuffered, config],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const cancelStream = useCallback(() => {
|
|
305
|
+
abortRef.current?.abort();
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const regenerate = useCallback(
|
|
309
|
+
async (messageId?: string) => {
|
|
310
|
+
const messages = stateRef.current.messages;
|
|
311
|
+
let targetUserIdx = -1;
|
|
312
|
+
if (messageId) {
|
|
313
|
+
const idx = messages.findIndex((m) => m.id === messageId);
|
|
314
|
+
if (idx !== -1) {
|
|
315
|
+
targetUserIdx =
|
|
316
|
+
messages[idx].role === 'user' ? idx : findPreviousUserIndex(messages, idx);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
targetUserIdx = findLastUserIndex(messages);
|
|
320
|
+
}
|
|
321
|
+
if (targetUserIdx === -1) return;
|
|
322
|
+
const userMsg = messages[targetUserIdx];
|
|
323
|
+
// Drop everything after this user message.
|
|
324
|
+
for (let i = messages.length - 1; i > targetUserIdx; i -= 1) {
|
|
325
|
+
dispatch({ type: 'MESSAGE_DELETE', id: messages[i].id });
|
|
326
|
+
}
|
|
327
|
+
const sessionId = stateRef.current.sessionId;
|
|
328
|
+
if (!sessionId) return;
|
|
329
|
+
if (streaming) {
|
|
330
|
+
await consumeStream(sessionId, userMsg.content, userMsg.attachments);
|
|
331
|
+
} else {
|
|
332
|
+
await consumeBuffered(sessionId, userMsg.content, userMsg.attachments);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
[streaming, consumeStream, consumeBuffered],
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const editMessage = useCallback(
|
|
339
|
+
async (id: string, content: string) => {
|
|
340
|
+
dispatch({ type: 'MESSAGE_EDIT', id, content });
|
|
341
|
+
const msg = stateRef.current.messages.find((m) => m.id === id);
|
|
342
|
+
if (msg?.role === 'user') {
|
|
343
|
+
await regenerate(id);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
[regenerate],
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const deleteMessage = useCallback((id: string) => {
|
|
350
|
+
dispatch({ type: 'MESSAGE_DELETE', id });
|
|
351
|
+
}, []);
|
|
352
|
+
|
|
353
|
+
const clearMessages = useCallback(() => {
|
|
354
|
+
abortRef.current?.abort();
|
|
355
|
+
dispatch({ type: 'MESSAGES_CLEAR' });
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
const loadMore = useCallback(async () => {
|
|
359
|
+
const sessionId = stateRef.current.sessionId;
|
|
360
|
+
if (!sessionId) return;
|
|
361
|
+
if (stateRef.current.isLoadingMore || !stateRef.current.hasMore) return;
|
|
362
|
+
dispatch({ type: 'HISTORY_MORE_START' });
|
|
363
|
+
try {
|
|
364
|
+
const page = await transport.loadHistory(
|
|
365
|
+
sessionId,
|
|
366
|
+
stateRef.current.oldestCursor,
|
|
367
|
+
pageSize,
|
|
368
|
+
);
|
|
369
|
+
dispatch({
|
|
370
|
+
type: 'HISTORY_MORE_DONE',
|
|
371
|
+
messages: page.messages,
|
|
372
|
+
hasMore: page.hasMore,
|
|
373
|
+
cursor: page.nextCursor,
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
377
|
+
lastErrorRef.current = e;
|
|
378
|
+
dispatch({ type: 'ERROR_SET', error: e.message });
|
|
379
|
+
config.onError?.(e);
|
|
380
|
+
}
|
|
381
|
+
}, [transport, pageSize, config]);
|
|
382
|
+
|
|
383
|
+
const newSession = useCallback(async () => {
|
|
384
|
+
abortRef.current?.abort();
|
|
385
|
+
const previous = stateRef.current.sessionId;
|
|
386
|
+
if (previous) {
|
|
387
|
+
try {
|
|
388
|
+
await transport.closeSession(previous);
|
|
389
|
+
} catch {
|
|
390
|
+
/* ignore */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
dispatch({ type: 'MESSAGES_CLEAR' });
|
|
394
|
+
try {
|
|
395
|
+
const info = await transport.createSession({ metadata: config.metadata });
|
|
396
|
+
dispatch({
|
|
397
|
+
type: 'SESSION_SET',
|
|
398
|
+
sessionId: info.sessionId,
|
|
399
|
+
messages: info.messages ?? [],
|
|
400
|
+
hasMore: info.hasMore ?? false,
|
|
401
|
+
cursor: info.cursor ?? null,
|
|
402
|
+
});
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
405
|
+
lastErrorRef.current = e;
|
|
406
|
+
dispatch({ type: 'ERROR_SET', error: e.message });
|
|
407
|
+
config.onError?.(e);
|
|
408
|
+
}
|
|
409
|
+
}, [transport, config]);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
...state,
|
|
413
|
+
sendMessage,
|
|
414
|
+
cancelStream,
|
|
415
|
+
regenerate,
|
|
416
|
+
editMessage,
|
|
417
|
+
deleteMessage,
|
|
418
|
+
clearMessages,
|
|
419
|
+
loadMore,
|
|
420
|
+
newSession,
|
|
421
|
+
lastError: lastErrorRef.current,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function findLastUserIndex(messages: ChatMessage[]): number {
|
|
426
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
427
|
+
if (messages[i].role === 'user') return i;
|
|
428
|
+
}
|
|
429
|
+
return -1;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function findPreviousUserIndex(messages: ChatMessage[], from: number): number {
|
|
433
|
+
for (let i = from - 1; i >= 0; i -= 1) {
|
|
434
|
+
if (messages[i].role === 'user') return i;
|
|
435
|
+
}
|
|
436
|
+
return -1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Suppress unused-action warnings if the action union grows.
|
|
440
|
+
type _Used = ChatAction;
|