@wix/legends-platform-sdk 1.7.0 → 1.8.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.
@@ -7,4 +7,10 @@ Object.keys(_useTextChat).forEach(function (key) {
7
7
  if (key in exports && exports[key] === _useTextChat[key]) return;
8
8
  exports[key] = _useTextChat[key];
9
9
  });
10
+ var _useTextChatStream = require("./useTextChatStream");
11
+ Object.keys(_useTextChatStream).forEach(function (key) {
12
+ if (key === "default" || key === "__esModule") return;
13
+ if (key in exports && exports[key] === _useTextChatStream[key]) return;
14
+ exports[key] = _useTextChatStream[key];
15
+ });
10
16
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_useTextChat","require","Object","keys","forEach","key","exports"],"sources":["../../../../src/hooks/useTextChat/index.ts"],"sourcesContent":["export * from './useTextChat';\n"],"mappings":";;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,YAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,YAAA,CAAAK,GAAA;EAAAC,OAAA,CAAAD,GAAA,IAAAL,YAAA,CAAAK,GAAA;AAAA","ignoreList":[]}
1
+ {"version":3,"names":["_useTextChat","require","Object","keys","forEach","key","exports","_useTextChatStream"],"sources":["../../../../src/hooks/useTextChat/index.ts"],"sourcesContent":["export * from './useTextChat';\nexport * from './useTextChatStream';\n"],"mappings":";;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,YAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,YAAA,CAAAK,GAAA;EAAAC,OAAA,CAAAD,GAAA,IAAAL,YAAA,CAAAK,GAAA;AAAA;AACA,IAAAE,kBAAA,GAAAN,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAI,kBAAA,EAAAH,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAE,kBAAA,CAAAF,GAAA;EAAAC,OAAA,CAAAD,GAAA,IAAAE,kBAAA,CAAAF,GAAA;AAAA","ignoreList":[]}
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.useTextChatStream = useTextChatStream;
5
+ var _react = require("react");
6
+ var _reactQuery = require("@tanstack/react-query");
7
+ var _sdkConsumer = require("../../sdk-consumer");
8
+ var _streamChatMessage = require("../../services/streamChatMessage");
9
+ exports.streamChatMessage = _streamChatMessage.streamChatMessage;
10
+ // Re-exported here so consumers can reach the framework-agnostic transport
11
+ // alongside the hook.
12
+
13
+ // Progressive-reveal drip. We uncover the buffered reply a few chars at a time
14
+ // on a timer rather than dumping each network chunk in. The effective rate
15
+ // (~100 chars/s) is kept just under a typical chat-UI typewriter's own rate so
16
+ // the typer always catches up between drips, yielding a smooth type-out whether
17
+ // tokens trickle in or arrive in one burst.
18
+ const REVEAL_INTERVAL_MS = 30;
19
+ const REVEAL_CHARS_PER_TICK = 3;
20
+
21
+ /**
22
+ * A `ChatMessage` augmented with streaming metadata. The host renderer uses
23
+ * `streamStatus`/`streamingText` to drive its typewriter; confirmed
24
+ * (non-streamed) messages carry neither.
25
+ */
26
+
27
+ /**
28
+ * Identity for deduping optimistic streamed messages against confirmed ones.
29
+ *
30
+ * This is a *content* join: the streamed text must equal the server-persisted
31
+ * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-
32
+ * whitespace differences, but heavier server-side post-processing (markdown /
33
+ * entity normalization, an appended citation) would make the keys diverge and
34
+ * briefly surface both copies. A shared message id would be more robust, but
35
+ * the streamed frames carry only token deltas — no id — so content equality is
36
+ * the best join available without backend support.
37
+ */
38
+ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
39
+
40
+ /**
41
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
42
+ * and progressively reveals the reply, merging the in-flight and completed
43
+ * streamed exchanges with the confirmed history (oldest-first).
44
+ *
45
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
46
+ * loop — parity with the current unary behavior.
47
+ */
48
+ function useTextChatStream(options) {
49
+ const {
50
+ streamUrl,
51
+ limit = 50,
52
+ revealIntervalMs = REVEAL_INTERVAL_MS,
53
+ revealCharsPerTick = REVEAL_CHARS_PER_TICK,
54
+ headers,
55
+ onError
56
+ } = options;
57
+ const {
58
+ chatId,
59
+ namespace,
60
+ personaChatService
61
+ } = (0, _sdkConsumer.useSdkConsumer)();
62
+ const {
63
+ data: sdkMessages = [],
64
+ status,
65
+ fetchNextPage,
66
+ refetch
67
+ } = (0, _reactQuery.useInfiniteQuery)({
68
+ queryKey: ['chat-messages', chatId, namespace],
69
+ queryFn: async ({
70
+ pageParam
71
+ }) => {
72
+ if (!personaChatService) {
73
+ return {
74
+ messages: [],
75
+ nextCursor: undefined
76
+ };
77
+ }
78
+ return personaChatService.queryChatMessages({
79
+ chatId,
80
+ namespace,
81
+ limit,
82
+ cursor: pageParam
83
+ });
84
+ },
85
+ getNextPageParam: lastPage => lastPage.nextCursor,
86
+ initialPageParam: undefined,
87
+ select: data => data.pages.flatMap(page => page.messages)
88
+ });
89
+
90
+ // Local state for the in-progress streaming exchange (user + assistant messages).
91
+ const [streamingMessages, setStreamingMessages] = (0, _react.useState)([]);
92
+ // Exchanges that finished streaming, kept for the lifetime of this chat. They
93
+ // keep their stable optimistic ids and stand in for the SDK's confirmed copies
94
+ // (which are hidden as duplicates) — this both bridges the refetch lag and
95
+ // avoids remounting the message (and restarting its typewriter) when the
96
+ // confirmed copy, with a different id, arrives.
97
+ const [completedMessages, setCompletedMessages] = (0, _react.useState)([]);
98
+ const abortRef = (0, _react.useRef)(null);
99
+ // Interval that drips the buffered reply into the streaming message.
100
+ const revealTimerRef = (0, _react.useRef)(null);
101
+ // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
102
+ // falling back to this keeps the history from disappearing and reappearing.
103
+ const lastConfirmedRef = (0, _react.useRef)([]);
104
+ // Monotonic per-hook counter so two sends within the same millisecond still
105
+ // produce distinct message ids (used as React keys downstream).
106
+ const sendSeqRef = (0, _react.useRef)(0);
107
+ const stopRevealTimer = (0, _react.useCallback)(() => {
108
+ if (revealTimerRef.current) {
109
+ clearInterval(revealTimerRef.current);
110
+ revealTimerRef.current = null;
111
+ }
112
+ }, []);
113
+
114
+ // Stop any in-flight stream + reveal on unmount.
115
+ (0, _react.useEffect)(() => {
116
+ return () => {
117
+ var _abortRef$current;
118
+ (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
119
+ stopRevealTimer();
120
+ };
121
+ }, [stopRevealTimer]);
122
+ const messages = (0, _react.useMemo)(() => {
123
+ const converted = [...sdkMessages].reverse();
124
+ const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
125
+
126
+ // Show our locally-tracked streamed exchanges (stable ids) and hide the
127
+ // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the
128
+ // message (different id) and restart its typewriter — that's the flicker.
129
+ // The in-progress streaming messages are always shown as-is.
130
+ const completedKeys = new Set(completedMessages.map(messageKey));
131
+ const confirmedShown = confirmedMessages.filter(m => !completedKeys.has(messageKey(m)));
132
+ return [...confirmedShown, ...completedMessages, ...streamingMessages];
133
+ }, [sdkMessages, streamingMessages, completedMessages]);
134
+
135
+ // Remember the latest non-empty confirmed list so a transient empty during a
136
+ // refetch doesn't blank the history.
137
+ (0, _react.useEffect)(() => {
138
+ const converted = [...sdkMessages].reverse();
139
+ if (converted.length > 0) {
140
+ lastConfirmedRef.current = converted;
141
+ }
142
+ }, [sdkMessages]);
143
+ const send = (0, _react.useCallback)(async ({
144
+ message,
145
+ chatConfig,
146
+ attachments
147
+ }) => {
148
+ var _abortRef$current2;
149
+ // Abort any in-flight stream + reveal before starting a new one.
150
+ (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
151
+ stopRevealTimer();
152
+ const controller = new AbortController();
153
+ abortRef.current = controller;
154
+ const seq = sendSeqRef.current += 1;
155
+ const userMsgId = `stream-user-${Date.now()}-${seq}`;
156
+ const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;
157
+ const optimisticUserMsg = {
158
+ id: userMsgId,
159
+ role: 'USER',
160
+ content: message,
161
+ attachments
162
+ };
163
+ const streamingAssistantMsg = {
164
+ id: assistantMsgId,
165
+ role: 'ASSISTANT',
166
+ content: '',
167
+ streamStatus: 'streaming',
168
+ streamingText: ''
169
+ };
170
+ setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
171
+
172
+ // Tokens are buffered into `accumulated` as they arrive; `revealedLen`
173
+ // tracks how much the drip timer has uncovered so far.
174
+ let accumulated = '';
175
+ let revealedLen = 0;
176
+ let streamDone = false;
177
+ const settle = () => {
178
+ stopRevealTimer();
179
+ // Mirror the streamed text into both `content` and `streamingText`. A
180
+ // host renderer can diff the two to place its typer at the end, so the
181
+ // settled message shows in full with no re-type or flicker. Kept in
182
+ // `completedMessages` (stable id) so the confirmed copy from refetch is
183
+ // hidden and never remounts.
184
+ const finalAssistantMsg = {
185
+ ...streamingAssistantMsg,
186
+ content: accumulated,
187
+ streamStatus: 'settled',
188
+ streamingText: accumulated
189
+ };
190
+ setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
191
+ setStreamingMessages([]);
192
+ // Sync the SDK's confirmed list only now — doing it mid-reveal would
193
+ // surface a confirmed duplicate next to the still-revealing copy.
194
+ void refetch();
195
+ };
196
+ const tick = () => {
197
+ if (revealedLen < accumulated.length) {
198
+ revealedLen = Math.min(accumulated.length, revealedLen + revealCharsPerTick);
199
+ const revealedText = accumulated.slice(0, revealedLen);
200
+ setStreamingMessages([optimisticUserMsg, {
201
+ ...streamingAssistantMsg,
202
+ content: '',
203
+ streamStatus: 'streaming',
204
+ streamingText: revealedText
205
+ }]);
206
+ } else if (streamDone) {
207
+ // Network finished and the reveal has caught up — settle.
208
+ settle();
209
+ }
210
+ };
211
+ const startReveal = () => {
212
+ if (!revealTimerRef.current) {
213
+ revealTimerRef.current = setInterval(tick, revealIntervalMs);
214
+ }
215
+ };
216
+
217
+ // Streaming deliberately bypasses the injected `personaChatService` (the
218
+ // DI seam every other hook uses): SSE needs a raw fetch with cookie auth
219
+ // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't
220
+ // model. History reads still go through the service (see the query above).
221
+ await (0, _streamChatMessage.streamChatMessage)({
222
+ url: streamUrl,
223
+ chatId,
224
+ message,
225
+ namespace,
226
+ chatConfig,
227
+ attachments,
228
+ headers
229
+ }, {
230
+ onChunk: token => {
231
+ accumulated += token;
232
+ startReveal();
233
+ },
234
+ onDone: () => {
235
+ // Let the drip finish revealing, then settle (see `tick`).
236
+ streamDone = true;
237
+ startReveal();
238
+ },
239
+ onError: err => {
240
+ stopRevealTimer();
241
+ onError == null || onError(err);
242
+ setStreamingMessages([]);
243
+ }
244
+ }, controller.signal);
245
+ }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
246
+ return {
247
+ status,
248
+ // Present for symmetry with `useTextChat`; the streaming path tracks its own
249
+ // optimistic state, so this is always empty.
250
+ pending: [],
251
+ messages,
252
+ fetchNext,
253
+ refetch,
254
+ send
255
+ };
256
+ function fetchNext() {
257
+ void fetchNextPage();
258
+ }
259
+ }
260
+ //# sourceMappingURL=useTextChatStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactQuery","_sdkConsumer","_streamChatMessage","exports","streamChatMessage","REVEAL_INTERVAL_MS","REVEAL_CHARS_PER_TICK","messageKey","m","role","content","trim","useTextChatStream","options","streamUrl","limit","revealIntervalMs","revealCharsPerTick","headers","onError","chatId","namespace","personaChatService","useSdkConsumer","data","sdkMessages","status","fetchNextPage","refetch","useInfiniteQuery","queryKey","queryFn","pageParam","messages","nextCursor","undefined","queryChatMessages","cursor","getNextPageParam","lastPage","initialPageParam","select","pages","flatMap","page","streamingMessages","setStreamingMessages","useState","completedMessages","setCompletedMessages","abortRef","useRef","revealTimerRef","lastConfirmedRef","sendSeqRef","stopRevealTimer","useCallback","current","clearInterval","useEffect","_abortRef$current","abort","useMemo","converted","reverse","confirmedMessages","length","completedKeys","Set","map","confirmedShown","filter","has","send","message","chatConfig","attachments","_abortRef$current2","controller","AbortController","seq","userMsgId","Date","now","assistantMsgId","optimisticUserMsg","id","streamingAssistantMsg","streamStatus","streamingText","accumulated","revealedLen","streamDone","settle","finalAssistantMsg","prev","tick","Math","min","revealedText","slice","startReveal","setInterval","url","onChunk","token","onDone","err","signal","pending","fetchNext"],"sources":["../../../../src/hooks/useTextChat/useTextChatStream.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport type { ChatMessage, ChatMessageAttachment } from '../../types';\nimport { useSdkConsumer } from '../../sdk-consumer';\nimport { streamChatMessage } from '../../services/streamChatMessage';\n\n// Re-exported here so consumers can reach the framework-agnostic transport\n// alongside the hook.\nexport {\n streamChatMessage,\n type StreamCallbacks,\n type StreamChatMessageParams,\n} from '../../services/streamChatMessage';\n\n// Progressive-reveal drip. We uncover the buffered reply a few chars at a time\n// on a timer rather than dumping each network chunk in. The effective rate\n// (~100 chars/s) is kept just under a typical chat-UI typewriter's own rate so\n// the typer always catches up between drips, yielding a smooth type-out whether\n// tokens trickle in or arrive in one burst.\nconst REVEAL_INTERVAL_MS = 30;\nconst REVEAL_CHARS_PER_TICK = 3;\n\n/**\n * A `ChatMessage` augmented with streaming metadata. The host renderer uses\n * `streamStatus`/`streamingText` to drive its typewriter; confirmed\n * (non-streamed) messages carry neither.\n */\nexport type StreamingChatMessage = ChatMessage & {\n streamingText?: string;\n streamStatus?: 'streaming' | 'settled';\n};\n\nexport interface UseTextChatStreamOptions {\n streamUrl: string;\n limit?: number;\n revealIntervalMs?: number;\n revealCharsPerTick?: number;\n // Extra headers forwarded on every stream request — e.g. an `x-xsrf-token`\n // when `streamUrl` sits behind a CSRF-protected gateway.\n headers?: Record<string, string>;\n // Stream failures surfaced to the host (e.g. to report to Sentry + toast).\n onError?: (err: Error) => void;\n}\n\nexport interface SendStreamMessageParams {\n message: string;\n chatConfig?: object;\n attachments?: ChatMessageAttachment[];\n}\n\n/**\n * Identity for deduping optimistic streamed messages against confirmed ones.\n *\n * This is a *content* join: the streamed text must equal the server-persisted\n * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-\n * whitespace differences, but heavier server-side post-processing (markdown /\n * entity normalization, an appended citation) would make the keys diverge and\n * briefly surface both copies. A shared message id would be more robust, but\n * the streamed frames carry only token deltas — no id — so content equality is\n * the best join available without backend support.\n */\nconst messageKey = (m: StreamingChatMessage) =>\n `${m.role}::${(m.content ?? '').trim()}`;\n\n/**\n * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport\n * and progressively reveals the reply, merging the in-flight and completed\n * streamed exchanges with the confirmed history (oldest-first).\n *\n * KNOWN LIMITATION: the streaming path does not run a function-call resolution\n * loop — parity with the current unary behavior.\n */\nexport function useTextChatStream(options: UseTextChatStreamOptions) {\n const {\n streamUrl,\n limit = 50,\n revealIntervalMs = REVEAL_INTERVAL_MS,\n revealCharsPerTick = REVEAL_CHARS_PER_TICK,\n headers,\n onError,\n } = options;\n\n const { chatId, namespace, personaChatService } = useSdkConsumer();\n\n const {\n data: sdkMessages = [],\n status,\n fetchNextPage,\n refetch,\n } = useInfiniteQuery({\n queryKey: ['chat-messages', chatId, namespace],\n queryFn: async ({ pageParam }: { pageParam?: string }) => {\n if (!personaChatService) {\n return { messages: [], nextCursor: undefined };\n }\n return personaChatService.queryChatMessages({\n chatId,\n namespace,\n limit,\n cursor: pageParam,\n });\n },\n getNextPageParam: (lastPage) => lastPage.nextCursor,\n initialPageParam: undefined as string | undefined,\n select: (data) =>\n data.pages.flatMap((page) => page.messages as ChatMessage[]),\n });\n\n // Local state for the in-progress streaming exchange (user + assistant messages).\n const [streamingMessages, setStreamingMessages] = useState<\n StreamingChatMessage[]\n >([]);\n // Exchanges that finished streaming, kept for the lifetime of this chat. They\n // keep their stable optimistic ids and stand in for the SDK's confirmed copies\n // (which are hidden as duplicates) — this both bridges the refetch lag and\n // avoids remounting the message (and restarting its typewriter) when the\n // confirmed copy, with a different id, arrives.\n const [completedMessages, setCompletedMessages] = useState<\n StreamingChatMessage[]\n >([]);\n const abortRef = useRef<AbortController | null>(null);\n // Interval that drips the buffered reply into the streaming message.\n const revealTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;\n // falling back to this keeps the history from disappearing and reappearing.\n const lastConfirmedRef = useRef<StreamingChatMessage[]>([]);\n // Monotonic per-hook counter so two sends within the same millisecond still\n // produce distinct message ids (used as React keys downstream).\n const sendSeqRef = useRef(0);\n\n const stopRevealTimer = useCallback(() => {\n if (revealTimerRef.current) {\n clearInterval(revealTimerRef.current);\n revealTimerRef.current = null;\n }\n }, []);\n\n // Stop any in-flight stream + reveal on unmount.\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n stopRevealTimer();\n };\n }, [stopRevealTimer]);\n\n const messages = useMemo<StreamingChatMessage[]>(() => {\n const converted = [...sdkMessages].reverse() as StreamingChatMessage[];\n const confirmedMessages =\n converted.length > 0 ? converted : lastConfirmedRef.current;\n\n // Show our locally-tracked streamed exchanges (stable ids) and hide the\n // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the\n // message (different id) and restart its typewriter — that's the flicker.\n // The in-progress streaming messages are always shown as-is.\n const completedKeys = new Set(completedMessages.map(messageKey));\n const confirmedShown = confirmedMessages.filter(\n (m) => !completedKeys.has(messageKey(m)),\n );\n return [...confirmedShown, ...completedMessages, ...streamingMessages];\n }, [sdkMessages, streamingMessages, completedMessages]);\n\n // Remember the latest non-empty confirmed list so a transient empty during a\n // refetch doesn't blank the history.\n useEffect(() => {\n const converted = [...sdkMessages].reverse() as StreamingChatMessage[];\n if (converted.length > 0) {\n lastConfirmedRef.current = converted;\n }\n }, [sdkMessages]);\n\n const send = useCallback(\n async ({ message, chatConfig, attachments }: SendStreamMessageParams) => {\n // Abort any in-flight stream + reveal before starting a new one.\n abortRef.current?.abort();\n stopRevealTimer();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const seq = (sendSeqRef.current += 1);\n const userMsgId = `stream-user-${Date.now()}-${seq}`;\n const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;\n\n const optimisticUserMsg: StreamingChatMessage = {\n id: userMsgId,\n role: 'USER',\n content: message,\n attachments,\n };\n\n const streamingAssistantMsg: StreamingChatMessage = {\n id: assistantMsgId,\n role: 'ASSISTANT',\n content: '',\n streamStatus: 'streaming',\n streamingText: '',\n };\n\n setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);\n\n // Tokens are buffered into `accumulated` as they arrive; `revealedLen`\n // tracks how much the drip timer has uncovered so far.\n let accumulated = '';\n let revealedLen = 0;\n let streamDone = false;\n\n const settle = () => {\n stopRevealTimer();\n // Mirror the streamed text into both `content` and `streamingText`. A\n // host renderer can diff the two to place its typer at the end, so the\n // settled message shows in full with no re-type or flicker. Kept in\n // `completedMessages` (stable id) so the confirmed copy from refetch is\n // hidden and never remounts.\n const finalAssistantMsg: StreamingChatMessage = {\n ...streamingAssistantMsg,\n content: accumulated,\n streamStatus: 'settled',\n streamingText: accumulated,\n };\n setCompletedMessages((prev) => [\n ...prev,\n optimisticUserMsg,\n finalAssistantMsg,\n ]);\n setStreamingMessages([]);\n // Sync the SDK's confirmed list only now — doing it mid-reveal would\n // surface a confirmed duplicate next to the still-revealing copy.\n void refetch();\n };\n\n const tick = () => {\n if (revealedLen < accumulated.length) {\n revealedLen = Math.min(\n accumulated.length,\n revealedLen + revealCharsPerTick,\n );\n const revealedText = accumulated.slice(0, revealedLen);\n setStreamingMessages([\n optimisticUserMsg,\n {\n ...streamingAssistantMsg,\n content: '',\n streamStatus: 'streaming',\n streamingText: revealedText,\n },\n ]);\n } else if (streamDone) {\n // Network finished and the reveal has caught up — settle.\n settle();\n }\n };\n\n const startReveal = () => {\n if (!revealTimerRef.current) {\n revealTimerRef.current = setInterval(tick, revealIntervalMs);\n }\n };\n\n // Streaming deliberately bypasses the injected `personaChatService` (the\n // DI seam every other hook uses): SSE needs a raw fetch with cookie auth\n // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't\n // model. History reads still go through the service (see the query above).\n await streamChatMessage(\n {\n url: streamUrl,\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n headers,\n },\n {\n onChunk: (token) => {\n accumulated += token;\n startReveal();\n },\n onDone: () => {\n // Let the drip finish revealing, then settle (see `tick`).\n streamDone = true;\n startReveal();\n },\n onError: (err) => {\n stopRevealTimer();\n onError?.(err);\n setStreamingMessages([]);\n },\n },\n controller.signal,\n );\n },\n [\n streamUrl,\n chatId,\n namespace,\n revealIntervalMs,\n revealCharsPerTick,\n headers,\n onError,\n refetch,\n stopRevealTimer,\n ],\n );\n\n return {\n status,\n // Present for symmetry with `useTextChat`; the streaming path tracks its own\n // optimistic state, so this is always empty.\n pending: [],\n messages,\n fetchNext,\n refetch,\n send,\n };\n\n function fetchNext() {\n void fetchNextPage();\n }\n}\n"],"mappings":";;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,WAAA,GAAAD,OAAA;AAEA,IAAAE,YAAA,GAAAF,OAAA;AACA,IAAAG,kBAAA,GAAAH,OAAA;AAAqEI,OAAA,CAAAC,iBAAA,GAAAF,kBAAA,CAAAE,iBAAA;AAErE;AACA;;AAOA;AACA;AACA;AACA;AACA;AACA,MAAMC,kBAAkB,GAAG,EAAE;AAC7B,MAAMC,qBAAqB,GAAG,CAAC;;AAE/B;AACA;AACA;AACA;AACA;;AAwBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,UAAU,GAAIC,CAAuB,IACzC,GAAGA,CAAC,CAACC,IAAI,KAAK,CAACD,CAAC,CAACE,OAAO,IAAI,EAAE,EAAEC,IAAI,CAAC,CAAC,EAAE;;AAE1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,iBAAiBA,CAACC,OAAiC,EAAE;EACnE,MAAM;IACJC,SAAS;IACTC,KAAK,GAAG,EAAE;IACVC,gBAAgB,GAAGX,kBAAkB;IACrCY,kBAAkB,GAAGX,qBAAqB;IAC1CY,OAAO;IACPC;EACF,CAAC,GAAGN,OAAO;EAEX,MAAM;IAAEO,MAAM;IAAEC,SAAS;IAAEC;EAAmB,CAAC,GAAG,IAAAC,2BAAc,EAAC,CAAC;EAElE,MAAM;IACJC,IAAI,EAAEC,WAAW,GAAG,EAAE;IACtBC,MAAM;IACNC,aAAa;IACbC;EACF,CAAC,GAAG,IAAAC,4BAAgB,EAAC;IACnBC,QAAQ,EAAE,CAAC,eAAe,EAAEV,MAAM,EAAEC,SAAS,CAAC;IAC9CU,OAAO,EAAE,MAAAA,CAAO;MAAEC;IAAkC,CAAC,KAAK;MACxD,IAAI,CAACV,kBAAkB,EAAE;QACvB,OAAO;UAAEW,QAAQ,EAAE,EAAE;UAAEC,UAAU,EAAEC;QAAU,CAAC;MAChD;MACA,OAAOb,kBAAkB,CAACc,iBAAiB,CAAC;QAC1ChB,MAAM;QACNC,SAAS;QACTN,KAAK;QACLsB,MAAM,EAAEL;MACV,CAAC,CAAC;IACJ,CAAC;IACDM,gBAAgB,EAAGC,QAAQ,IAAKA,QAAQ,CAACL,UAAU;IACnDM,gBAAgB,EAAEL,SAA+B;IACjDM,MAAM,EAAGjB,IAAI,IACXA,IAAI,CAACkB,KAAK,CAACC,OAAO,CAAEC,IAAI,IAAKA,IAAI,CAACX,QAAyB;EAC/D,CAAC,CAAC;;EAEF;EACA,MAAM,CAACY,iBAAiB,EAAEC,oBAAoB,CAAC,GAAG,IAAAC,eAAQ,EAExD,EAAE,CAAC;EACL;EACA;EACA;EACA;EACA;EACA,MAAM,CAACC,iBAAiB,EAAEC,oBAAoB,CAAC,GAAG,IAAAF,eAAQ,EAExD,EAAE,CAAC;EACL,MAAMG,QAAQ,GAAG,IAAAC,aAAM,EAAyB,IAAI,CAAC;EACrD;EACA,MAAMC,cAAc,GAAG,IAAAD,aAAM,EAAwC,IAAI,CAAC;EAC1E;EACA;EACA,MAAME,gBAAgB,GAAG,IAAAF,aAAM,EAAyB,EAAE,CAAC;EAC3D;EACA;EACA,MAAMG,UAAU,GAAG,IAAAH,aAAM,EAAC,CAAC,CAAC;EAE5B,MAAMI,eAAe,GAAG,IAAAC,kBAAW,EAAC,MAAM;IACxC,IAAIJ,cAAc,CAACK,OAAO,EAAE;MAC1BC,aAAa,CAACN,cAAc,CAACK,OAAO,CAAC;MACrCL,cAAc,CAACK,OAAO,GAAG,IAAI;IAC/B;EACF,CAAC,EAAE,EAAE,CAAC;;EAEN;EACA,IAAAE,gBAAS,EAAC,MAAM;IACd,OAAO,MAAM;MAAA,IAAAC,iBAAA;MACX,CAAAA,iBAAA,GAAAV,QAAQ,CAACO,OAAO,aAAhBG,iBAAA,CAAkBC,KAAK,CAAC,CAAC;MACzBN,eAAe,CAAC,CAAC;IACnB,CAAC;EACH,CAAC,EAAE,CAACA,eAAe,CAAC,CAAC;EAErB,MAAMtB,QAAQ,GAAG,IAAA6B,cAAO,EAAyB,MAAM;IACrD,MAAMC,SAAS,GAAG,CAAC,GAAGtC,WAAW,CAAC,CAACuC,OAAO,CAAC,CAA2B;IACtE,MAAMC,iBAAiB,GACrBF,SAAS,CAACG,MAAM,GAAG,CAAC,GAAGH,SAAS,GAAGV,gBAAgB,CAACI,OAAO;;IAE7D;IACA;IACA;IACA;IACA,MAAMU,aAAa,GAAG,IAAIC,GAAG,CAACpB,iBAAiB,CAACqB,GAAG,CAAC9D,UAAU,CAAC,CAAC;IAChE,MAAM+D,cAAc,GAAGL,iBAAiB,CAACM,MAAM,CAC5C/D,CAAC,IAAK,CAAC2D,aAAa,CAACK,GAAG,CAACjE,UAAU,CAACC,CAAC,CAAC,CACzC,CAAC;IACD,OAAO,CAAC,GAAG8D,cAAc,EAAE,GAAGtB,iBAAiB,EAAE,GAAGH,iBAAiB,CAAC;EACxE,CAAC,EAAE,CAACpB,WAAW,EAAEoB,iBAAiB,EAAEG,iBAAiB,CAAC,CAAC;;EAEvD;EACA;EACA,IAAAW,gBAAS,EAAC,MAAM;IACd,MAAMI,SAAS,GAAG,CAAC,GAAGtC,WAAW,CAAC,CAACuC,OAAO,CAAC,CAA2B;IACtE,IAAID,SAAS,CAACG,MAAM,GAAG,CAAC,EAAE;MACxBb,gBAAgB,CAACI,OAAO,GAAGM,SAAS;IACtC;EACF,CAAC,EAAE,CAACtC,WAAW,CAAC,CAAC;EAEjB,MAAMgD,IAAI,GAAG,IAAAjB,kBAAW,EACtB,OAAO;IAAEkB,OAAO;IAAEC,UAAU;IAAEC;EAAqC,CAAC,KAAK;IAAA,IAAAC,kBAAA;IACvE;IACA,CAAAA,kBAAA,GAAA3B,QAAQ,CAACO,OAAO,aAAhBoB,kBAAA,CAAkBhB,KAAK,CAAC,CAAC;IACzBN,eAAe,CAAC,CAAC;IACjB,MAAMuB,UAAU,GAAG,IAAIC,eAAe,CAAC,CAAC;IACxC7B,QAAQ,CAACO,OAAO,GAAGqB,UAAU;IAE7B,MAAME,GAAG,GAAI1B,UAAU,CAACG,OAAO,IAAI,CAAE;IACrC,MAAMwB,SAAS,GAAG,eAAeC,IAAI,CAACC,GAAG,CAAC,CAAC,IAAIH,GAAG,EAAE;IACpD,MAAMI,cAAc,GAAG,oBAAoBF,IAAI,CAACC,GAAG,CAAC,CAAC,IAAIH,GAAG,EAAE;IAE9D,MAAMK,iBAAuC,GAAG;MAC9CC,EAAE,EAAEL,SAAS;MACbxE,IAAI,EAAE,MAAM;MACZC,OAAO,EAAEgE,OAAO;MAChBE;IACF,CAAC;IAED,MAAMW,qBAA2C,GAAG;MAClDD,EAAE,EAAEF,cAAc;MAClB3E,IAAI,EAAE,WAAW;MACjBC,OAAO,EAAE,EAAE;MACX8E,YAAY,EAAE,WAAW;MACzBC,aAAa,EAAE;IACjB,CAAC;IAED3C,oBAAoB,CAAC,CAACuC,iBAAiB,EAAEE,qBAAqB,CAAC,CAAC;;IAEhE;IACA;IACA,IAAIG,WAAW,GAAG,EAAE;IACpB,IAAIC,WAAW,GAAG,CAAC;IACnB,IAAIC,UAAU,GAAG,KAAK;IAEtB,MAAMC,MAAM,GAAGA,CAAA,KAAM;MACnBtC,eAAe,CAAC,CAAC;MACjB;MACA;MACA;MACA;MACA;MACA,MAAMuC,iBAAuC,GAAG;QAC9C,GAAGP,qBAAqB;QACxB7E,OAAO,EAAEgF,WAAW;QACpBF,YAAY,EAAE,SAAS;QACvBC,aAAa,EAAEC;MACjB,CAAC;MACDzC,oBAAoB,CAAE8C,IAAI,IAAK,CAC7B,GAAGA,IAAI,EACPV,iBAAiB,EACjBS,iBAAiB,CAClB,CAAC;MACFhD,oBAAoB,CAAC,EAAE,CAAC;MACxB;MACA;MACA,KAAKlB,OAAO,CAAC,CAAC;IAChB,CAAC;IAED,MAAMoE,IAAI,GAAGA,CAAA,KAAM;MACjB,IAAIL,WAAW,GAAGD,WAAW,CAACxB,MAAM,EAAE;QACpCyB,WAAW,GAAGM,IAAI,CAACC,GAAG,CACpBR,WAAW,CAACxB,MAAM,EAClByB,WAAW,GAAG1E,kBAChB,CAAC;QACD,MAAMkF,YAAY,GAAGT,WAAW,CAACU,KAAK,CAAC,CAAC,EAAET,WAAW,CAAC;QACtD7C,oBAAoB,CAAC,CACnBuC,iBAAiB,EACjB;UACE,GAAGE,qBAAqB;UACxB7E,OAAO,EAAE,EAAE;UACX8E,YAAY,EAAE,WAAW;UACzBC,aAAa,EAAEU;QACjB,CAAC,CACF,CAAC;MACJ,CAAC,MAAM,IAAIP,UAAU,EAAE;QACrB;QACAC,MAAM,CAAC,CAAC;MACV;IACF,CAAC;IAED,MAAMQ,WAAW,GAAGA,CAAA,KAAM;MACxB,IAAI,CAACjD,cAAc,CAACK,OAAO,EAAE;QAC3BL,cAAc,CAACK,OAAO,GAAG6C,WAAW,CAACN,IAAI,EAAEhF,gBAAgB,CAAC;MAC9D;IACF,CAAC;;IAED;IACA;IACA;IACA;IACA,MAAM,IAAAZ,oCAAiB,EACrB;MACEmG,GAAG,EAAEzF,SAAS;MACdM,MAAM;MACNsD,OAAO;MACPrD,SAAS;MACTsD,UAAU;MACVC,WAAW;MACX1D;IACF,CAAC,EACD;MACEsF,OAAO,EAAGC,KAAK,IAAK;QAClBf,WAAW,IAAIe,KAAK;QACpBJ,WAAW,CAAC,CAAC;MACf,CAAC;MACDK,MAAM,EAAEA,CAAA,KAAM;QACZ;QACAd,UAAU,GAAG,IAAI;QACjBS,WAAW,CAAC,CAAC;MACf,CAAC;MACDlF,OAAO,EAAGwF,GAAG,IAAK;QAChBpD,eAAe,CAAC,CAAC;QACjBpC,OAAO,YAAPA,OAAO,CAAGwF,GAAG,CAAC;QACd7D,oBAAoB,CAAC,EAAE,CAAC;MAC1B;IACF,CAAC,EACDgC,UAAU,CAAC8B,MACb,CAAC;EACH,CAAC,EACD,CACE9F,SAAS,EACTM,MAAM,EACNC,SAAS,EACTL,gBAAgB,EAChBC,kBAAkB,EAClBC,OAAO,EACPC,OAAO,EACPS,OAAO,EACP2B,eAAe,CAEnB,CAAC;EAED,OAAO;IACL7B,MAAM;IACN;IACA;IACAmF,OAAO,EAAE,EAAE;IACX5E,QAAQ;IACR6E,SAAS;IACTlF,OAAO;IACP6C;EACF,CAAC;EAED,SAASqC,SAASA,CAAA,EAAG;IACnB,KAAKnF,aAAa,CAAC,CAAC;EACtB;AACF","ignoreList":[]}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.createObjectScanner = createObjectScanner;
5
+ exports.extractContent = extractContent;
6
+ exports.streamChatMessage = streamChatMessage;
7
+ // Pull the assistant text out of one streamed frame. Two framings reach us: a
8
+ // proxy route yields `{ content }` objects, while the gateway wraps each frame
9
+ // as `{ body: base64(json) }`. Returns null for frames without content.
10
+ function extractContent(frame) {
11
+ try {
12
+ const parsed = JSON.parse(frame);
13
+ if (typeof parsed.content === 'string') {
14
+ return parsed.content;
15
+ }
16
+ if (typeof parsed.body === 'string') {
17
+ const decoded = new TextDecoder().decode(Uint8Array.from(atob(parsed.body), char => char.charCodeAt(0)));
18
+ const inner = JSON.parse(decoded);
19
+ return typeof inner.content === 'string' ? inner.content : null;
20
+ }
21
+ } catch {
22
+ // Incomplete or non-object frame — skip it.
23
+ }
24
+ return null;
25
+ }
26
+
27
+ // Feeds decoded text through a running scanner that emits each complete
28
+ // top-level `{…}` object as its closing brace arrives. This keeps a message
29
+ // that's split across reads intact, and parses a newline-free JSON array (the
30
+ // gateway framing) incrementally. Anything outside a top-level object is ignored.
31
+ function createObjectScanner(onObject) {
32
+ let buffer = '';
33
+ let cursor = 0;
34
+ let braceDepth = 0;
35
+ let objectStart = -1;
36
+ let inString = false;
37
+ let escaped = false;
38
+ return function feed(text) {
39
+ buffer += text;
40
+ for (; cursor < buffer.length; cursor++) {
41
+ const char = buffer[cursor];
42
+ if (inString) {
43
+ if (escaped) {
44
+ escaped = false;
45
+ } else if (char === '\\') {
46
+ escaped = true;
47
+ } else if (char === '"') {
48
+ inString = false;
49
+ }
50
+ continue;
51
+ }
52
+ if (char === '"') {
53
+ inString = true;
54
+ } else if (char === '{') {
55
+ if (braceDepth === 0) {
56
+ objectStart = cursor;
57
+ }
58
+ braceDepth++;
59
+ } else if (char === '}' && braceDepth > 0) {
60
+ braceDepth--;
61
+ if (braceDepth === 0 && objectStart >= 0) {
62
+ onObject(buffer.slice(objectStart, cursor + 1));
63
+ objectStart = -1;
64
+ }
65
+ }
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Framework-agnostic streaming transport. POSTs the message to the
72
+ * caller-supplied SSE endpoint, reads the response body incrementally, and
73
+ * fires `onChunk` per assistant token, `onDone` on close, `onError` on failure.
74
+ * A caller-supplied `AbortSignal` cancels the request; `AbortError` is swallowed.
75
+ */
76
+ async function streamChatMessage(params, callbacks, signal) {
77
+ const {
78
+ url,
79
+ chatId,
80
+ message,
81
+ namespace,
82
+ chatConfig,
83
+ attachments,
84
+ headers
85
+ } = params;
86
+ const {
87
+ onChunk,
88
+ onDone,
89
+ onError
90
+ } = callbacks;
91
+ try {
92
+ const response = await fetch(url, {
93
+ method: 'POST',
94
+ credentials: 'include',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ ...headers
98
+ },
99
+ body: JSON.stringify({
100
+ chatId,
101
+ message,
102
+ namespace,
103
+ chatConfig,
104
+ attachments
105
+ }),
106
+ signal
107
+ });
108
+ if (!response.ok || !response.body) {
109
+ onError(new Error(`Stream request failed: ${response.status}`));
110
+ return;
111
+ }
112
+ const scanForObjects = createObjectScanner(frame => {
113
+ const content = extractContent(frame);
114
+ if (content !== null) {
115
+ onChunk(content);
116
+ }
117
+ });
118
+ const reader = response.body.getReader();
119
+ const decoder = new TextDecoder();
120
+ for (;;) {
121
+ const {
122
+ done,
123
+ value
124
+ } = await reader.read();
125
+ if (done) {
126
+ break;
127
+ }
128
+ scanForObjects(decoder.decode(value, {
129
+ stream: true
130
+ }));
131
+ }
132
+ scanForObjects(decoder.decode());
133
+ onDone();
134
+ } catch (err) {
135
+ if (err.name !== 'AbortError') {
136
+ onError(err);
137
+ }
138
+ }
139
+ }
140
+ //# sourceMappingURL=streamChatMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["extractContent","frame","parsed","JSON","parse","content","body","decoded","TextDecoder","decode","Uint8Array","from","atob","char","charCodeAt","inner","createObjectScanner","onObject","buffer","cursor","braceDepth","objectStart","inString","escaped","feed","text","length","slice","streamChatMessage","params","callbacks","signal","url","chatId","message","namespace","chatConfig","attachments","headers","onChunk","onDone","onError","response","fetch","method","credentials","stringify","ok","Error","status","scanForObjects","reader","getReader","decoder","done","value","read","stream","err","name"],"sources":["../../../src/services/streamChatMessage.ts"],"sourcesContent":["import type { ChatMessageAttachment } from '../types';\n\nexport type StreamCallbacks = {\n onChunk: (token: string) => void;\n onDone: () => void;\n onError: (err: Error) => void;\n};\n\nexport type StreamChatMessageParams = {\n // The caller supplies the endpoint (e.g. a proxied SSE route URL). The SDK\n // stays agnostic about base paths and gateway passthroughs.\n //\n // SECURITY: the request is sent with `credentials: 'include'`, so the session\n // cookie is delivered to whatever origin this resolves to. It must come from\n // trusted host/config — never from user input.\n url: string;\n chatId: string;\n message: string;\n namespace: string;\n chatConfig?: object;\n attachments?: ChatMessageAttachment[];\n // Extra request headers merged onto the POST. Lets the caller satisfy the\n // host's transport requirements the SDK can't know about — e.g. an\n // `x-xsrf-token` when the endpoint sits behind a CSRF-protected gateway.\n headers?: Record<string, string>;\n};\n\n// Pull the assistant text out of one streamed frame. Two framings reach us: a\n// proxy route yields `{ content }` objects, while the gateway wraps each frame\n// as `{ body: base64(json) }`. Returns null for frames without content.\nexport function extractContent(frame: string): string | null {\n try {\n const parsed = JSON.parse(frame);\n if (typeof parsed.content === 'string') {\n return parsed.content;\n }\n if (typeof parsed.body === 'string') {\n const decoded = new TextDecoder().decode(\n Uint8Array.from(atob(parsed.body), (char) => char.charCodeAt(0)),\n );\n const inner = JSON.parse(decoded);\n return typeof inner.content === 'string' ? inner.content : null;\n }\n } catch {\n // Incomplete or non-object frame — skip it.\n }\n return null;\n}\n\n// Feeds decoded text through a running scanner that emits each complete\n// top-level `{…}` object as its closing brace arrives. This keeps a message\n// that's split across reads intact, and parses a newline-free JSON array (the\n// gateway framing) incrementally. Anything outside a top-level object is ignored.\nexport function createObjectScanner(onObject: (frame: string) => void) {\n let buffer = '';\n let cursor = 0;\n let braceDepth = 0;\n let objectStart = -1;\n let inString = false;\n let escaped = false;\n\n return function feed(text: string) {\n buffer += text;\n for (; cursor < buffer.length; cursor++) {\n const char = buffer[cursor];\n if (inString) {\n if (escaped) {\n escaped = false;\n } else if (char === '\\\\') {\n escaped = true;\n } else if (char === '\"') {\n inString = false;\n }\n continue;\n }\n if (char === '\"') {\n inString = true;\n } else if (char === '{') {\n if (braceDepth === 0) {\n objectStart = cursor;\n }\n braceDepth++;\n } else if (char === '}' && braceDepth > 0) {\n braceDepth--;\n if (braceDepth === 0 && objectStart >= 0) {\n onObject(buffer.slice(objectStart, cursor + 1));\n objectStart = -1;\n }\n }\n }\n };\n}\n\n/**\n * Framework-agnostic streaming transport. POSTs the message to the\n * caller-supplied SSE endpoint, reads the response body incrementally, and\n * fires `onChunk` per assistant token, `onDone` on close, `onError` on failure.\n * A caller-supplied `AbortSignal` cancels the request; `AbortError` is swallowed.\n */\nexport async function streamChatMessage(\n params: StreamChatMessageParams,\n callbacks: StreamCallbacks,\n signal?: AbortSignal,\n): Promise<void> {\n const { url, chatId, message, namespace, chatConfig, attachments, headers } =\n params;\n const { onChunk, onDone, onError } = callbacks;\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...headers },\n body: JSON.stringify({\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n }),\n signal,\n });\n\n if (!response.ok || !response.body) {\n onError(new Error(`Stream request failed: ${response.status}`));\n return;\n }\n\n const scanForObjects = createObjectScanner((frame) => {\n const content = extractContent(frame);\n if (content !== null) {\n onChunk(content);\n }\n });\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n scanForObjects(decoder.decode(value, { stream: true }));\n }\n scanForObjects(decoder.decode());\n onDone();\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n onError(err as Error);\n }\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,2 +1,3 @@
1
1
  export * from './useTextChat';
2
+ export * from './useTextChatStream';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["../../../../src/hooks/useTextChat/index.ts"],"sourcesContent":["export * from './useTextChat';\n"],"mappings":"AAAA,cAAc,eAAe","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["../../../../src/hooks/useTextChat/index.ts"],"sourcesContent":["export * from './useTextChat';\nexport * from './useTextChatStream';\n"],"mappings":"AAAA,cAAc,eAAe;AAC7B,cAAc,qBAAqB","ignoreList":[]}
@@ -0,0 +1,259 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useInfiniteQuery } from '@tanstack/react-query';
3
+ import { useSdkConsumer } from '../../sdk-consumer';
4
+ import { streamChatMessage } from '../../services/streamChatMessage';
5
+
6
+ // Re-exported here so consumers can reach the framework-agnostic transport
7
+ // alongside the hook.
8
+ export { streamChatMessage } from '../../services/streamChatMessage';
9
+
10
+ // Progressive-reveal drip. We uncover the buffered reply a few chars at a time
11
+ // on a timer rather than dumping each network chunk in. The effective rate
12
+ // (~100 chars/s) is kept just under a typical chat-UI typewriter's own rate so
13
+ // the typer always catches up between drips, yielding a smooth type-out whether
14
+ // tokens trickle in or arrive in one burst.
15
+ const REVEAL_INTERVAL_MS = 30;
16
+ const REVEAL_CHARS_PER_TICK = 3;
17
+
18
+ /**
19
+ * A `ChatMessage` augmented with streaming metadata. The host renderer uses
20
+ * `streamStatus`/`streamingText` to drive its typewriter; confirmed
21
+ * (non-streamed) messages carry neither.
22
+ */
23
+
24
+ /**
25
+ * Identity for deduping optimistic streamed messages against confirmed ones.
26
+ *
27
+ * This is a *content* join: the streamed text must equal the server-persisted
28
+ * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-
29
+ * whitespace differences, but heavier server-side post-processing (markdown /
30
+ * entity normalization, an appended citation) would make the keys diverge and
31
+ * briefly surface both copies. A shared message id would be more robust, but
32
+ * the streamed frames carry only token deltas — no id — so content equality is
33
+ * the best join available without backend support.
34
+ */
35
+ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
36
+
37
+ /**
38
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
39
+ * and progressively reveals the reply, merging the in-flight and completed
40
+ * streamed exchanges with the confirmed history (oldest-first).
41
+ *
42
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
43
+ * loop — parity with the current unary behavior.
44
+ */
45
+ export function useTextChatStream(options) {
46
+ const streamUrl = options.streamUrl,
47
+ _options$limit = options.limit,
48
+ limit = _options$limit === void 0 ? 50 : _options$limit,
49
+ _options$revealInterv = options.revealIntervalMs,
50
+ revealIntervalMs = _options$revealInterv === void 0 ? REVEAL_INTERVAL_MS : _options$revealInterv,
51
+ _options$revealCharsP = options.revealCharsPerTick,
52
+ revealCharsPerTick = _options$revealCharsP === void 0 ? REVEAL_CHARS_PER_TICK : _options$revealCharsP,
53
+ headers = options.headers,
54
+ onError = options.onError;
55
+ const _useSdkConsumer = useSdkConsumer(),
56
+ chatId = _useSdkConsumer.chatId,
57
+ namespace = _useSdkConsumer.namespace,
58
+ personaChatService = _useSdkConsumer.personaChatService;
59
+ const _useInfiniteQuery = useInfiniteQuery({
60
+ queryKey: ['chat-messages', chatId, namespace],
61
+ queryFn: async _ref => {
62
+ let pageParam = _ref.pageParam;
63
+ if (!personaChatService) {
64
+ return {
65
+ messages: [],
66
+ nextCursor: undefined
67
+ };
68
+ }
69
+ return personaChatService.queryChatMessages({
70
+ chatId,
71
+ namespace,
72
+ limit,
73
+ cursor: pageParam
74
+ });
75
+ },
76
+ getNextPageParam: lastPage => lastPage.nextCursor,
77
+ initialPageParam: undefined,
78
+ select: data => data.pages.flatMap(page => page.messages)
79
+ }),
80
+ _useInfiniteQuery$dat = _useInfiniteQuery.data,
81
+ sdkMessages = _useInfiniteQuery$dat === void 0 ? [] : _useInfiniteQuery$dat,
82
+ status = _useInfiniteQuery.status,
83
+ fetchNextPage = _useInfiniteQuery.fetchNextPage,
84
+ refetch = _useInfiniteQuery.refetch;
85
+
86
+ // Local state for the in-progress streaming exchange (user + assistant messages).
87
+ const _useState = useState([]),
88
+ streamingMessages = _useState[0],
89
+ setStreamingMessages = _useState[1];
90
+ // Exchanges that finished streaming, kept for the lifetime of this chat. They
91
+ // keep their stable optimistic ids and stand in for the SDK's confirmed copies
92
+ // (which are hidden as duplicates) — this both bridges the refetch lag and
93
+ // avoids remounting the message (and restarting its typewriter) when the
94
+ // confirmed copy, with a different id, arrives.
95
+ const _useState2 = useState([]),
96
+ completedMessages = _useState2[0],
97
+ setCompletedMessages = _useState2[1];
98
+ const abortRef = useRef(null);
99
+ // Interval that drips the buffered reply into the streaming message.
100
+ const revealTimerRef = useRef(null);
101
+ // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
102
+ // falling back to this keeps the history from disappearing and reappearing.
103
+ const lastConfirmedRef = useRef([]);
104
+ // Monotonic per-hook counter so two sends within the same millisecond still
105
+ // produce distinct message ids (used as React keys downstream).
106
+ const sendSeqRef = useRef(0);
107
+ const stopRevealTimer = useCallback(() => {
108
+ if (revealTimerRef.current) {
109
+ clearInterval(revealTimerRef.current);
110
+ revealTimerRef.current = null;
111
+ }
112
+ }, []);
113
+
114
+ // Stop any in-flight stream + reveal on unmount.
115
+ useEffect(() => {
116
+ return () => {
117
+ var _abortRef$current;
118
+ (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
119
+ stopRevealTimer();
120
+ };
121
+ }, [stopRevealTimer]);
122
+ const messages = useMemo(() => {
123
+ const converted = [...sdkMessages].reverse();
124
+ const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
125
+
126
+ // Show our locally-tracked streamed exchanges (stable ids) and hide the
127
+ // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the
128
+ // message (different id) and restart its typewriter — that's the flicker.
129
+ // The in-progress streaming messages are always shown as-is.
130
+ const completedKeys = new Set(completedMessages.map(messageKey));
131
+ const confirmedShown = confirmedMessages.filter(m => !completedKeys.has(messageKey(m)));
132
+ return [...confirmedShown, ...completedMessages, ...streamingMessages];
133
+ }, [sdkMessages, streamingMessages, completedMessages]);
134
+
135
+ // Remember the latest non-empty confirmed list so a transient empty during a
136
+ // refetch doesn't blank the history.
137
+ useEffect(() => {
138
+ const converted = [...sdkMessages].reverse();
139
+ if (converted.length > 0) {
140
+ lastConfirmedRef.current = converted;
141
+ }
142
+ }, [sdkMessages]);
143
+ const send = useCallback(async _ref2 => {
144
+ var _abortRef$current2;
145
+ let message = _ref2.message,
146
+ chatConfig = _ref2.chatConfig,
147
+ attachments = _ref2.attachments;
148
+ // Abort any in-flight stream + reveal before starting a new one.
149
+ (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
150
+ stopRevealTimer();
151
+ const controller = new AbortController();
152
+ abortRef.current = controller;
153
+ const seq = sendSeqRef.current += 1;
154
+ const userMsgId = `stream-user-${Date.now()}-${seq}`;
155
+ const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;
156
+ const optimisticUserMsg = {
157
+ id: userMsgId,
158
+ role: 'USER',
159
+ content: message,
160
+ attachments
161
+ };
162
+ const streamingAssistantMsg = {
163
+ id: assistantMsgId,
164
+ role: 'ASSISTANT',
165
+ content: '',
166
+ streamStatus: 'streaming',
167
+ streamingText: ''
168
+ };
169
+ setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
170
+
171
+ // Tokens are buffered into `accumulated` as they arrive; `revealedLen`
172
+ // tracks how much the drip timer has uncovered so far.
173
+ let accumulated = '';
174
+ let revealedLen = 0;
175
+ let streamDone = false;
176
+ const settle = () => {
177
+ stopRevealTimer();
178
+ // Mirror the streamed text into both `content` and `streamingText`. A
179
+ // host renderer can diff the two to place its typer at the end, so the
180
+ // settled message shows in full with no re-type or flicker. Kept in
181
+ // `completedMessages` (stable id) so the confirmed copy from refetch is
182
+ // hidden and never remounts.
183
+ const finalAssistantMsg = {
184
+ ...streamingAssistantMsg,
185
+ content: accumulated,
186
+ streamStatus: 'settled',
187
+ streamingText: accumulated
188
+ };
189
+ setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
190
+ setStreamingMessages([]);
191
+ // Sync the SDK's confirmed list only now — doing it mid-reveal would
192
+ // surface a confirmed duplicate next to the still-revealing copy.
193
+ void refetch();
194
+ };
195
+ const tick = () => {
196
+ if (revealedLen < accumulated.length) {
197
+ revealedLen = Math.min(accumulated.length, revealedLen + revealCharsPerTick);
198
+ const revealedText = accumulated.slice(0, revealedLen);
199
+ setStreamingMessages([optimisticUserMsg, {
200
+ ...streamingAssistantMsg,
201
+ content: '',
202
+ streamStatus: 'streaming',
203
+ streamingText: revealedText
204
+ }]);
205
+ } else if (streamDone) {
206
+ // Network finished and the reveal has caught up — settle.
207
+ settle();
208
+ }
209
+ };
210
+ const startReveal = () => {
211
+ if (!revealTimerRef.current) {
212
+ revealTimerRef.current = setInterval(tick, revealIntervalMs);
213
+ }
214
+ };
215
+
216
+ // Streaming deliberately bypasses the injected `personaChatService` (the
217
+ // DI seam every other hook uses): SSE needs a raw fetch with cookie auth
218
+ // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't
219
+ // model. History reads still go through the service (see the query above).
220
+ await streamChatMessage({
221
+ url: streamUrl,
222
+ chatId,
223
+ message,
224
+ namespace,
225
+ chatConfig,
226
+ attachments,
227
+ headers
228
+ }, {
229
+ onChunk: token => {
230
+ accumulated += token;
231
+ startReveal();
232
+ },
233
+ onDone: () => {
234
+ // Let the drip finish revealing, then settle (see `tick`).
235
+ streamDone = true;
236
+ startReveal();
237
+ },
238
+ onError: err => {
239
+ stopRevealTimer();
240
+ onError == null || onError(err);
241
+ setStreamingMessages([]);
242
+ }
243
+ }, controller.signal);
244
+ }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
245
+ return {
246
+ status,
247
+ // Present for symmetry with `useTextChat`; the streaming path tracks its own
248
+ // optimistic state, so this is always empty.
249
+ pending: [],
250
+ messages,
251
+ fetchNext,
252
+ refetch,
253
+ send
254
+ };
255
+ function fetchNext() {
256
+ void fetchNextPage();
257
+ }
258
+ }
259
+ //# sourceMappingURL=useTextChatStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useCallback","useEffect","useMemo","useRef","useState","useInfiniteQuery","useSdkConsumer","streamChatMessage","REVEAL_INTERVAL_MS","REVEAL_CHARS_PER_TICK","messageKey","m","role","content","trim","useTextChatStream","options","streamUrl","_options$limit","limit","_options$revealInterv","revealIntervalMs","_options$revealCharsP","revealCharsPerTick","headers","onError","_useSdkConsumer","chatId","namespace","personaChatService","_useInfiniteQuery","queryKey","queryFn","_ref","pageParam","messages","nextCursor","undefined","queryChatMessages","cursor","getNextPageParam","lastPage","initialPageParam","select","data","pages","flatMap","page","_useInfiniteQuery$dat","sdkMessages","status","fetchNextPage","refetch","_useState","streamingMessages","setStreamingMessages","_useState2","completedMessages","setCompletedMessages","abortRef","revealTimerRef","lastConfirmedRef","sendSeqRef","stopRevealTimer","current","clearInterval","_abortRef$current","abort","converted","reverse","confirmedMessages","length","completedKeys","Set","map","confirmedShown","filter","has","send","_ref2","_abortRef$current2","message","chatConfig","attachments","controller","AbortController","seq","userMsgId","Date","now","assistantMsgId","optimisticUserMsg","id","streamingAssistantMsg","streamStatus","streamingText","accumulated","revealedLen","streamDone","settle","finalAssistantMsg","prev","tick","Math","min","revealedText","slice","startReveal","setInterval","url","onChunk","token","onDone","err","signal","pending","fetchNext"],"sources":["../../../../src/hooks/useTextChat/useTextChatStream.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport type { ChatMessage, ChatMessageAttachment } from '../../types';\nimport { useSdkConsumer } from '../../sdk-consumer';\nimport { streamChatMessage } from '../../services/streamChatMessage';\n\n// Re-exported here so consumers can reach the framework-agnostic transport\n// alongside the hook.\nexport {\n streamChatMessage,\n type StreamCallbacks,\n type StreamChatMessageParams,\n} from '../../services/streamChatMessage';\n\n// Progressive-reveal drip. We uncover the buffered reply a few chars at a time\n// on a timer rather than dumping each network chunk in. The effective rate\n// (~100 chars/s) is kept just under a typical chat-UI typewriter's own rate so\n// the typer always catches up between drips, yielding a smooth type-out whether\n// tokens trickle in or arrive in one burst.\nconst REVEAL_INTERVAL_MS = 30;\nconst REVEAL_CHARS_PER_TICK = 3;\n\n/**\n * A `ChatMessage` augmented with streaming metadata. The host renderer uses\n * `streamStatus`/`streamingText` to drive its typewriter; confirmed\n * (non-streamed) messages carry neither.\n */\nexport type StreamingChatMessage = ChatMessage & {\n streamingText?: string;\n streamStatus?: 'streaming' | 'settled';\n};\n\nexport interface UseTextChatStreamOptions {\n streamUrl: string;\n limit?: number;\n revealIntervalMs?: number;\n revealCharsPerTick?: number;\n // Extra headers forwarded on every stream request — e.g. an `x-xsrf-token`\n // when `streamUrl` sits behind a CSRF-protected gateway.\n headers?: Record<string, string>;\n // Stream failures surfaced to the host (e.g. to report to Sentry + toast).\n onError?: (err: Error) => void;\n}\n\nexport interface SendStreamMessageParams {\n message: string;\n chatConfig?: object;\n attachments?: ChatMessageAttachment[];\n}\n\n/**\n * Identity for deduping optimistic streamed messages against confirmed ones.\n *\n * This is a *content* join: the streamed text must equal the server-persisted\n * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-\n * whitespace differences, but heavier server-side post-processing (markdown /\n * entity normalization, an appended citation) would make the keys diverge and\n * briefly surface both copies. A shared message id would be more robust, but\n * the streamed frames carry only token deltas — no id — so content equality is\n * the best join available without backend support.\n */\nconst messageKey = (m: StreamingChatMessage) =>\n `${m.role}::${(m.content ?? '').trim()}`;\n\n/**\n * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport\n * and progressively reveals the reply, merging the in-flight and completed\n * streamed exchanges with the confirmed history (oldest-first).\n *\n * KNOWN LIMITATION: the streaming path does not run a function-call resolution\n * loop — parity with the current unary behavior.\n */\nexport function useTextChatStream(options: UseTextChatStreamOptions) {\n const {\n streamUrl,\n limit = 50,\n revealIntervalMs = REVEAL_INTERVAL_MS,\n revealCharsPerTick = REVEAL_CHARS_PER_TICK,\n headers,\n onError,\n } = options;\n\n const { chatId, namespace, personaChatService } = useSdkConsumer();\n\n const {\n data: sdkMessages = [],\n status,\n fetchNextPage,\n refetch,\n } = useInfiniteQuery({\n queryKey: ['chat-messages', chatId, namespace],\n queryFn: async ({ pageParam }: { pageParam?: string }) => {\n if (!personaChatService) {\n return { messages: [], nextCursor: undefined };\n }\n return personaChatService.queryChatMessages({\n chatId,\n namespace,\n limit,\n cursor: pageParam,\n });\n },\n getNextPageParam: (lastPage) => lastPage.nextCursor,\n initialPageParam: undefined as string | undefined,\n select: (data) =>\n data.pages.flatMap((page) => page.messages as ChatMessage[]),\n });\n\n // Local state for the in-progress streaming exchange (user + assistant messages).\n const [streamingMessages, setStreamingMessages] = useState<\n StreamingChatMessage[]\n >([]);\n // Exchanges that finished streaming, kept for the lifetime of this chat. They\n // keep their stable optimistic ids and stand in for the SDK's confirmed copies\n // (which are hidden as duplicates) — this both bridges the refetch lag and\n // avoids remounting the message (and restarting its typewriter) when the\n // confirmed copy, with a different id, arrives.\n const [completedMessages, setCompletedMessages] = useState<\n StreamingChatMessage[]\n >([]);\n const abortRef = useRef<AbortController | null>(null);\n // Interval that drips the buffered reply into the streaming message.\n const revealTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;\n // falling back to this keeps the history from disappearing and reappearing.\n const lastConfirmedRef = useRef<StreamingChatMessage[]>([]);\n // Monotonic per-hook counter so two sends within the same millisecond still\n // produce distinct message ids (used as React keys downstream).\n const sendSeqRef = useRef(0);\n\n const stopRevealTimer = useCallback(() => {\n if (revealTimerRef.current) {\n clearInterval(revealTimerRef.current);\n revealTimerRef.current = null;\n }\n }, []);\n\n // Stop any in-flight stream + reveal on unmount.\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n stopRevealTimer();\n };\n }, [stopRevealTimer]);\n\n const messages = useMemo<StreamingChatMessage[]>(() => {\n const converted = [...sdkMessages].reverse() as StreamingChatMessage[];\n const confirmedMessages =\n converted.length > 0 ? converted : lastConfirmedRef.current;\n\n // Show our locally-tracked streamed exchanges (stable ids) and hide the\n // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the\n // message (different id) and restart its typewriter — that's the flicker.\n // The in-progress streaming messages are always shown as-is.\n const completedKeys = new Set(completedMessages.map(messageKey));\n const confirmedShown = confirmedMessages.filter(\n (m) => !completedKeys.has(messageKey(m)),\n );\n return [...confirmedShown, ...completedMessages, ...streamingMessages];\n }, [sdkMessages, streamingMessages, completedMessages]);\n\n // Remember the latest non-empty confirmed list so a transient empty during a\n // refetch doesn't blank the history.\n useEffect(() => {\n const converted = [...sdkMessages].reverse() as StreamingChatMessage[];\n if (converted.length > 0) {\n lastConfirmedRef.current = converted;\n }\n }, [sdkMessages]);\n\n const send = useCallback(\n async ({ message, chatConfig, attachments }: SendStreamMessageParams) => {\n // Abort any in-flight stream + reveal before starting a new one.\n abortRef.current?.abort();\n stopRevealTimer();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const seq = (sendSeqRef.current += 1);\n const userMsgId = `stream-user-${Date.now()}-${seq}`;\n const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;\n\n const optimisticUserMsg: StreamingChatMessage = {\n id: userMsgId,\n role: 'USER',\n content: message,\n attachments,\n };\n\n const streamingAssistantMsg: StreamingChatMessage = {\n id: assistantMsgId,\n role: 'ASSISTANT',\n content: '',\n streamStatus: 'streaming',\n streamingText: '',\n };\n\n setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);\n\n // Tokens are buffered into `accumulated` as they arrive; `revealedLen`\n // tracks how much the drip timer has uncovered so far.\n let accumulated = '';\n let revealedLen = 0;\n let streamDone = false;\n\n const settle = () => {\n stopRevealTimer();\n // Mirror the streamed text into both `content` and `streamingText`. A\n // host renderer can diff the two to place its typer at the end, so the\n // settled message shows in full with no re-type or flicker. Kept in\n // `completedMessages` (stable id) so the confirmed copy from refetch is\n // hidden and never remounts.\n const finalAssistantMsg: StreamingChatMessage = {\n ...streamingAssistantMsg,\n content: accumulated,\n streamStatus: 'settled',\n streamingText: accumulated,\n };\n setCompletedMessages((prev) => [\n ...prev,\n optimisticUserMsg,\n finalAssistantMsg,\n ]);\n setStreamingMessages([]);\n // Sync the SDK's confirmed list only now — doing it mid-reveal would\n // surface a confirmed duplicate next to the still-revealing copy.\n void refetch();\n };\n\n const tick = () => {\n if (revealedLen < accumulated.length) {\n revealedLen = Math.min(\n accumulated.length,\n revealedLen + revealCharsPerTick,\n );\n const revealedText = accumulated.slice(0, revealedLen);\n setStreamingMessages([\n optimisticUserMsg,\n {\n ...streamingAssistantMsg,\n content: '',\n streamStatus: 'streaming',\n streamingText: revealedText,\n },\n ]);\n } else if (streamDone) {\n // Network finished and the reveal has caught up — settle.\n settle();\n }\n };\n\n const startReveal = () => {\n if (!revealTimerRef.current) {\n revealTimerRef.current = setInterval(tick, revealIntervalMs);\n }\n };\n\n // Streaming deliberately bypasses the injected `personaChatService` (the\n // DI seam every other hook uses): SSE needs a raw fetch with cookie auth\n // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't\n // model. History reads still go through the service (see the query above).\n await streamChatMessage(\n {\n url: streamUrl,\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n headers,\n },\n {\n onChunk: (token) => {\n accumulated += token;\n startReveal();\n },\n onDone: () => {\n // Let the drip finish revealing, then settle (see `tick`).\n streamDone = true;\n startReveal();\n },\n onError: (err) => {\n stopRevealTimer();\n onError?.(err);\n setStreamingMessages([]);\n },\n },\n controller.signal,\n );\n },\n [\n streamUrl,\n chatId,\n namespace,\n revealIntervalMs,\n revealCharsPerTick,\n headers,\n onError,\n refetch,\n stopRevealTimer,\n ],\n );\n\n return {\n status,\n // Present for symmetry with `useTextChat`; the streaming path tracks its own\n // optimistic state, so this is always empty.\n pending: [],\n messages,\n fetchNext,\n refetch,\n send,\n };\n\n function fetchNext() {\n void fetchNextPage();\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,129 @@
1
+ // Pull the assistant text out of one streamed frame. Two framings reach us: a
2
+ // proxy route yields `{ content }` objects, while the gateway wraps each frame
3
+ // as `{ body: base64(json) }`. Returns null for frames without content.
4
+ export function extractContent(frame) {
5
+ try {
6
+ const parsed = JSON.parse(frame);
7
+ if (typeof parsed.content === 'string') {
8
+ return parsed.content;
9
+ }
10
+ if (typeof parsed.body === 'string') {
11
+ const decoded = new TextDecoder().decode(Uint8Array.from(atob(parsed.body), char => char.charCodeAt(0)));
12
+ const inner = JSON.parse(decoded);
13
+ return typeof inner.content === 'string' ? inner.content : null;
14
+ }
15
+ } catch {
16
+ // Incomplete or non-object frame — skip it.
17
+ }
18
+ return null;
19
+ }
20
+
21
+ // Feeds decoded text through a running scanner that emits each complete
22
+ // top-level `{…}` object as its closing brace arrives. This keeps a message
23
+ // that's split across reads intact, and parses a newline-free JSON array (the
24
+ // gateway framing) incrementally. Anything outside a top-level object is ignored.
25
+ export function createObjectScanner(onObject) {
26
+ let buffer = '';
27
+ let cursor = 0;
28
+ let braceDepth = 0;
29
+ let objectStart = -1;
30
+ let inString = false;
31
+ let escaped = false;
32
+ return function feed(text) {
33
+ buffer += text;
34
+ for (; cursor < buffer.length; cursor++) {
35
+ const char = buffer[cursor];
36
+ if (inString) {
37
+ if (escaped) {
38
+ escaped = false;
39
+ } else if (char === '\\') {
40
+ escaped = true;
41
+ } else if (char === '"') {
42
+ inString = false;
43
+ }
44
+ continue;
45
+ }
46
+ if (char === '"') {
47
+ inString = true;
48
+ } else if (char === '{') {
49
+ if (braceDepth === 0) {
50
+ objectStart = cursor;
51
+ }
52
+ braceDepth++;
53
+ } else if (char === '}' && braceDepth > 0) {
54
+ braceDepth--;
55
+ if (braceDepth === 0 && objectStart >= 0) {
56
+ onObject(buffer.slice(objectStart, cursor + 1));
57
+ objectStart = -1;
58
+ }
59
+ }
60
+ }
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Framework-agnostic streaming transport. POSTs the message to the
66
+ * caller-supplied SSE endpoint, reads the response body incrementally, and
67
+ * fires `onChunk` per assistant token, `onDone` on close, `onError` on failure.
68
+ * A caller-supplied `AbortSignal` cancels the request; `AbortError` is swallowed.
69
+ */
70
+ export async function streamChatMessage(params, callbacks, signal) {
71
+ const url = params.url,
72
+ chatId = params.chatId,
73
+ message = params.message,
74
+ namespace = params.namespace,
75
+ chatConfig = params.chatConfig,
76
+ attachments = params.attachments,
77
+ headers = params.headers;
78
+ const onChunk = callbacks.onChunk,
79
+ onDone = callbacks.onDone,
80
+ onError = callbacks.onError;
81
+ try {
82
+ const response = await fetch(url, {
83
+ method: 'POST',
84
+ credentials: 'include',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ ...headers
88
+ },
89
+ body: JSON.stringify({
90
+ chatId,
91
+ message,
92
+ namespace,
93
+ chatConfig,
94
+ attachments
95
+ }),
96
+ signal
97
+ });
98
+ if (!response.ok || !response.body) {
99
+ onError(new Error(`Stream request failed: ${response.status}`));
100
+ return;
101
+ }
102
+ const scanForObjects = createObjectScanner(frame => {
103
+ const content = extractContent(frame);
104
+ if (content !== null) {
105
+ onChunk(content);
106
+ }
107
+ });
108
+ const reader = response.body.getReader();
109
+ const decoder = new TextDecoder();
110
+ for (;;) {
111
+ const _await$reader$read = await reader.read(),
112
+ done = _await$reader$read.done,
113
+ value = _await$reader$read.value;
114
+ if (done) {
115
+ break;
116
+ }
117
+ scanForObjects(decoder.decode(value, {
118
+ stream: true
119
+ }));
120
+ }
121
+ scanForObjects(decoder.decode());
122
+ onDone();
123
+ } catch (err) {
124
+ if (err.name !== 'AbortError') {
125
+ onError(err);
126
+ }
127
+ }
128
+ }
129
+ //# sourceMappingURL=streamChatMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["extractContent","frame","parsed","JSON","parse","content","body","decoded","TextDecoder","decode","Uint8Array","from","atob","char","charCodeAt","inner","createObjectScanner","onObject","buffer","cursor","braceDepth","objectStart","inString","escaped","feed","text","length","slice","streamChatMessage","params","callbacks","signal","url","chatId","message","namespace","chatConfig","attachments","headers","onChunk","onDone","onError","response","fetch","method","credentials","stringify","ok","Error","status","scanForObjects","reader","getReader","decoder","_await$reader$read","read","done","value","stream","err","name"],"sources":["../../../src/services/streamChatMessage.ts"],"sourcesContent":["import type { ChatMessageAttachment } from '../types';\n\nexport type StreamCallbacks = {\n onChunk: (token: string) => void;\n onDone: () => void;\n onError: (err: Error) => void;\n};\n\nexport type StreamChatMessageParams = {\n // The caller supplies the endpoint (e.g. a proxied SSE route URL). The SDK\n // stays agnostic about base paths and gateway passthroughs.\n //\n // SECURITY: the request is sent with `credentials: 'include'`, so the session\n // cookie is delivered to whatever origin this resolves to. It must come from\n // trusted host/config — never from user input.\n url: string;\n chatId: string;\n message: string;\n namespace: string;\n chatConfig?: object;\n attachments?: ChatMessageAttachment[];\n // Extra request headers merged onto the POST. Lets the caller satisfy the\n // host's transport requirements the SDK can't know about — e.g. an\n // `x-xsrf-token` when the endpoint sits behind a CSRF-protected gateway.\n headers?: Record<string, string>;\n};\n\n// Pull the assistant text out of one streamed frame. Two framings reach us: a\n// proxy route yields `{ content }` objects, while the gateway wraps each frame\n// as `{ body: base64(json) }`. Returns null for frames without content.\nexport function extractContent(frame: string): string | null {\n try {\n const parsed = JSON.parse(frame);\n if (typeof parsed.content === 'string') {\n return parsed.content;\n }\n if (typeof parsed.body === 'string') {\n const decoded = new TextDecoder().decode(\n Uint8Array.from(atob(parsed.body), (char) => char.charCodeAt(0)),\n );\n const inner = JSON.parse(decoded);\n return typeof inner.content === 'string' ? inner.content : null;\n }\n } catch {\n // Incomplete or non-object frame — skip it.\n }\n return null;\n}\n\n// Feeds decoded text through a running scanner that emits each complete\n// top-level `{…}` object as its closing brace arrives. This keeps a message\n// that's split across reads intact, and parses a newline-free JSON array (the\n// gateway framing) incrementally. Anything outside a top-level object is ignored.\nexport function createObjectScanner(onObject: (frame: string) => void) {\n let buffer = '';\n let cursor = 0;\n let braceDepth = 0;\n let objectStart = -1;\n let inString = false;\n let escaped = false;\n\n return function feed(text: string) {\n buffer += text;\n for (; cursor < buffer.length; cursor++) {\n const char = buffer[cursor];\n if (inString) {\n if (escaped) {\n escaped = false;\n } else if (char === '\\\\') {\n escaped = true;\n } else if (char === '\"') {\n inString = false;\n }\n continue;\n }\n if (char === '\"') {\n inString = true;\n } else if (char === '{') {\n if (braceDepth === 0) {\n objectStart = cursor;\n }\n braceDepth++;\n } else if (char === '}' && braceDepth > 0) {\n braceDepth--;\n if (braceDepth === 0 && objectStart >= 0) {\n onObject(buffer.slice(objectStart, cursor + 1));\n objectStart = -1;\n }\n }\n }\n };\n}\n\n/**\n * Framework-agnostic streaming transport. POSTs the message to the\n * caller-supplied SSE endpoint, reads the response body incrementally, and\n * fires `onChunk` per assistant token, `onDone` on close, `onError` on failure.\n * A caller-supplied `AbortSignal` cancels the request; `AbortError` is swallowed.\n */\nexport async function streamChatMessage(\n params: StreamChatMessageParams,\n callbacks: StreamCallbacks,\n signal?: AbortSignal,\n): Promise<void> {\n const { url, chatId, message, namespace, chatConfig, attachments, headers } =\n params;\n const { onChunk, onDone, onError } = callbacks;\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...headers },\n body: JSON.stringify({\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n }),\n signal,\n });\n\n if (!response.ok || !response.body) {\n onError(new Error(`Stream request failed: ${response.status}`));\n return;\n }\n\n const scanForObjects = createObjectScanner((frame) => {\n const content = extractContent(frame);\n if (content !== null) {\n onChunk(content);\n }\n });\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n scanForObjects(decoder.decode(value, { stream: true }));\n }\n scanForObjects(decoder.decode());\n onDone();\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n onError(err as Error);\n }\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,2 +1,3 @@
1
1
  export * from './useTextChat';
2
+ export * from './useTextChatStream';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextChat/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextChat/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { ChatMessage, ChatMessageAttachment } from '../../types';
2
+ export { streamChatMessage, type StreamCallbacks, type StreamChatMessageParams, } from '../../services/streamChatMessage';
3
+ /**
4
+ * A `ChatMessage` augmented with streaming metadata. The host renderer uses
5
+ * `streamStatus`/`streamingText` to drive its typewriter; confirmed
6
+ * (non-streamed) messages carry neither.
7
+ */
8
+ export type StreamingChatMessage = ChatMessage & {
9
+ streamingText?: string;
10
+ streamStatus?: 'streaming' | 'settled';
11
+ };
12
+ export interface UseTextChatStreamOptions {
13
+ streamUrl: string;
14
+ limit?: number;
15
+ revealIntervalMs?: number;
16
+ revealCharsPerTick?: number;
17
+ headers?: Record<string, string>;
18
+ onError?: (err: Error) => void;
19
+ }
20
+ export interface SendStreamMessageParams {
21
+ message: string;
22
+ chatConfig?: object;
23
+ attachments?: ChatMessageAttachment[];
24
+ }
25
+ /**
26
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
27
+ * and progressively reveals the reply, merging the in-flight and completed
28
+ * streamed exchanges with the confirmed history (oldest-first).
29
+ *
30
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
31
+ * loop — parity with the current unary behavior.
32
+ */
33
+ export declare function useTextChatStream(options: UseTextChatStreamOptions): {
34
+ status: "pending" | "error" | "success";
35
+ pending: never[];
36
+ messages: StreamingChatMessage[];
37
+ fetchNext: () => void;
38
+ refetch: (options?: import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aq | undefined) => Promise<import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aH<ChatMessage[], Error>>;
39
+ send: ({ message, chatConfig, attachments }: SendStreamMessageParams) => Promise<void>;
40
+ };
41
+ //# sourceMappingURL=useTextChatStream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTextChatStream.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextChat/useTextChatStream.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAMtE,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,GAC7B,MAAM,kCAAkC,CAAC;AAU1C;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAG5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC;AAgBD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;iDAmGlB,uBAAuB;EAkJvE"}
@@ -0,0 +1,25 @@
1
+ import type { ChatMessageAttachment } from '../types';
2
+ export type StreamCallbacks = {
3
+ onChunk: (token: string) => void;
4
+ onDone: () => void;
5
+ onError: (err: Error) => void;
6
+ };
7
+ export type StreamChatMessageParams = {
8
+ url: string;
9
+ chatId: string;
10
+ message: string;
11
+ namespace: string;
12
+ chatConfig?: object;
13
+ attachments?: ChatMessageAttachment[];
14
+ headers?: Record<string, string>;
15
+ };
16
+ export declare function extractContent(frame: string): string | null;
17
+ export declare function createObjectScanner(onObject: (frame: string) => void): (text: string) => void;
18
+ /**
19
+ * Framework-agnostic streaming transport. POSTs the message to the
20
+ * caller-supplied SSE endpoint, reads the response body incrementally, and
21
+ * fires `onChunk` per assistant token, `onDone` on close, `onError` on failure.
22
+ * A caller-supplied `AbortSignal` cancels the request; `AbortError` is swallowed.
23
+ */
24
+ export declare function streamChatMessage(params: StreamChatMessageParams, callbacks: StreamCallbacks, signal?: AbortSignal): Promise<void>;
25
+ //# sourceMappingURL=streamChatMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamChatMessage.d.ts","sourceRoot":"","sources":["../../../src/services/streamChatMessage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IAOpC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAItC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAKF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB3D;AAMD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,UAQxC,MAAM,UA8BlC;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,uBAAuB,EAC/B,SAAS,EAAE,eAAe,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAgDf"}
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.7.0",
2
+ "version": "1.8.0",
3
3
  "name": "@wix/legends-platform-sdk",
4
4
  "license": "MIT",
5
5
  "author": {
@@ -103,5 +103,5 @@
103
103
  "wallaby": {
104
104
  "autoDetect": true
105
105
  },
106
- "falconPackageHash": "65d6f955a3a805b2511326986d408969ce62ade38ddcda7deb8bcf9c"
106
+ "falconPackageHash": "8a061f964dc13796483f409e83598c703ed05cf2545ea59da3f54ddf"
107
107
  }