@wix/legends-platform-sdk 1.7.0 → 1.9.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,272 @@
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
+ /**
14
+ * A `ChatMessage` augmented with streaming metadata. The host renderer uses
15
+ * `streamStatus`/`streamingText` to drive its typewriter; confirmed
16
+ * (non-streamed) messages carry neither.
17
+ */
18
+
19
+ /**
20
+ * Identity for deduping optimistic streamed messages against confirmed ones.
21
+ *
22
+ * This is a *content* join: the streamed text must equal the server-persisted
23
+ * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-
24
+ * whitespace differences, but heavier server-side post-processing (markdown /
25
+ * entity normalization, an appended citation) would make the keys diverge and
26
+ * briefly surface both copies. A shared message id would be more robust, but
27
+ * the streamed frames carry only token deltas — no id — so content equality is
28
+ * the best join available without backend support.
29
+ */
30
+ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
31
+
32
+ /**
33
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
34
+ * and drips the accumulated reply into `streamingText` at a steady, time-paced
35
+ * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight
36
+ * and completed streamed exchanges with the confirmed history (oldest-first).
37
+ * The host renderer (e.g. dolly) animates the dripped prefix with its own
38
+ * typewriter; pacing the drip below the typer's per-char rate keeps the typer
39
+ * from building a backlog it would otherwise skip through (jumpy reveal). The
40
+ * exchange settles only once the drip has revealed everything AND the network
41
+ * stream has ended, which avoids an end-of-reply snap.
42
+ *
43
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
44
+ * loop — parity with the current unary behavior.
45
+ */
46
+ function useTextChatStream(options) {
47
+ const {
48
+ streamUrl,
49
+ limit = 50,
50
+ headers,
51
+ onError,
52
+ revealMsPerChar = 16
53
+ } = options;
54
+ const {
55
+ chatId,
56
+ namespace,
57
+ personaChatService
58
+ } = (0, _sdkConsumer.useSdkConsumer)();
59
+ const {
60
+ data: sdkMessages = [],
61
+ status,
62
+ fetchNextPage,
63
+ refetch
64
+ } = (0, _reactQuery.useInfiniteQuery)({
65
+ queryKey: ['chat-messages', chatId, namespace],
66
+ queryFn: async ({
67
+ pageParam
68
+ }) => {
69
+ if (!personaChatService) {
70
+ return {
71
+ messages: [],
72
+ nextCursor: undefined
73
+ };
74
+ }
75
+ return personaChatService.queryChatMessages({
76
+ chatId,
77
+ namespace,
78
+ limit,
79
+ cursor: pageParam
80
+ });
81
+ },
82
+ getNextPageParam: lastPage => lastPage.nextCursor,
83
+ initialPageParam: undefined,
84
+ select: data => data.pages.flatMap(page => page.messages)
85
+ });
86
+
87
+ // Local state for the in-progress streaming exchange (user + assistant messages).
88
+ const [streamingMessages, setStreamingMessages] = (0, _react.useState)([]);
89
+ // Exchanges that finished streaming, kept for the lifetime of this chat. They
90
+ // keep their stable optimistic ids and stand in for the SDK's confirmed copies
91
+ // (which are hidden as duplicates) — this both bridges the refetch lag and
92
+ // avoids remounting the message (and restarting its typewriter) when the
93
+ // confirmed copy, with a different id, arrives.
94
+ const [completedMessages, setCompletedMessages] = (0, _react.useState)([]);
95
+ const abortRef = (0, _react.useRef)(null);
96
+ // Handle of the pending reveal-drip animation frame, if any.
97
+ const rafRef = (0, _react.useRef)(null);
98
+ // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
99
+ // falling back to this keeps the history from disappearing and reappearing.
100
+ const lastConfirmedRef = (0, _react.useRef)([]);
101
+ // Monotonic per-hook counter so two sends within the same millisecond still
102
+ // produce distinct message ids (used as React keys downstream).
103
+ const sendSeqRef = (0, _react.useRef)(0);
104
+
105
+ // Cancel any pending reveal-drip frame.
106
+ const stopRaf = (0, _react.useCallback)(() => {
107
+ if (rafRef.current !== null) {
108
+ cancelAnimationFrame(rafRef.current);
109
+ rafRef.current = null;
110
+ }
111
+ }, []);
112
+
113
+ // Stop any in-flight stream (and reveal drip) on unmount.
114
+ (0, _react.useEffect)(() => {
115
+ return () => {
116
+ var _abortRef$current;
117
+ (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
118
+ stopRaf();
119
+ };
120
+ }, [stopRaf]);
121
+ const messages = (0, _react.useMemo)(() => {
122
+ const converted = [...sdkMessages].reverse();
123
+ const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
124
+
125
+ // Show our locally-tracked streamed exchanges (stable ids) and hide the
126
+ // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the
127
+ // message (different id) and restart its typewriter — that's the flicker.
128
+ // The in-progress streaming messages are always shown as-is.
129
+ const completedKeys = new Set(completedMessages.map(messageKey));
130
+ const confirmedShown = confirmedMessages.filter(m => !completedKeys.has(messageKey(m)));
131
+ return [...confirmedShown, ...completedMessages, ...streamingMessages];
132
+ }, [sdkMessages, streamingMessages, completedMessages]);
133
+
134
+ // Remember the latest non-empty confirmed list so a transient empty during a
135
+ // refetch doesn't blank the history.
136
+ (0, _react.useEffect)(() => {
137
+ const converted = [...sdkMessages].reverse();
138
+ if (converted.length > 0) {
139
+ lastConfirmedRef.current = converted;
140
+ }
141
+ }, [sdkMessages]);
142
+ const send = (0, _react.useCallback)(async ({
143
+ message,
144
+ chatConfig,
145
+ attachments
146
+ }) => {
147
+ var _abortRef$current2;
148
+ // Abort any in-flight stream before starting a new one.
149
+ (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
150
+ const controller = new AbortController();
151
+ abortRef.current = controller;
152
+ const seq = sendSeqRef.current += 1;
153
+ const userMsgId = `stream-user-${Date.now()}-${seq}`;
154
+ const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;
155
+ const optimisticUserMsg = {
156
+ id: userMsgId,
157
+ role: 'USER',
158
+ content: message,
159
+ attachments
160
+ };
161
+ const streamingAssistantMsg = {
162
+ id: assistantMsgId,
163
+ role: 'ASSISTANT',
164
+ content: '',
165
+ streamStatus: 'streaming',
166
+ streamingText: ''
167
+ };
168
+ setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
169
+
170
+ // Tokens accumulate as they arrive, but we reveal them into
171
+ // `streamingText` at a steady time-paced rate via requestAnimationFrame.
172
+ // Pacing the reveal below the host typer's per-char rate stops it from
173
+ // building a backlog it would skip through (the jumpy/"super fast" look).
174
+ let accumulated = ''; // full text received so far
175
+ let revealedLen = 0; // how many chars we've dripped into streamingText
176
+ let streamDone = false; // network stream ended
177
+ let carry = 0; // fractional chars carried between frames
178
+ let lastFrameTime = 0; // 0 = uninitialized
179
+
180
+ const settle = () => {
181
+ // Settle only after the drip has revealed everything AND the stream
182
+ // ended. Mirror the text into both `content` and `streamingText` so a
183
+ // host typer hands off with no re-type or flicker. Kept in
184
+ // `completedMessages` (stable id) so the confirmed copy from refetch is
185
+ // hidden and never remounts.
186
+ stopRaf();
187
+ const finalAssistantMsg = {
188
+ ...streamingAssistantMsg,
189
+ content: accumulated,
190
+ streamStatus: 'settled',
191
+ streamingText: accumulated
192
+ };
193
+ setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
194
+ setStreamingMessages([]);
195
+ void refetch();
196
+ };
197
+ const frame = now => {
198
+ if (lastFrameTime === 0) {
199
+ lastFrameTime = now;
200
+ }
201
+ const elapsedMs = now - lastFrameTime;
202
+ lastFrameTime = now;
203
+ if (revealedLen < accumulated.length) {
204
+ carry += elapsedMs / revealMsPerChar;
205
+ const step = Math.floor(carry);
206
+ if (step > 0) {
207
+ carry -= step;
208
+ revealedLen = Math.min(accumulated.length, revealedLen + step);
209
+ setStreamingMessages([optimisticUserMsg, {
210
+ ...streamingAssistantMsg,
211
+ streamStatus: 'streaming',
212
+ streamingText: accumulated.slice(0, revealedLen)
213
+ }]);
214
+ }
215
+ rafRef.current = requestAnimationFrame(frame);
216
+ } else if (streamDone) {
217
+ settle();
218
+ } else {
219
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
220
+ stopRaf();
221
+ }
222
+ };
223
+ const startRaf = () => {
224
+ if (rafRef.current === null) {
225
+ lastFrameTime = 0;
226
+ rafRef.current = requestAnimationFrame(frame);
227
+ }
228
+ };
229
+
230
+ // Streaming deliberately bypasses the injected `personaChatService` (the
231
+ // DI seam every other hook uses): SSE needs a raw fetch with cookie auth
232
+ // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't
233
+ // model. History reads still go through the service (see the query above).
234
+ await (0, _streamChatMessage.streamChatMessage)({
235
+ url: streamUrl,
236
+ chatId,
237
+ message,
238
+ namespace,
239
+ chatConfig,
240
+ attachments,
241
+ headers
242
+ }, {
243
+ onChunk: token => {
244
+ accumulated += token;
245
+ startRaf();
246
+ },
247
+ onDone: () => {
248
+ streamDone = true;
249
+ startRaf();
250
+ },
251
+ onError: err => {
252
+ stopRaf();
253
+ onError == null || onError(err);
254
+ setStreamingMessages([]);
255
+ }
256
+ }, controller.signal);
257
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
258
+ return {
259
+ status,
260
+ // Present for symmetry with `useTextChat`; the streaming path tracks its own
261
+ // optimistic state, so this is always empty.
262
+ pending: [],
263
+ messages,
264
+ fetchNext,
265
+ refetch,
266
+ send
267
+ };
268
+ function fetchNext() {
269
+ void fetchNextPage();
270
+ }
271
+ }
272
+ //# sourceMappingURL=useTextChatStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactQuery","_sdkConsumer","_streamChatMessage","exports","streamChatMessage","messageKey","m","role","content","trim","useTextChatStream","options","streamUrl","limit","headers","onError","revealMsPerChar","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","rafRef","lastConfirmedRef","sendSeqRef","stopRaf","useCallback","current","cancelAnimationFrame","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","carry","lastFrameTime","settle","finalAssistantMsg","prev","frame","elapsedMs","step","Math","floor","min","slice","requestAnimationFrame","startRaf","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/**\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 // 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 // Milliseconds per character for the requestAnimationFrame reveal drip.\n // Default 16 (~62 chars/s) — comfortably slower than the chat UI typer's\n // ~8ms/char, so the typer never builds a backlog it would skip through.\n revealMsPerChar?: number;\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 drips the accumulated reply into `streamingText` at a steady, time-paced\n * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight\n * and completed streamed exchanges with the confirmed history (oldest-first).\n * The host renderer (e.g. dolly) animates the dripped prefix with its own\n * typewriter; pacing the drip below the typer's per-char rate keeps the typer\n * from building a backlog it would otherwise skip through (jumpy reveal). The\n * exchange settles only once the drip has revealed everything AND the network\n * stream has ended, which avoids an end-of-reply snap.\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 headers,\n onError,\n revealMsPerChar = 16,\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 // Handle of the pending reveal-drip animation frame, if any.\n const rafRef = useRef<number | 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 // Cancel any pending reveal-drip frame.\n const stopRaf = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n }, []);\n\n // Stop any in-flight stream (and reveal drip) on unmount.\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n stopRaf();\n };\n }, [stopRaf]);\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 before starting a new one.\n abortRef.current?.abort();\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 accumulate as they arrive, but we reveal them into\n // `streamingText` at a steady time-paced rate via requestAnimationFrame.\n // Pacing the reveal below the host typer's per-char rate stops it from\n // building a backlog it would skip through (the jumpy/\"super fast\" look).\n let accumulated = ''; // full text received so far\n let revealedLen = 0; // how many chars we've dripped into streamingText\n let streamDone = false; // network stream ended\n let carry = 0; // fractional chars carried between frames\n let lastFrameTime = 0; // 0 = uninitialized\n\n const settle = () => {\n // Settle only after the drip has revealed everything AND the stream\n // ended. Mirror the text into both `content` and `streamingText` so a\n // host typer hands off 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 stopRaf();\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 void refetch();\n };\n\n const frame = (now: number) => {\n if (lastFrameTime === 0) {\n lastFrameTime = now;\n }\n const elapsedMs = now - lastFrameTime;\n lastFrameTime = now;\n\n if (revealedLen < accumulated.length) {\n carry += elapsedMs / revealMsPerChar;\n const step = Math.floor(carry);\n if (step > 0) {\n carry -= step;\n revealedLen = Math.min(accumulated.length, revealedLen + step);\n setStreamingMessages([\n optimisticUserMsg,\n {\n ...streamingAssistantMsg,\n streamStatus: 'streaming',\n streamingText: accumulated.slice(0, revealedLen),\n },\n ]);\n }\n rafRef.current = requestAnimationFrame(frame);\n } else if (streamDone) {\n settle();\n } else {\n // Caught up, waiting for more tokens; onChunk will restart the loop.\n stopRaf();\n }\n };\n\n const startRaf = () => {\n if (rafRef.current === null) {\n lastFrameTime = 0;\n rafRef.current = requestAnimationFrame(frame);\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 startRaf();\n },\n onDone: () => {\n streamDone = true;\n startRaf();\n },\n onError: (err) => {\n stopRaf();\n onError?.(err);\n setStreamingMessages([]);\n },\n },\n controller.signal,\n );\n },\n [\n streamUrl,\n chatId,\n namespace,\n headers,\n onError,\n refetch,\n revealMsPerChar,\n stopRaf,\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,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,270 @@
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
+ /**
11
+ * A `ChatMessage` augmented with streaming metadata. The host renderer uses
12
+ * `streamStatus`/`streamingText` to drive its typewriter; confirmed
13
+ * (non-streamed) messages carry neither.
14
+ */
15
+
16
+ /**
17
+ * Identity for deduping optimistic streamed messages against confirmed ones.
18
+ *
19
+ * This is a *content* join: the streamed text must equal the server-persisted
20
+ * copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-
21
+ * whitespace differences, but heavier server-side post-processing (markdown /
22
+ * entity normalization, an appended citation) would make the keys diverge and
23
+ * briefly surface both copies. A shared message id would be more robust, but
24
+ * the streamed frames carry only token deltas — no id — so content equality is
25
+ * the best join available without backend support.
26
+ */
27
+ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
28
+
29
+ /**
30
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
31
+ * and drips the accumulated reply into `streamingText` at a steady, time-paced
32
+ * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight
33
+ * and completed streamed exchanges with the confirmed history (oldest-first).
34
+ * The host renderer (e.g. dolly) animates the dripped prefix with its own
35
+ * typewriter; pacing the drip below the typer's per-char rate keeps the typer
36
+ * from building a backlog it would otherwise skip through (jumpy reveal). The
37
+ * exchange settles only once the drip has revealed everything AND the network
38
+ * stream has ended, which avoids an end-of-reply snap.
39
+ *
40
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
41
+ * loop — parity with the current unary behavior.
42
+ */
43
+ export function useTextChatStream(options) {
44
+ const streamUrl = options.streamUrl,
45
+ _options$limit = options.limit,
46
+ limit = _options$limit === void 0 ? 50 : _options$limit,
47
+ headers = options.headers,
48
+ onError = options.onError,
49
+ _options$revealMsPerC = options.revealMsPerChar,
50
+ revealMsPerChar = _options$revealMsPerC === void 0 ? 16 : _options$revealMsPerC;
51
+ const _useSdkConsumer = useSdkConsumer(),
52
+ chatId = _useSdkConsumer.chatId,
53
+ namespace = _useSdkConsumer.namespace,
54
+ personaChatService = _useSdkConsumer.personaChatService;
55
+ const _useInfiniteQuery = useInfiniteQuery({
56
+ queryKey: ['chat-messages', chatId, namespace],
57
+ queryFn: async _ref => {
58
+ let pageParam = _ref.pageParam;
59
+ if (!personaChatService) {
60
+ return {
61
+ messages: [],
62
+ nextCursor: undefined
63
+ };
64
+ }
65
+ return personaChatService.queryChatMessages({
66
+ chatId,
67
+ namespace,
68
+ limit,
69
+ cursor: pageParam
70
+ });
71
+ },
72
+ getNextPageParam: lastPage => lastPage.nextCursor,
73
+ initialPageParam: undefined,
74
+ select: data => data.pages.flatMap(page => page.messages)
75
+ }),
76
+ _useInfiniteQuery$dat = _useInfiniteQuery.data,
77
+ sdkMessages = _useInfiniteQuery$dat === void 0 ? [] : _useInfiniteQuery$dat,
78
+ status = _useInfiniteQuery.status,
79
+ fetchNextPage = _useInfiniteQuery.fetchNextPage,
80
+ refetch = _useInfiniteQuery.refetch;
81
+
82
+ // Local state for the in-progress streaming exchange (user + assistant messages).
83
+ const _useState = useState([]),
84
+ streamingMessages = _useState[0],
85
+ setStreamingMessages = _useState[1];
86
+ // Exchanges that finished streaming, kept for the lifetime of this chat. They
87
+ // keep their stable optimistic ids and stand in for the SDK's confirmed copies
88
+ // (which are hidden as duplicates) — this both bridges the refetch lag and
89
+ // avoids remounting the message (and restarting its typewriter) when the
90
+ // confirmed copy, with a different id, arrives.
91
+ const _useState2 = useState([]),
92
+ completedMessages = _useState2[0],
93
+ setCompletedMessages = _useState2[1];
94
+ const abortRef = useRef(null);
95
+ // Handle of the pending reveal-drip animation frame, if any.
96
+ const rafRef = useRef(null);
97
+ // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
98
+ // falling back to this keeps the history from disappearing and reappearing.
99
+ const lastConfirmedRef = useRef([]);
100
+ // Monotonic per-hook counter so two sends within the same millisecond still
101
+ // produce distinct message ids (used as React keys downstream).
102
+ const sendSeqRef = useRef(0);
103
+
104
+ // Cancel any pending reveal-drip frame.
105
+ const stopRaf = useCallback(() => {
106
+ if (rafRef.current !== null) {
107
+ cancelAnimationFrame(rafRef.current);
108
+ rafRef.current = null;
109
+ }
110
+ }, []);
111
+
112
+ // Stop any in-flight stream (and reveal drip) on unmount.
113
+ useEffect(() => {
114
+ return () => {
115
+ var _abortRef$current;
116
+ (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
117
+ stopRaf();
118
+ };
119
+ }, [stopRaf]);
120
+ const messages = useMemo(() => {
121
+ const converted = [...sdkMessages].reverse();
122
+ const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
123
+
124
+ // Show our locally-tracked streamed exchanges (stable ids) and hide the
125
+ // SDK's confirmed duplicates. Swapping to a confirmed copy would remount the
126
+ // message (different id) and restart its typewriter — that's the flicker.
127
+ // The in-progress streaming messages are always shown as-is.
128
+ const completedKeys = new Set(completedMessages.map(messageKey));
129
+ const confirmedShown = confirmedMessages.filter(m => !completedKeys.has(messageKey(m)));
130
+ return [...confirmedShown, ...completedMessages, ...streamingMessages];
131
+ }, [sdkMessages, streamingMessages, completedMessages]);
132
+
133
+ // Remember the latest non-empty confirmed list so a transient empty during a
134
+ // refetch doesn't blank the history.
135
+ useEffect(() => {
136
+ const converted = [...sdkMessages].reverse();
137
+ if (converted.length > 0) {
138
+ lastConfirmedRef.current = converted;
139
+ }
140
+ }, [sdkMessages]);
141
+ const send = useCallback(async _ref2 => {
142
+ var _abortRef$current2;
143
+ let message = _ref2.message,
144
+ chatConfig = _ref2.chatConfig,
145
+ attachments = _ref2.attachments;
146
+ // Abort any in-flight stream before starting a new one.
147
+ (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
148
+ const controller = new AbortController();
149
+ abortRef.current = controller;
150
+ const seq = sendSeqRef.current += 1;
151
+ const userMsgId = `stream-user-${Date.now()}-${seq}`;
152
+ const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;
153
+ const optimisticUserMsg = {
154
+ id: userMsgId,
155
+ role: 'USER',
156
+ content: message,
157
+ attachments
158
+ };
159
+ const streamingAssistantMsg = {
160
+ id: assistantMsgId,
161
+ role: 'ASSISTANT',
162
+ content: '',
163
+ streamStatus: 'streaming',
164
+ streamingText: ''
165
+ };
166
+ setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
167
+
168
+ // Tokens accumulate as they arrive, but we reveal them into
169
+ // `streamingText` at a steady time-paced rate via requestAnimationFrame.
170
+ // Pacing the reveal below the host typer's per-char rate stops it from
171
+ // building a backlog it would skip through (the jumpy/"super fast" look).
172
+ let accumulated = ''; // full text received so far
173
+ let revealedLen = 0; // how many chars we've dripped into streamingText
174
+ let streamDone = false; // network stream ended
175
+ let carry = 0; // fractional chars carried between frames
176
+ let lastFrameTime = 0; // 0 = uninitialized
177
+
178
+ const settle = () => {
179
+ // Settle only after the drip has revealed everything AND the stream
180
+ // ended. Mirror the text into both `content` and `streamingText` so a
181
+ // host typer hands off 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
+ stopRaf();
185
+ const finalAssistantMsg = {
186
+ ...streamingAssistantMsg,
187
+ content: accumulated,
188
+ streamStatus: 'settled',
189
+ streamingText: accumulated
190
+ };
191
+ setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
192
+ setStreamingMessages([]);
193
+ void refetch();
194
+ };
195
+ const frame = now => {
196
+ if (lastFrameTime === 0) {
197
+ lastFrameTime = now;
198
+ }
199
+ const elapsedMs = now - lastFrameTime;
200
+ lastFrameTime = now;
201
+ if (revealedLen < accumulated.length) {
202
+ carry += elapsedMs / revealMsPerChar;
203
+ const step = Math.floor(carry);
204
+ if (step > 0) {
205
+ carry -= step;
206
+ revealedLen = Math.min(accumulated.length, revealedLen + step);
207
+ setStreamingMessages([optimisticUserMsg, {
208
+ ...streamingAssistantMsg,
209
+ streamStatus: 'streaming',
210
+ streamingText: accumulated.slice(0, revealedLen)
211
+ }]);
212
+ }
213
+ rafRef.current = requestAnimationFrame(frame);
214
+ } else if (streamDone) {
215
+ settle();
216
+ } else {
217
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
218
+ stopRaf();
219
+ }
220
+ };
221
+ const startRaf = () => {
222
+ if (rafRef.current === null) {
223
+ lastFrameTime = 0;
224
+ rafRef.current = requestAnimationFrame(frame);
225
+ }
226
+ };
227
+
228
+ // Streaming deliberately bypasses the injected `personaChatService` (the
229
+ // DI seam every other hook uses): SSE needs a raw fetch with cookie auth
230
+ // to a caller-supplied url, which the Api-Key `IPersonaChatService` can't
231
+ // model. History reads still go through the service (see the query above).
232
+ await streamChatMessage({
233
+ url: streamUrl,
234
+ chatId,
235
+ message,
236
+ namespace,
237
+ chatConfig,
238
+ attachments,
239
+ headers
240
+ }, {
241
+ onChunk: token => {
242
+ accumulated += token;
243
+ startRaf();
244
+ },
245
+ onDone: () => {
246
+ streamDone = true;
247
+ startRaf();
248
+ },
249
+ onError: err => {
250
+ stopRaf();
251
+ onError == null || onError(err);
252
+ setStreamingMessages([]);
253
+ }
254
+ }, controller.signal);
255
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
256
+ return {
257
+ status,
258
+ // Present for symmetry with `useTextChat`; the streaming path tracks its own
259
+ // optimistic state, so this is always empty.
260
+ pending: [],
261
+ messages,
262
+ fetchNext,
263
+ refetch,
264
+ send
265
+ };
266
+ function fetchNext() {
267
+ void fetchNextPage();
268
+ }
269
+ }
270
+ //# sourceMappingURL=useTextChatStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useCallback","useEffect","useMemo","useRef","useState","useInfiniteQuery","useSdkConsumer","streamChatMessage","messageKey","m","role","content","trim","useTextChatStream","options","streamUrl","_options$limit","limit","headers","onError","_options$revealMsPerC","revealMsPerChar","_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","rafRef","lastConfirmedRef","sendSeqRef","stopRaf","current","cancelAnimationFrame","_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","carry","lastFrameTime","settle","finalAssistantMsg","prev","frame","elapsedMs","step","Math","floor","min","slice","requestAnimationFrame","startRaf","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/**\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 // 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 // Milliseconds per character for the requestAnimationFrame reveal drip.\n // Default 16 (~62 chars/s) — comfortably slower than the chat UI typer's\n // ~8ms/char, so the typer never builds a backlog it would skip through.\n revealMsPerChar?: number;\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 drips the accumulated reply into `streamingText` at a steady, time-paced\n * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight\n * and completed streamed exchanges with the confirmed history (oldest-first).\n * The host renderer (e.g. dolly) animates the dripped prefix with its own\n * typewriter; pacing the drip below the typer's per-char rate keeps the typer\n * from building a backlog it would otherwise skip through (jumpy reveal). The\n * exchange settles only once the drip has revealed everything AND the network\n * stream has ended, which avoids an end-of-reply snap.\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 headers,\n onError,\n revealMsPerChar = 16,\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 // Handle of the pending reveal-drip animation frame, if any.\n const rafRef = useRef<number | 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 // Cancel any pending reveal-drip frame.\n const stopRaf = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n }, []);\n\n // Stop any in-flight stream (and reveal drip) on unmount.\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n stopRaf();\n };\n }, [stopRaf]);\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 before starting a new one.\n abortRef.current?.abort();\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 accumulate as they arrive, but we reveal them into\n // `streamingText` at a steady time-paced rate via requestAnimationFrame.\n // Pacing the reveal below the host typer's per-char rate stops it from\n // building a backlog it would skip through (the jumpy/\"super fast\" look).\n let accumulated = ''; // full text received so far\n let revealedLen = 0; // how many chars we've dripped into streamingText\n let streamDone = false; // network stream ended\n let carry = 0; // fractional chars carried between frames\n let lastFrameTime = 0; // 0 = uninitialized\n\n const settle = () => {\n // Settle only after the drip has revealed everything AND the stream\n // ended. Mirror the text into both `content` and `streamingText` so a\n // host typer hands off 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 stopRaf();\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 void refetch();\n };\n\n const frame = (now: number) => {\n if (lastFrameTime === 0) {\n lastFrameTime = now;\n }\n const elapsedMs = now - lastFrameTime;\n lastFrameTime = now;\n\n if (revealedLen < accumulated.length) {\n carry += elapsedMs / revealMsPerChar;\n const step = Math.floor(carry);\n if (step > 0) {\n carry -= step;\n revealedLen = Math.min(accumulated.length, revealedLen + step);\n setStreamingMessages([\n optimisticUserMsg,\n {\n ...streamingAssistantMsg,\n streamStatus: 'streaming',\n streamingText: accumulated.slice(0, revealedLen),\n },\n ]);\n }\n rafRef.current = requestAnimationFrame(frame);\n } else if (streamDone) {\n settle();\n } else {\n // Caught up, waiting for more tokens; onChunk will restart the loop.\n stopRaf();\n }\n };\n\n const startRaf = () => {\n if (rafRef.current === null) {\n lastFrameTime = 0;\n rafRef.current = requestAnimationFrame(frame);\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 startRaf();\n },\n onDone: () => {\n streamDone = true;\n startRaf();\n },\n onError: (err) => {\n stopRaf();\n onError?.(err);\n setStreamingMessages([]);\n },\n },\n controller.signal,\n );\n },\n [\n streamUrl,\n chatId,\n namespace,\n headers,\n onError,\n refetch,\n revealMsPerChar,\n stopRaf,\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,46 @@
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
+ headers?: Record<string, string>;
16
+ onError?: (err: Error) => void;
17
+ revealMsPerChar?: number;
18
+ }
19
+ export interface SendStreamMessageParams {
20
+ message: string;
21
+ chatConfig?: object;
22
+ attachments?: ChatMessageAttachment[];
23
+ }
24
+ /**
25
+ * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
26
+ * and drips the accumulated reply into `streamingText` at a steady, time-paced
27
+ * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight
28
+ * and completed streamed exchanges with the confirmed history (oldest-first).
29
+ * The host renderer (e.g. dolly) animates the dripped prefix with its own
30
+ * typewriter; pacing the drip below the typer's per-char rate keeps the typer
31
+ * from building a backlog it would otherwise skip through (jumpy reveal). The
32
+ * exchange settles only once the drip has revealed everything AND the network
33
+ * stream has ended, which avoids an end-of-reply snap.
34
+ *
35
+ * KNOWN LIMITATION: the streaming path does not run a function-call resolution
36
+ * loop — parity with the current unary behavior.
37
+ */
38
+ export declare function useTextChatStream(options: UseTextChatStreamOptions): {
39
+ status: "pending" | "error" | "success";
40
+ pending: never[];
41
+ messages: StreamingChatMessage[];
42
+ fetchNext: () => void;
43
+ refetch: (options?: import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aq | undefined) => Promise<import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aH<ChatMessage[], Error>>;
44
+ send: ({ message, chatConfig, attachments }: SendStreamMessageParams) => Promise<void>;
45
+ };
46
+ //# 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;AAE1C;;;;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;IAGf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAI/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC;AAgBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;iDAmGlB,uBAAuB;EA2JvE"}
@@ -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.9.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": "e93bb4f738434f45ce730b0465ade783249af8d03bd939de0c73a284"
107
107
  }