@wix/legends-platform-sdk 1.8.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.
@@ -10,14 +10,6 @@ exports.streamChatMessage = _streamChatMessage.streamChatMessage;
10
10
  // Re-exported here so consumers can reach the framework-agnostic transport
11
11
  // alongside the hook.
12
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
13
  /**
22
14
  * A `ChatMessage` augmented with streaming metadata. The host renderer uses
23
15
  * `streamStatus`/`streamingText` to drive its typewriter; confirmed
@@ -39,8 +31,14 @@ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
39
31
 
40
32
  /**
41
33
  * 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).
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.
44
42
  *
45
43
  * KNOWN LIMITATION: the streaming path does not run a function-call resolution
46
44
  * loop — parity with the current unary behavior.
@@ -49,10 +47,9 @@ function useTextChatStream(options) {
49
47
  const {
50
48
  streamUrl,
51
49
  limit = 50,
52
- revealIntervalMs = REVEAL_INTERVAL_MS,
53
- revealCharsPerTick = REVEAL_CHARS_PER_TICK,
54
50
  headers,
55
- onError
51
+ onError,
52
+ revealMsPerChar = 16
56
53
  } = options;
57
54
  const {
58
55
  chatId,
@@ -96,29 +93,31 @@ function useTextChatStream(options) {
96
93
  // confirmed copy, with a different id, arrives.
97
94
  const [completedMessages, setCompletedMessages] = (0, _react.useState)([]);
98
95
  const abortRef = (0, _react.useRef)(null);
99
- // Interval that drips the buffered reply into the streaming message.
100
- const revealTimerRef = (0, _react.useRef)(null);
96
+ // Handle of the pending reveal-drip animation frame, if any.
97
+ const rafRef = (0, _react.useRef)(null);
101
98
  // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
102
99
  // falling back to this keeps the history from disappearing and reappearing.
103
100
  const lastConfirmedRef = (0, _react.useRef)([]);
104
101
  // Monotonic per-hook counter so two sends within the same millisecond still
105
102
  // produce distinct message ids (used as React keys downstream).
106
103
  const sendSeqRef = (0, _react.useRef)(0);
107
- const stopRevealTimer = (0, _react.useCallback)(() => {
108
- if (revealTimerRef.current) {
109
- clearInterval(revealTimerRef.current);
110
- revealTimerRef.current = null;
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;
111
110
  }
112
111
  }, []);
113
112
 
114
- // Stop any in-flight stream + reveal on unmount.
113
+ // Stop any in-flight stream (and reveal drip) on unmount.
115
114
  (0, _react.useEffect)(() => {
116
115
  return () => {
117
116
  var _abortRef$current;
118
117
  (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
119
- stopRevealTimer();
118
+ stopRaf();
120
119
  };
121
- }, [stopRevealTimer]);
120
+ }, [stopRaf]);
122
121
  const messages = (0, _react.useMemo)(() => {
123
122
  const converted = [...sdkMessages].reverse();
124
123
  const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
@@ -146,9 +145,8 @@ function useTextChatStream(options) {
146
145
  attachments
147
146
  }) => {
148
147
  var _abortRef$current2;
149
- // Abort any in-flight stream + reveal before starting a new one.
148
+ // Abort any in-flight stream before starting a new one.
150
149
  (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
151
- stopRevealTimer();
152
150
  const controller = new AbortController();
153
151
  abortRef.current = controller;
154
152
  const seq = sendSeqRef.current += 1;
@@ -169,18 +167,23 @@ function useTextChatStream(options) {
169
167
  };
170
168
  setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
171
169
 
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;
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
+
177
180
  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
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
182
184
  // `completedMessages` (stable id) so the confirmed copy from refetch is
183
185
  // hidden and never remounts.
186
+ stopRaf();
184
187
  const finalAssistantMsg = {
185
188
  ...streamingAssistantMsg,
186
189
  content: accumulated,
@@ -189,28 +192,38 @@ function useTextChatStream(options) {
189
192
  };
190
193
  setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
191
194
  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
195
  void refetch();
195
196
  };
196
- const tick = () => {
197
+ const frame = now => {
198
+ if (lastFrameTime === 0) {
199
+ lastFrameTime = now;
200
+ }
201
+ const elapsedMs = now - lastFrameTime;
202
+ lastFrameTime = now;
197
203
  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
- }]);
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);
206
216
  } else if (streamDone) {
207
- // Network finished and the reveal has caught up — settle.
208
217
  settle();
218
+ } else {
219
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
220
+ stopRaf();
209
221
  }
210
222
  };
211
- const startReveal = () => {
212
- if (!revealTimerRef.current) {
213
- revealTimerRef.current = setInterval(tick, revealIntervalMs);
223
+ const startRaf = () => {
224
+ if (rafRef.current === null) {
225
+ lastFrameTime = 0;
226
+ rafRef.current = requestAnimationFrame(frame);
214
227
  }
215
228
  };
216
229
 
@@ -229,20 +242,19 @@ function useTextChatStream(options) {
229
242
  }, {
230
243
  onChunk: token => {
231
244
  accumulated += token;
232
- startReveal();
245
+ startRaf();
233
246
  },
234
247
  onDone: () => {
235
- // Let the drip finish revealing, then settle (see `tick`).
236
248
  streamDone = true;
237
- startReveal();
249
+ startRaf();
238
250
  },
239
251
  onError: err => {
240
- stopRevealTimer();
252
+ stopRaf();
241
253
  onError == null || onError(err);
242
254
  setStreamingMessages([]);
243
255
  }
244
256
  }, controller.signal);
245
- }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
257
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
246
258
  return {
247
259
  status,
248
260
  // Present for symmetry with `useTextChat`; the streaming path tracks its own
@@ -1 +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":"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","ignoreList":[]}
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":[]}
@@ -7,14 +7,6 @@ import { streamChatMessage } from '../../services/streamChatMessage';
7
7
  // alongside the hook.
8
8
  export { streamChatMessage } from '../../services/streamChatMessage';
9
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
10
  /**
19
11
  * A `ChatMessage` augmented with streaming metadata. The host renderer uses
20
12
  * `streamStatus`/`streamingText` to drive its typewriter; confirmed
@@ -36,8 +28,14 @@ const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
36
28
 
37
29
  /**
38
30
  * 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).
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.
41
39
  *
42
40
  * KNOWN LIMITATION: the streaming path does not run a function-call resolution
43
41
  * loop — parity with the current unary behavior.
@@ -46,12 +44,10 @@ export function useTextChatStream(options) {
46
44
  const streamUrl = options.streamUrl,
47
45
  _options$limit = options.limit,
48
46
  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
47
  headers = options.headers,
54
- onError = options.onError;
48
+ onError = options.onError,
49
+ _options$revealMsPerC = options.revealMsPerChar,
50
+ revealMsPerChar = _options$revealMsPerC === void 0 ? 16 : _options$revealMsPerC;
55
51
  const _useSdkConsumer = useSdkConsumer(),
56
52
  chatId = _useSdkConsumer.chatId,
57
53
  namespace = _useSdkConsumer.namespace,
@@ -96,29 +92,31 @@ export function useTextChatStream(options) {
96
92
  completedMessages = _useState2[0],
97
93
  setCompletedMessages = _useState2[1];
98
94
  const abortRef = useRef(null);
99
- // Interval that drips the buffered reply into the streaming message.
100
- const revealTimerRef = useRef(null);
95
+ // Handle of the pending reveal-drip animation frame, if any.
96
+ const rafRef = useRef(null);
101
97
  // Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
102
98
  // falling back to this keeps the history from disappearing and reappearing.
103
99
  const lastConfirmedRef = useRef([]);
104
100
  // Monotonic per-hook counter so two sends within the same millisecond still
105
101
  // produce distinct message ids (used as React keys downstream).
106
102
  const sendSeqRef = useRef(0);
107
- const stopRevealTimer = useCallback(() => {
108
- if (revealTimerRef.current) {
109
- clearInterval(revealTimerRef.current);
110
- revealTimerRef.current = null;
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;
111
109
  }
112
110
  }, []);
113
111
 
114
- // Stop any in-flight stream + reveal on unmount.
112
+ // Stop any in-flight stream (and reveal drip) on unmount.
115
113
  useEffect(() => {
116
114
  return () => {
117
115
  var _abortRef$current;
118
116
  (_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
119
- stopRevealTimer();
117
+ stopRaf();
120
118
  };
121
- }, [stopRevealTimer]);
119
+ }, [stopRaf]);
122
120
  const messages = useMemo(() => {
123
121
  const converted = [...sdkMessages].reverse();
124
122
  const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
@@ -145,9 +143,8 @@ export function useTextChatStream(options) {
145
143
  let message = _ref2.message,
146
144
  chatConfig = _ref2.chatConfig,
147
145
  attachments = _ref2.attachments;
148
- // Abort any in-flight stream + reveal before starting a new one.
146
+ // Abort any in-flight stream before starting a new one.
149
147
  (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
150
- stopRevealTimer();
151
148
  const controller = new AbortController();
152
149
  abortRef.current = controller;
153
150
  const seq = sendSeqRef.current += 1;
@@ -168,18 +165,23 @@ export function useTextChatStream(options) {
168
165
  };
169
166
  setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
170
167
 
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;
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
+
176
178
  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
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
181
182
  // `completedMessages` (stable id) so the confirmed copy from refetch is
182
183
  // hidden and never remounts.
184
+ stopRaf();
183
185
  const finalAssistantMsg = {
184
186
  ...streamingAssistantMsg,
185
187
  content: accumulated,
@@ -188,28 +190,38 @@ export function useTextChatStream(options) {
188
190
  };
189
191
  setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
190
192
  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
193
  void refetch();
194
194
  };
195
- const tick = () => {
195
+ const frame = now => {
196
+ if (lastFrameTime === 0) {
197
+ lastFrameTime = now;
198
+ }
199
+ const elapsedMs = now - lastFrameTime;
200
+ lastFrameTime = now;
196
201
  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
- }]);
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);
205
214
  } else if (streamDone) {
206
- // Network finished and the reveal has caught up — settle.
207
215
  settle();
216
+ } else {
217
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
218
+ stopRaf();
208
219
  }
209
220
  };
210
- const startReveal = () => {
211
- if (!revealTimerRef.current) {
212
- revealTimerRef.current = setInterval(tick, revealIntervalMs);
221
+ const startRaf = () => {
222
+ if (rafRef.current === null) {
223
+ lastFrameTime = 0;
224
+ rafRef.current = requestAnimationFrame(frame);
213
225
  }
214
226
  };
215
227
 
@@ -228,20 +240,19 @@ export function useTextChatStream(options) {
228
240
  }, {
229
241
  onChunk: token => {
230
242
  accumulated += token;
231
- startReveal();
243
+ startRaf();
232
244
  },
233
245
  onDone: () => {
234
- // Let the drip finish revealing, then settle (see `tick`).
235
246
  streamDone = true;
236
- startReveal();
247
+ startRaf();
237
248
  },
238
249
  onError: err => {
239
- stopRevealTimer();
250
+ stopRaf();
240
251
  onError == null || onError(err);
241
252
  setStreamingMessages([]);
242
253
  }
243
254
  }, controller.signal);
244
- }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
255
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
245
256
  return {
246
257
  status,
247
258
  // Present for symmetry with `useTextChat`; the streaming path tracks its own
@@ -1 +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":[]}
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":[]}
@@ -12,10 +12,9 @@ export type StreamingChatMessage = ChatMessage & {
12
12
  export interface UseTextChatStreamOptions {
13
13
  streamUrl: string;
14
14
  limit?: number;
15
- revealIntervalMs?: number;
16
- revealCharsPerTick?: number;
17
15
  headers?: Record<string, string>;
18
16
  onError?: (err: Error) => void;
17
+ revealMsPerChar?: number;
19
18
  }
20
19
  export interface SendStreamMessageParams {
21
20
  message: string;
@@ -24,8 +23,14 @@ export interface SendStreamMessageParams {
24
23
  }
25
24
  /**
26
25
  * 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).
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.
29
34
  *
30
35
  * KNOWN LIMITATION: the streaming path does not run a function-call resolution
31
36
  * loop — parity with the current unary behavior.
@@ -1 +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"}
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"}
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.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": "8a061f964dc13796483f409e83598c703ed05cf2545ea59da3f54ddf"
106
+ "falconPackageHash": "e93bb4f738434f45ce730b0465ade783249af8d03bd939de0c73a284"
107
107
  }