@wix/legends-platform-sdk 1.12.0 → 1.14.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.
- package/README.md +17 -3
- package/dist/cjs/hooks/useConversationEvents/types.js.map +1 -1
- package/dist/cjs/hooks/useSpeechActivityDetector/helpers.js.map +1 -1
- package/dist/cjs/hooks/useTextChat/index.js +0 -6
- package/dist/cjs/hooks/useTextChat/index.js.map +1 -1
- package/dist/cjs/livekit/sdk.js +42 -0
- package/dist/cjs/livekit/sdk.js.map +1 -1
- package/dist/cjs/sdk.js +0 -42
- package/dist/cjs/sdk.js.map +1 -1
- package/dist/esm/hooks/useConversationEvents/types.js.map +1 -1
- package/dist/esm/hooks/useSpeechActivityDetector/helpers.js.map +1 -1
- package/dist/esm/hooks/useTextChat/index.js +0 -1
- package/dist/esm/hooks/useTextChat/index.js.map +1 -1
- package/dist/esm/livekit/sdk.js +10 -0
- package/dist/esm/livekit/sdk.js.map +1 -1
- package/dist/esm/sdk.js +0 -7
- package/dist/esm/sdk.js.map +1 -1
- package/dist/types/hooks/useConversationEvents/types.d.ts +1 -1
- package/dist/types/hooks/useConversationEvents/types.d.ts.map +1 -1
- package/dist/types/hooks/useSpeechActivityDetector/helpers.d.ts +1 -1
- package/dist/types/hooks/useSpeechActivityDetector/helpers.d.ts.map +1 -1
- package/dist/types/hooks/useTextChat/index.d.ts +0 -1
- package/dist/types/hooks/useTextChat/index.d.ts.map +1 -1
- package/dist/types/livekit/sdk.d.ts +7 -0
- package/dist/types/livekit/sdk.d.ts.map +1 -1
- package/dist/types/sdk.d.ts +0 -7
- package/dist/types/sdk.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/cjs/hooks/useTextChat/useTextChatStream.js +0 -274
- package/dist/cjs/hooks/useTextChat/useTextChatStream.js.map +0 -1
- package/dist/cjs/services/streamChatMessage.js +0 -166
- package/dist/cjs/services/streamChatMessage.js.map +0 -1
- package/dist/esm/hooks/useTextChat/useTextChatStream.js +0 -276
- package/dist/esm/hooks/useTextChat/useTextChatStream.js.map +0 -1
- package/dist/esm/services/streamChatMessage.js +0 -155
- package/dist/esm/services/streamChatMessage.js.map +0 -1
- package/dist/types/hooks/useTextChat/useTextChatStream.d.ts +0 -47
- package/dist/types/hooks/useTextChat/useTextChatStream.d.ts.map +0 -1
- package/dist/types/services/streamChatMessage.d.ts +0 -27
- package/dist/types/services/streamChatMessage.d.ts.map +0 -1
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
exports.__esModule = true;
|
|
4
|
-
exports.useTextChatStream = useTextChatStream;
|
|
5
|
-
var _react = require("react");
|
|
6
|
-
var _reactQuery = require("@tanstack/react-query");
|
|
7
|
-
var _sdkConsumer = require("../../sdk-consumer");
|
|
8
|
-
var _streamChatMessage = require("../../services/streamChatMessage");
|
|
9
|
-
exports.streamChatMessage = _streamChatMessage.streamChatMessage;
|
|
10
|
-
// Re-exported here so consumers can reach the framework-agnostic transport
|
|
11
|
-
// alongside the hook.
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A `ChatMessage` augmented with streaming metadata. The host renderer uses
|
|
15
|
-
* `streamStatus`/`streamingText` to drive its typewriter; confirmed
|
|
16
|
-
* (non-streamed) messages carry neither.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Identity for deduping optimistic streamed messages against confirmed ones.
|
|
21
|
-
*
|
|
22
|
-
* This is a *content* join: the streamed text must equal the server-persisted
|
|
23
|
-
* copy for the confirmed duplicate to be hidden. `.trim()` absorbs trailing-
|
|
24
|
-
* whitespace differences, but heavier server-side post-processing (markdown /
|
|
25
|
-
* entity normalization, an appended citation) would make the keys diverge and
|
|
26
|
-
* briefly surface both copies. A shared message id would be more robust, but
|
|
27
|
-
* the streamed frames carry only token deltas — no id — so content equality is
|
|
28
|
-
* the best join available without backend support.
|
|
29
|
-
*/
|
|
30
|
-
const messageKey = m => `${m.role}::${(m.content ?? '').trim()}`;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Streaming counterpart to `useTextChat`. Sends a message over the SSE transport
|
|
34
|
-
* and drips the accumulated reply into `streamingText` at a steady, time-paced
|
|
35
|
-
* rate (see `revealMsPerChar`) via requestAnimationFrame, merging the in-flight
|
|
36
|
-
* and completed streamed exchanges with the confirmed history (oldest-first).
|
|
37
|
-
* The host renderer (e.g. dolly) animates the dripped prefix with its own
|
|
38
|
-
* typewriter; pacing the drip below the typer's per-char rate keeps the typer
|
|
39
|
-
* from building a backlog it would otherwise skip through (jumpy reveal). The
|
|
40
|
-
* exchange settles only once the drip has revealed everything AND the network
|
|
41
|
-
* stream has ended, which avoids an end-of-reply snap.
|
|
42
|
-
*
|
|
43
|
-
* KNOWN LIMITATION: the streaming path does not run a function-call resolution
|
|
44
|
-
* loop — parity with the current unary behavior.
|
|
45
|
-
*/
|
|
46
|
-
function useTextChatStream(options = {}) {
|
|
47
|
-
const {
|
|
48
|
-
streamUrl,
|
|
49
|
-
limit = 50,
|
|
50
|
-
headers,
|
|
51
|
-
onError,
|
|
52
|
-
revealMsPerChar = 16
|
|
53
|
-
} = options;
|
|
54
|
-
const {
|
|
55
|
-
chatId,
|
|
56
|
-
namespace,
|
|
57
|
-
personaChatService
|
|
58
|
-
} = (0, _sdkConsumer.useSdkConsumer)();
|
|
59
|
-
const {
|
|
60
|
-
data: sdkMessages = [],
|
|
61
|
-
status,
|
|
62
|
-
fetchNextPage,
|
|
63
|
-
refetch
|
|
64
|
-
} = (0, _reactQuery.useInfiniteQuery)({
|
|
65
|
-
queryKey: ['chat-messages', chatId, namespace],
|
|
66
|
-
queryFn: async ({
|
|
67
|
-
pageParam
|
|
68
|
-
}) => {
|
|
69
|
-
if (!personaChatService) {
|
|
70
|
-
return {
|
|
71
|
-
messages: [],
|
|
72
|
-
nextCursor: undefined
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return personaChatService.queryChatMessages({
|
|
76
|
-
chatId,
|
|
77
|
-
namespace,
|
|
78
|
-
limit,
|
|
79
|
-
cursor: pageParam
|
|
80
|
-
});
|
|
81
|
-
},
|
|
82
|
-
getNextPageParam: lastPage => lastPage.nextCursor,
|
|
83
|
-
initialPageParam: undefined,
|
|
84
|
-
select: data => data.pages.flatMap(page => page.messages)
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Local state for the in-progress streaming exchange (user + assistant messages).
|
|
88
|
-
const [streamingMessages, setStreamingMessages] = (0, _react.useState)([]);
|
|
89
|
-
// Exchanges that finished streaming, kept for the lifetime of this chat. They
|
|
90
|
-
// keep their stable optimistic ids and stand in for the SDK's confirmed copies
|
|
91
|
-
// (which are hidden as duplicates) — this both bridges the refetch lag and
|
|
92
|
-
// avoids remounting the message (and restarting its typewriter) when the
|
|
93
|
-
// confirmed copy, with a different id, arrives.
|
|
94
|
-
const [completedMessages, setCompletedMessages] = (0, _react.useState)([]);
|
|
95
|
-
const abortRef = (0, _react.useRef)(null);
|
|
96
|
-
// Handle of the pending reveal-drip animation frame, if any.
|
|
97
|
-
const rafRef = (0, _react.useRef)(null);
|
|
98
|
-
// Last non-empty confirmed list. `refetch()` can transiently empty sdkMessages;
|
|
99
|
-
// falling back to this keeps the history from disappearing and reappearing.
|
|
100
|
-
const lastConfirmedRef = (0, _react.useRef)([]);
|
|
101
|
-
// Monotonic per-hook counter so two sends within the same millisecond still
|
|
102
|
-
// produce distinct message ids (used as React keys downstream).
|
|
103
|
-
const sendSeqRef = (0, _react.useRef)(0);
|
|
104
|
-
|
|
105
|
-
// Cancel any pending reveal-drip frame.
|
|
106
|
-
const stopRaf = (0, _react.useCallback)(() => {
|
|
107
|
-
if (rafRef.current !== null) {
|
|
108
|
-
cancelAnimationFrame(rafRef.current);
|
|
109
|
-
rafRef.current = null;
|
|
110
|
-
}
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
// Stop any in-flight stream (and reveal drip) on unmount.
|
|
114
|
-
(0, _react.useEffect)(() => {
|
|
115
|
-
return () => {
|
|
116
|
-
var _abortRef$current;
|
|
117
|
-
(_abortRef$current = abortRef.current) == null || _abortRef$current.abort();
|
|
118
|
-
stopRaf();
|
|
119
|
-
};
|
|
120
|
-
}, [stopRaf]);
|
|
121
|
-
const messages = (0, _react.useMemo)(() => {
|
|
122
|
-
const converted = [...sdkMessages].reverse();
|
|
123
|
-
const confirmedMessages = converted.length > 0 ? converted : lastConfirmedRef.current;
|
|
124
|
-
|
|
125
|
-
// Show our locally-tracked streamed exchanges (stable ids) and hide the
|
|
126
|
-
// SDK's confirmed duplicates. Swapping to a confirmed copy would remount the
|
|
127
|
-
// message (different id) and restart its typewriter — that's the flicker.
|
|
128
|
-
// The in-progress streaming messages are always shown as-is.
|
|
129
|
-
const completedKeys = new Set(completedMessages.map(messageKey));
|
|
130
|
-
const confirmedShown = confirmedMessages.filter(m => !completedKeys.has(messageKey(m)));
|
|
131
|
-
return [...confirmedShown, ...completedMessages, ...streamingMessages];
|
|
132
|
-
}, [sdkMessages, streamingMessages, completedMessages]);
|
|
133
|
-
|
|
134
|
-
// Remember the latest non-empty confirmed list so a transient empty during a
|
|
135
|
-
// refetch doesn't blank the history.
|
|
136
|
-
(0, _react.useEffect)(() => {
|
|
137
|
-
const converted = [...sdkMessages].reverse();
|
|
138
|
-
if (converted.length > 0) {
|
|
139
|
-
lastConfirmedRef.current = converted;
|
|
140
|
-
}
|
|
141
|
-
}, [sdkMessages]);
|
|
142
|
-
const send = (0, _react.useCallback)(async ({
|
|
143
|
-
message,
|
|
144
|
-
chatConfig,
|
|
145
|
-
attachments,
|
|
146
|
-
metadata
|
|
147
|
-
}) => {
|
|
148
|
-
var _abortRef$current2;
|
|
149
|
-
// Abort any in-flight stream before starting a new one.
|
|
150
|
-
(_abortRef$current2 = abortRef.current) == null || _abortRef$current2.abort();
|
|
151
|
-
const controller = new AbortController();
|
|
152
|
-
abortRef.current = controller;
|
|
153
|
-
const seq = sendSeqRef.current += 1;
|
|
154
|
-
const userMsgId = `stream-user-${Date.now()}-${seq}`;
|
|
155
|
-
const assistantMsgId = `stream-assistant-${Date.now()}-${seq}`;
|
|
156
|
-
const optimisticUserMsg = {
|
|
157
|
-
id: userMsgId,
|
|
158
|
-
role: 'USER',
|
|
159
|
-
content: message,
|
|
160
|
-
attachments
|
|
161
|
-
};
|
|
162
|
-
const streamingAssistantMsg = {
|
|
163
|
-
id: assistantMsgId,
|
|
164
|
-
role: 'ASSISTANT',
|
|
165
|
-
content: '',
|
|
166
|
-
streamStatus: 'streaming',
|
|
167
|
-
streamingText: ''
|
|
168
|
-
};
|
|
169
|
-
setStreamingMessages([optimisticUserMsg, streamingAssistantMsg]);
|
|
170
|
-
|
|
171
|
-
// Tokens 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
|
-
|
|
181
|
-
const settle = () => {
|
|
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
|
|
185
|
-
// `completedMessages` (stable id) so the confirmed copy from refetch is
|
|
186
|
-
// hidden and never remounts.
|
|
187
|
-
stopRaf();
|
|
188
|
-
const finalAssistantMsg = {
|
|
189
|
-
...streamingAssistantMsg,
|
|
190
|
-
content: accumulated,
|
|
191
|
-
streamStatus: 'settled',
|
|
192
|
-
streamingText: accumulated
|
|
193
|
-
};
|
|
194
|
-
setCompletedMessages(prev => [...prev, optimisticUserMsg, finalAssistantMsg]);
|
|
195
|
-
setStreamingMessages([]);
|
|
196
|
-
void refetch();
|
|
197
|
-
};
|
|
198
|
-
const frame = now => {
|
|
199
|
-
if (lastFrameTime === 0) {
|
|
200
|
-
lastFrameTime = now;
|
|
201
|
-
}
|
|
202
|
-
const elapsedMs = now - lastFrameTime;
|
|
203
|
-
lastFrameTime = now;
|
|
204
|
-
if (revealedLen < accumulated.length) {
|
|
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);
|
|
217
|
-
} else if (streamDone) {
|
|
218
|
-
settle();
|
|
219
|
-
} else {
|
|
220
|
-
// Caught up, waiting for more tokens; onChunk will restart the loop.
|
|
221
|
-
stopRaf();
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
const startRaf = () => {
|
|
225
|
-
if (rafRef.current === null) {
|
|
226
|
-
lastFrameTime = 0;
|
|
227
|
-
rafRef.current = requestAnimationFrame(frame);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Streaming deliberately bypasses the injected `personaChatService` (the
|
|
232
|
-
// DI seam every other hook uses): SSE needs a raw fetch with cookie auth
|
|
233
|
-
// to a caller-supplied url, which the Api-Key `IPersonaChatService` can't
|
|
234
|
-
// model. History reads still go through the service (see the query above).
|
|
235
|
-
await (0, _streamChatMessage.streamChatMessage)({
|
|
236
|
-
url: streamUrl,
|
|
237
|
-
chatId,
|
|
238
|
-
message,
|
|
239
|
-
namespace,
|
|
240
|
-
chatConfig,
|
|
241
|
-
attachments,
|
|
242
|
-
metadata,
|
|
243
|
-
headers
|
|
244
|
-
}, {
|
|
245
|
-
onChunk: token => {
|
|
246
|
-
accumulated += token;
|
|
247
|
-
startRaf();
|
|
248
|
-
},
|
|
249
|
-
onDone: () => {
|
|
250
|
-
streamDone = true;
|
|
251
|
-
startRaf();
|
|
252
|
-
},
|
|
253
|
-
onError: err => {
|
|
254
|
-
stopRaf();
|
|
255
|
-
onError == null || onError(err);
|
|
256
|
-
setStreamingMessages([]);
|
|
257
|
-
}
|
|
258
|
-
}, controller.signal);
|
|
259
|
-
}, [streamUrl, chatId, namespace, headers, onError, refetch, revealMsPerChar, stopRaf]);
|
|
260
|
-
return {
|
|
261
|
-
status,
|
|
262
|
-
// Present for symmetry with `useTextChat`; the streaming path tracks its own
|
|
263
|
-
// optimistic state, so this is always empty.
|
|
264
|
-
pending: [],
|
|
265
|
-
messages,
|
|
266
|
-
fetchNext,
|
|
267
|
-
refetch,
|
|
268
|
-
send
|
|
269
|
-
};
|
|
270
|
-
function fetchNext() {
|
|
271
|
-
void fetchNextPage();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
//# sourceMappingURL=useTextChatStream.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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 // Optional endpoint override. Defaults to the same-origin Wix persona-chat\n // gateway path, so a consumer needs no proxy route of its own.\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":[]}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
exports.__esModule = true;
|
|
4
|
-
exports.createObjectScanner = createObjectScanner;
|
|
5
|
-
exports.extractContent = extractContent;
|
|
6
|
-
exports.streamChatMessage = streamChatMessage;
|
|
7
|
-
// Same-origin Wix persona-chat gateway path. Used when the caller doesn't
|
|
8
|
-
// supply a `url`, so a consumer needs no proxy route of its own.
|
|
9
|
-
const DEFAULT_STREAM_PATH = '/_api/persona-chat-service/v1/chat/messages/stream';
|
|
10
|
-
// Read a cookie value by name in the browser. Returns undefined outside a
|
|
11
|
-
// browser or when the cookie is absent. Dependency-free by design.
|
|
12
|
-
function readCookie(name) {
|
|
13
|
-
var _document$cookie$spli;
|
|
14
|
-
if (typeof document === 'undefined') {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
return (_document$cookie$spli = document.cookie.split(';').map(part => part.trim()).find(part => part.startsWith(`${name}=`))) == null ? void 0 : _document$cookie$spli.slice(name.length + 1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Pull the assistant text out of one streamed frame. Two framings reach us: a
|
|
21
|
-
// proxy route yields `{ content }` objects, while the gateway wraps each frame
|
|
22
|
-
// as `{ body: base64(json) }`. Returns null for frames without content.
|
|
23
|
-
function extractContent(frame) {
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(frame);
|
|
26
|
-
if (typeof parsed.content === 'string') {
|
|
27
|
-
return parsed.content;
|
|
28
|
-
}
|
|
29
|
-
if (typeof parsed.body === 'string') {
|
|
30
|
-
const decoded = new TextDecoder().decode(Uint8Array.from(atob(parsed.body), char => char.charCodeAt(0)));
|
|
31
|
-
const inner = JSON.parse(decoded);
|
|
32
|
-
return typeof inner.content === 'string' ? inner.content : null;
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// Incomplete or non-object frame — skip it.
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Feeds decoded text through a running scanner that emits each complete
|
|
41
|
-
// top-level `{…}` object as its closing brace arrives. This keeps a message
|
|
42
|
-
// that's split across reads intact, and parses a newline-free JSON array (the
|
|
43
|
-
// gateway framing) incrementally. Anything outside a top-level object is ignored.
|
|
44
|
-
function createObjectScanner(onObject) {
|
|
45
|
-
let buffer = '';
|
|
46
|
-
let cursor = 0;
|
|
47
|
-
let braceDepth = 0;
|
|
48
|
-
let objectStart = -1;
|
|
49
|
-
let inString = false;
|
|
50
|
-
let escaped = false;
|
|
51
|
-
return function feed(text) {
|
|
52
|
-
buffer += text;
|
|
53
|
-
for (; cursor < buffer.length; cursor++) {
|
|
54
|
-
const char = buffer[cursor];
|
|
55
|
-
if (inString) {
|
|
56
|
-
if (escaped) {
|
|
57
|
-
escaped = false;
|
|
58
|
-
} else if (char === '\\') {
|
|
59
|
-
escaped = true;
|
|
60
|
-
} else if (char === '"') {
|
|
61
|
-
inString = false;
|
|
62
|
-
}
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (char === '"') {
|
|
66
|
-
inString = true;
|
|
67
|
-
} else if (char === '{') {
|
|
68
|
-
if (braceDepth === 0) {
|
|
69
|
-
objectStart = cursor;
|
|
70
|
-
}
|
|
71
|
-
braceDepth++;
|
|
72
|
-
} else if (char === '}' && braceDepth > 0) {
|
|
73
|
-
braceDepth--;
|
|
74
|
-
if (braceDepth === 0 && objectStart >= 0) {
|
|
75
|
-
onObject(buffer.slice(objectStart, cursor + 1));
|
|
76
|
-
objectStart = -1;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Framework-agnostic streaming transport. POSTs the message to the same-origin
|
|
85
|
-
* Wix persona-chat gateway (or a caller-supplied `url` override), reads the
|
|
86
|
-
* response body incrementally, and fires `onChunk` per assistant token,
|
|
87
|
-
* `onDone` on close, `onError` on failure. A caller-supplied `AbortSignal`
|
|
88
|
-
* cancels the request; `AbortError` is swallowed.
|
|
89
|
-
*/
|
|
90
|
-
async function streamChatMessage(params, callbacks, signal) {
|
|
91
|
-
const {
|
|
92
|
-
url,
|
|
93
|
-
chatId,
|
|
94
|
-
message,
|
|
95
|
-
namespace,
|
|
96
|
-
chatConfig,
|
|
97
|
-
attachments,
|
|
98
|
-
metadata,
|
|
99
|
-
headers
|
|
100
|
-
} = params;
|
|
101
|
-
const {
|
|
102
|
-
onChunk,
|
|
103
|
-
onDone,
|
|
104
|
-
onError
|
|
105
|
-
} = callbacks;
|
|
106
|
-
const endpoint = url ?? DEFAULT_STREAM_PATH;
|
|
107
|
-
|
|
108
|
-
// Derive the XSRF header in the browser only when the caller didn't supply
|
|
109
|
-
// one — the same-origin gateway is CSRF-protected and the token lives in the
|
|
110
|
-
// JS-readable `XSRF-TOKEN` cookie. An explicit caller header always wins.
|
|
111
|
-
const xsrfToken = readCookie('XSRF-TOKEN');
|
|
112
|
-
const autoXsrf = !(headers != null && headers['x-xsrf-token']) && xsrfToken ? {
|
|
113
|
-
'x-xsrf-token': xsrfToken
|
|
114
|
-
} : undefined;
|
|
115
|
-
try {
|
|
116
|
-
const response = await fetch(endpoint, {
|
|
117
|
-
method: 'POST',
|
|
118
|
-
credentials: 'include',
|
|
119
|
-
headers: {
|
|
120
|
-
'Content-Type': 'application/json',
|
|
121
|
-
...autoXsrf,
|
|
122
|
-
...headers
|
|
123
|
-
},
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
chatId,
|
|
126
|
-
message,
|
|
127
|
-
namespace,
|
|
128
|
-
chatConfig,
|
|
129
|
-
attachments,
|
|
130
|
-
metadata
|
|
131
|
-
}),
|
|
132
|
-
signal
|
|
133
|
-
});
|
|
134
|
-
if (!response.ok || !response.body) {
|
|
135
|
-
onError(new Error(`Stream request failed: ${response.status}`));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const scanForObjects = createObjectScanner(frame => {
|
|
139
|
-
const content = extractContent(frame);
|
|
140
|
-
if (content !== null) {
|
|
141
|
-
onChunk(content);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
const reader = response.body.getReader();
|
|
145
|
-
const decoder = new TextDecoder();
|
|
146
|
-
for (;;) {
|
|
147
|
-
const {
|
|
148
|
-
done,
|
|
149
|
-
value
|
|
150
|
-
} = await reader.read();
|
|
151
|
-
if (done) {
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
scanForObjects(decoder.decode(value, {
|
|
155
|
-
stream: true
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
scanForObjects(decoder.decode());
|
|
159
|
-
onDone();
|
|
160
|
-
} catch (err) {
|
|
161
|
-
if (err.name !== 'AbortError') {
|
|
162
|
-
onError(err);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
//# sourceMappingURL=streamChatMessage.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["DEFAULT_STREAM_PATH","readCookie","name","_document$cookie$spli","document","undefined","cookie","split","map","part","trim","find","startsWith","slice","length","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","streamChatMessage","params","callbacks","signal","url","chatId","message","namespace","chatConfig","attachments","metadata","headers","onChunk","onDone","onError","endpoint","xsrfToken","autoXsrf","response","fetch","method","credentials","stringify","ok","Error","status","scanForObjects","reader","getReader","decoder","done","value","read","stream","err"],"sources":["../../../src/services/streamChatMessage.ts"],"sourcesContent":["import type { ChatMessageAttachment } from '../types';\n\n// Same-origin Wix persona-chat gateway path. Used when the caller doesn't\n// supply a `url`, so a consumer needs no proxy route of its own.\nconst DEFAULT_STREAM_PATH =\n '/_api/persona-chat-service/v1/chat/messages/stream';\n\nexport type StreamCallbacks = {\n onChunk: (token: string) => void;\n onDone: () => void;\n onError: (err: Error) => void;\n};\n\nexport type StreamChatMessageParams = {\n // Optional endpoint override. Defaults to the same-origin Wix persona-chat\n // gateway path (`DEFAULT_STREAM_PATH`), so a consumer needs no proxy route.\n //\n // SECURITY: the request is sent with `credentials: 'include'`, so the session\n // cookie is delivered to whatever origin this resolves to. Any override MUST\n // come from 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// Read a cookie value by name in the browser. Returns undefined outside a\n// browser or when the cookie is absent. Dependency-free by design.\nfunction readCookie(name: string): string | undefined {\n if (typeof document === 'undefined') {\n return undefined;\n }\n return document.cookie\n .split(';')\n .map((part) => part.trim())\n .find((part) => part.startsWith(`${name}=`))\n ?.slice(name.length + 1);\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 same-origin\n * Wix persona-chat gateway (or a caller-supplied `url` override), reads the\n * response body incrementally, and fires `onChunk` per assistant token,\n * `onDone` on close, `onError` on failure. A caller-supplied `AbortSignal`\n * 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 const endpoint = url ?? DEFAULT_STREAM_PATH;\n\n // Derive the XSRF header in the browser only when the caller didn't supply\n // one — the same-origin gateway is CSRF-protected and the token lives in the\n // JS-readable `XSRF-TOKEN` cookie. An explicit caller header always wins.\n const xsrfToken = readCookie('XSRF-TOKEN');\n const autoXsrf =\n !headers?.['x-xsrf-token'] && xsrfToken\n ? { 'x-xsrf-token': xsrfToken }\n : undefined;\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...autoXsrf, ...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":[]}
|