@wix/legends-platform-sdk 1.8.0 → 1.10.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;
@@ -143,12 +142,12 @@ function useTextChatStream(options) {
143
142
  const send = (0, _react.useCallback)(async ({
144
143
  message,
145
144
  chatConfig,
146
- attachments
145
+ attachments,
146
+ metadata
147
147
  }) => {
148
148
  var _abortRef$current2;
149
- // Abort any in-flight stream + reveal before starting a new one.
149
+ // Abort any in-flight stream before starting a new one.
150
150
  (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
151
- stopRevealTimer();
152
151
  const controller = new AbortController();
153
152
  abortRef.current = controller;
154
153
  const seq = sendSeqRef.current += 1;
@@ -169,18 +168,23 @@ function useTextChatStream(options) {
169
168
  };
170
169
  setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
171
170
 
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;
171
+ // Tokens accumulate as they arrive, but we reveal them into
172
+ // `streamingText` at a steady time-paced rate via requestAnimationFrame.
173
+ // Pacing the reveal below the host typer's per-char rate stops it from
174
+ // building a backlog it would skip through (the jumpy/"super fast" look).
175
+ let accumulated = ''; // full text received so far
176
+ let revealedLen = 0; // how many chars we've dripped into streamingText
177
+ let streamDone = false; // network stream ended
178
+ let carry = 0; // fractional chars carried between frames
179
+ let lastFrameTime = 0; // 0 = uninitialized
180
+
177
181
  const settle = () => {
178
- stopRevealTimer();
179
- // Mirror the streamed text into both `content` and `streamingText`. A
180
- // host renderer can diff the two to place its typer at the end, so the
181
- // settled message shows in full with no re-type or flicker. Kept in
182
+ // Settle only after the drip has revealed everything AND the stream
183
+ // ended. Mirror the text into both `content` and `streamingText` so a
184
+ // host typer hands off with no re-type or flicker. Kept in
182
185
  // `completedMessages` (stable id) so the confirmed copy from refetch is
183
186
  // hidden and never remounts.
187
+ stopRaf();
184
188
  const finalAssistantMsg = {
185
189
  ...streamingAssistantMsg,
186
190
  content: accumulated,
@@ -189,28 +193,38 @@ function useTextChatStream(options) {
189
193
  };
190
194
  setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
191
195
  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
196
  void refetch();
195
197
  };
196
- const tick = () => {
198
+ const frame = now => {
199
+ if (lastFrameTime === 0) {
200
+ lastFrameTime = now;
201
+ }
202
+ const elapsedMs = now - lastFrameTime;
203
+ lastFrameTime = now;
197
204
  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
- }]);
205
+ carry += elapsedMs / revealMsPerChar;
206
+ const step = Math.floor(carry);
207
+ if (step > 0) {
208
+ carry -= step;
209
+ revealedLen = Math.min(accumulated.length, revealedLen + step);
210
+ setStreamingMessages([optimisticUserMsg, {
211
+ ...streamingAssistantMsg,
212
+ streamStatus: 'streaming',
213
+ streamingText: accumulated.slice(0, revealedLen)
214
+ }]);
215
+ }
216
+ rafRef.current = requestAnimationFrame(frame);
206
217
  } else if (streamDone) {
207
- // Network finished and the reveal has caught up — settle.
208
218
  settle();
219
+ } else {
220
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
221
+ stopRaf();
209
222
  }
210
223
  };
211
- const startReveal = () => {
212
- if (!revealTimerRef.current) {
213
- revealTimerRef.current = setInterval(tick, revealIntervalMs);
224
+ const startRaf = () => {
225
+ if (rafRef.current === null) {
226
+ lastFrameTime = 0;
227
+ rafRef.current = requestAnimationFrame(frame);
214
228
  }
215
229
  };
216
230
 
@@ -225,24 +239,24 @@ function useTextChatStream(options) {
225
239
  namespace,
226
240
  chatConfig,
227
241
  attachments,
242
+ metadata,
228
243
  headers
229
244
  }, {
230
245
  onChunk: token => {
231
246
  accumulated += token;
232
- startReveal();
247
+ startRaf();
233
248
  },
234
249
  onDone: () => {
235
- // Let the drip finish revealing, then settle (see `tick`).
236
250
  streamDone = true;
237
- startReveal();
251
+ startRaf();
238
252
  },
239
253
  onError: err => {
240
- stopRevealTimer();
254
+ stopRaf();
241
255
  onError == null || onError(err);
242
256
  setStreamingMessages([]);
243
257
  }
244
258
  }, controller.signal);
245
- }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
259
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
246
260
  return {
247
261
  status,
248
262
  // 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","metadata","_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 // Request metadata (e.g. `additionalMetadata`) forwarded to the stream\n // transport, at parity with the unary `sendChatMessage` request body.\n metadata?: object;\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 ({\n message,\n chatConfig,\n attachments,\n metadata,\n }: 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 metadata,\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":[]}
@@ -81,6 +81,7 @@ async function streamChatMessage(params, callbacks, signal) {
81
81
  namespace,
82
82
  chatConfig,
83
83
  attachments,
84
+ metadata,
84
85
  headers
85
86
  } = params;
86
87
  const {
@@ -101,7 +102,8 @@ async function streamChatMessage(params, callbacks, signal) {
101
102
  message,
102
103
  namespace,
103
104
  chatConfig,
104
- attachments
105
+ attachments,
106
+ metadata
105
107
  }),
106
108
  signal
107
109
  });
@@ -1 +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
+ {"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","metadata","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 // Request metadata forwarded to the RPC (e.g. `additionalMetadata` carrying\n // opening-message context). Mirrors the top-level `metadata` object the unary\n // `sendChatMessage` request accepts, so a streamed send carries it identically.\n metadata?: object;\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 {\n url,\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n metadata,\n 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 metadata,\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":[]}
@@ -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;
@@ -144,10 +142,10 @@ export function useTextChatStream(options) {
144
142
  var _abortRef$current2;
145
143
  let message = _ref2.message,
146
144
  chatConfig = _ref2.chatConfig,
147
- attachments = _ref2.attachments;
148
- // Abort any in-flight stream + reveal before starting a new one.
145
+ attachments = _ref2.attachments,
146
+ metadata = _ref2.metadata;
147
+ // Abort any in-flight stream before starting a new one.
149
148
  (_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
150
- stopRevealTimer();
151
149
  const controller = new AbortController();
152
150
  abortRef.current = controller;
153
151
  const seq = sendSeqRef.current += 1;
@@ -168,18 +166,23 @@ export function useTextChatStream(options) {
168
166
  };
169
167
  setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
170
168
 
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;
169
+ // Tokens accumulate as they arrive, but we reveal them into
170
+ // `streamingText` at a steady time-paced rate via requestAnimationFrame.
171
+ // Pacing the reveal below the host typer's per-char rate stops it from
172
+ // building a backlog it would skip through (the jumpy/"super fast" look).
173
+ let accumulated = ''; // full text received so far
174
+ let revealedLen = 0; // how many chars we've dripped into streamingText
175
+ let streamDone = false; // network stream ended
176
+ let carry = 0; // fractional chars carried between frames
177
+ let lastFrameTime = 0; // 0 = uninitialized
178
+
176
179
  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
180
+ // Settle only after the drip has revealed everything AND the stream
181
+ // ended. Mirror the text into both `content` and `streamingText` so a
182
+ // host typer hands off with no re-type or flicker. Kept in
181
183
  // `completedMessages` (stable id) so the confirmed copy from refetch is
182
184
  // hidden and never remounts.
185
+ stopRaf();
183
186
  const finalAssistantMsg = {
184
187
  ...streamingAssistantMsg,
185
188
  content: accumulated,
@@ -188,28 +191,38 @@ export function useTextChatStream(options) {
188
191
  };
189
192
  setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
190
193
  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
194
  void refetch();
194
195
  };
195
- const tick = () => {
196
+ const frame = now => {
197
+ if (lastFrameTime === 0) {
198
+ lastFrameTime = now;
199
+ }
200
+ const elapsedMs = now - lastFrameTime;
201
+ lastFrameTime = now;
196
202
  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
- }]);
203
+ carry += elapsedMs / revealMsPerChar;
204
+ const step = Math.floor(carry);
205
+ if (step > 0) {
206
+ carry -= step;
207
+ revealedLen = Math.min(accumulated.length, revealedLen + step);
208
+ setStreamingMessages([optimisticUserMsg, {
209
+ ...streamingAssistantMsg,
210
+ streamStatus: 'streaming',
211
+ streamingText: accumulated.slice(0, revealedLen)
212
+ }]);
213
+ }
214
+ rafRef.current = requestAnimationFrame(frame);
205
215
  } else if (streamDone) {
206
- // Network finished and the reveal has caught up — settle.
207
216
  settle();
217
+ } else {
218
+ // Caught up, waiting for more tokens; onChunk will restart the loop.
219
+ stopRaf();
208
220
  }
209
221
  };
210
- const startReveal = () => {
211
- if (!revealTimerRef.current) {
212
- revealTimerRef.current = setInterval(tick, revealIntervalMs);
222
+ const startRaf = () => {
223
+ if (rafRef.current === null) {
224
+ lastFrameTime = 0;
225
+ rafRef.current = requestAnimationFrame(frame);
213
226
  }
214
227
  };
215
228
 
@@ -224,24 +237,24 @@ export function useTextChatStream(options) {
224
237
  namespace,
225
238
  chatConfig,
226
239
  attachments,
240
+ metadata,
227
241
  headers
228
242
  }, {
229
243
  onChunk: token => {
230
244
  accumulated += token;
231
- startReveal();
245
+ startRaf();
232
246
  },
233
247
  onDone: () => {
234
- // Let the drip finish revealing, then settle (see `tick`).
235
248
  streamDone = true;
236
- startReveal();
249
+ startRaf();
237
250
  },
238
251
  onError: err => {
239
- stopRevealTimer();
252
+ stopRaf();
240
253
  onError == null || onError(err);
241
254
  setStreamingMessages([]);
242
255
  }
243
256
  }, controller.signal);
244
- }, [streamUrl, chatId, namespace, revealIntervalMs, revealCharsPerTick, headers, onError, refetch, stopRevealTimer]);
257
+ }, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
245
258
  return {
246
259
  status,
247
260
  // 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","metadata","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 // Request metadata (e.g. `additionalMetadata`) forwarded to the stream\n // transport, at parity with the unary `sendChatMessage` request body.\n metadata?: object;\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 ({\n message,\n chatConfig,\n attachments,\n metadata,\n }: 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 metadata,\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":[]}
@@ -74,6 +74,7 @@ export async function streamChatMessage(params, callbacks, signal) {
74
74
  namespace = params.namespace,
75
75
  chatConfig = params.chatConfig,
76
76
  attachments = params.attachments,
77
+ metadata = params.metadata,
77
78
  headers = params.headers;
78
79
  const onChunk = callbacks.onChunk,
79
80
  onDone = callbacks.onDone,
@@ -91,7 +92,8 @@ export async function streamChatMessage(params, callbacks, signal) {
91
92
  message,
92
93
  namespace,
93
94
  chatConfig,
94
- attachments
95
+ attachments,
96
+ metadata
95
97
  }),
96
98
  signal
97
99
  });
@@ -1 +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
+ {"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","metadata","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 // Request metadata forwarded to the RPC (e.g. `additionalMetadata` carrying\n // opening-message context). Mirrors the top-level `metadata` object the unary\n // `sendChatMessage` request accepts, so a streamed send carries it identically.\n metadata?: object;\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 {\n url,\n chatId,\n message,\n namespace,\n chatConfig,\n attachments,\n metadata,\n 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 metadata,\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":[]}
@@ -12,20 +12,26 @@ 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;
22
21
  chatConfig?: object;
23
22
  attachments?: ChatMessageAttachment[];
23
+ metadata?: object;
24
24
  }
25
25
  /**
26
26
  * Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
27
- * and progressively reveals the reply, merging the in-flight and completed
28
- * streamed exchanges with the confirmed history (oldest-first).
27
+ * and drips the accumulated reply into `streamingText` at a steady, time-paced
28
+ * rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight
29
+ * and completed streamed exchanges with the confirmed history (oldest-first).
30
+ * The host renderer (e.g. dolly) animates the dripped prefix with its own
31
+ * typewriter; pacing the drip below the typer's per-char rate keeps the typer
32
+ * from building a backlog it would otherwise skip through (jumpy reveal). The
33
+ * exchange settles only once the drip has revealed everything AND the network
34
+ * stream has ended, which avoids an end-of-reply snap.
29
35
  *
30
36
  * KNOWN LIMITATION: the streaming path does not run a function-call resolution
31
37
  * loop — parity with the current unary behavior.
@@ -36,6 +42,6 @@ export declare function useTextChatStream(options: UseTextChatStreamOptions): {
36
42
  messages: StreamingChatMessage[];
37
43
  fetchNext: () => void;
38
44
  refetch: (options?: import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aq | undefined) => Promise<import("@tanstack/query-core/build/legacy/hydration-BlEVG2Lp").aH<ChatMessage[], Error>>;
39
- send: ({ message, chatConfig, attachments }: SendStreamMessageParams) => Promise<void>;
45
+ send: ({ message, chatConfig, attachments, metadata, }: SendStreamMessageParams) => Promise<void>;
40
46
  };
41
47
  //# sourceMappingURL=useTextChatStream.d.ts.map
@@ -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;IAGtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAgBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;4DAwG5D,uBAAuB;EA4J7B"}
@@ -11,6 +11,7 @@ export type StreamChatMessageParams = {
11
11
  namespace: string;
12
12
  chatConfig?: object;
13
13
  attachments?: ChatMessageAttachment[];
14
+ metadata?: object;
14
15
  headers?: Record<string, string>;
15
16
  };
16
17
  export declare function extractContent(frame: string): string | null;
@@ -1 +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"}
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,QAAQ,CAAC,EAAE,MAAM,CAAC;IAIlB,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,CAyDf"}
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0",
2
+ "version": "1.10.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": "18c2eb4ee97d69ddad821c7e45766958577391a3fa4bebca529fc4e5"
107
107
  }