@deltakit/react 0.2.0 → 0.2.1
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 +116 -4
- package/dist/index.cjs +488 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -5
- package/dist/index.d.ts +145 -5
- package/dist/index.js +489 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -85,8 +85,9 @@ function useAutoScroll(dependencies, options) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// src/use-stream-chat.ts
|
|
88
|
-
import {
|
|
89
|
-
|
|
88
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
89
|
+
|
|
90
|
+
// src/chat-controller.ts
|
|
90
91
|
var counter = 0;
|
|
91
92
|
function generateId() {
|
|
92
93
|
return `msg_${Date.now()}_${++counter}`;
|
|
@@ -94,30 +95,403 @@ function generateId() {
|
|
|
94
95
|
function createMessage(role, parts) {
|
|
95
96
|
return { id: generateId(), role, parts };
|
|
96
97
|
}
|
|
98
|
+
function createChatTransportContext(options) {
|
|
99
|
+
const helpers = {
|
|
100
|
+
appendPart: options.appendPart,
|
|
101
|
+
appendText: options.appendText,
|
|
102
|
+
setMessages: options.setMessages
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
emit: (event) => {
|
|
106
|
+
options.eventHandler(event, helpers);
|
|
107
|
+
},
|
|
108
|
+
ensureAssistantMessage: () => {
|
|
109
|
+
options.setMessages((prev) => {
|
|
110
|
+
const last = prev[prev.length - 1];
|
|
111
|
+
if (last?.role === "assistant") {
|
|
112
|
+
return prev;
|
|
113
|
+
}
|
|
114
|
+
return [...prev, createMessage("assistant", [])];
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
fail: (error) => {
|
|
118
|
+
options.setError(error);
|
|
119
|
+
options.onError?.(error);
|
|
120
|
+
options.setIsLoading(false);
|
|
121
|
+
options.setRunId(null);
|
|
122
|
+
},
|
|
123
|
+
finish: () => {
|
|
124
|
+
options.setIsLoading(false);
|
|
125
|
+
options.setRunId(null);
|
|
126
|
+
const finalMessages = options.getMessages();
|
|
127
|
+
const lastMessage = finalMessages[finalMessages.length - 1];
|
|
128
|
+
if (lastMessage?.role === "assistant") {
|
|
129
|
+
options.onMessage?.(lastMessage);
|
|
130
|
+
}
|
|
131
|
+
options.onFinish?.(finalMessages);
|
|
132
|
+
},
|
|
133
|
+
getMessages: options.getMessages,
|
|
134
|
+
setRunId: (runId) => {
|
|
135
|
+
options.setRunId(runId);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/transports.ts
|
|
141
|
+
import { parseSSEStream } from "@deltakit/core";
|
|
142
|
+
function toError(error) {
|
|
143
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
144
|
+
}
|
|
145
|
+
function isAbortError(error) {
|
|
146
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
147
|
+
}
|
|
148
|
+
function resolveRunId(response) {
|
|
149
|
+
if (!response || typeof response !== "object") {
|
|
150
|
+
throw new Error("Background SSE start response did not contain a run id");
|
|
151
|
+
}
|
|
152
|
+
const maybeRunId = "runId" in response ? response.runId : "job_id" in response ? response.job_id : null;
|
|
153
|
+
if (typeof maybeRunId !== "string" || maybeRunId.length === 0) {
|
|
154
|
+
throw new Error("Background SSE start response did not contain a run id");
|
|
155
|
+
}
|
|
156
|
+
return maybeRunId;
|
|
157
|
+
}
|
|
158
|
+
function resolveUrl(url, runId) {
|
|
159
|
+
return typeof url === "function" ? url(runId) : url.replace(":runId", runId);
|
|
160
|
+
}
|
|
161
|
+
async function streamFetchSSE(response, context, signal) {
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`SSE request failed: ${response.status} ${response.statusText}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (!response.body) {
|
|
168
|
+
throw new Error("Response body is null \u2014 SSE streaming not supported");
|
|
169
|
+
}
|
|
170
|
+
for await (const event of parseSSEStream(response.body, signal)) {
|
|
171
|
+
context.emit(event);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function createDirectSSETransport(config) {
|
|
175
|
+
return {
|
|
176
|
+
start: ({ context, message }) => {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const run = {
|
|
179
|
+
close: () => {
|
|
180
|
+
controller.abort();
|
|
181
|
+
},
|
|
182
|
+
stop: () => {
|
|
183
|
+
controller.abort();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
187
|
+
void (async () => {
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetchImpl(config.api, {
|
|
190
|
+
body: JSON.stringify({ message, ...config.body }),
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
...config.headers
|
|
194
|
+
},
|
|
195
|
+
method: config.method ?? "POST",
|
|
196
|
+
signal: controller.signal
|
|
197
|
+
});
|
|
198
|
+
await streamFetchSSE(response, context, controller.signal);
|
|
199
|
+
context.finish();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!isAbortError(error)) {
|
|
202
|
+
context.fail(toError(error));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
return run;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function createBackgroundSSETransport(config) {
|
|
211
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
212
|
+
const connect = (runId, context) => {
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
void (async () => {
|
|
215
|
+
try {
|
|
216
|
+
context.ensureAssistantMessage();
|
|
217
|
+
const response = await fetchImpl(resolveUrl(config.eventsApi, runId), {
|
|
218
|
+
headers: config.eventHeaders,
|
|
219
|
+
method: "GET",
|
|
220
|
+
signal: controller.signal
|
|
221
|
+
});
|
|
222
|
+
await streamFetchSSE(response, context, controller.signal);
|
|
223
|
+
context.finish();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (!isAbortError(error)) {
|
|
226
|
+
context.fail(toError(error));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
return {
|
|
231
|
+
close: () => {
|
|
232
|
+
controller.abort();
|
|
233
|
+
},
|
|
234
|
+
stop: () => {
|
|
235
|
+
controller.abort();
|
|
236
|
+
if (config.cancelApi) {
|
|
237
|
+
void fetchImpl(resolveUrl(config.cancelApi, runId), {
|
|
238
|
+
method: "POST"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
runId
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
resume: ({ context, runId }) => {
|
|
247
|
+
context.setRunId(runId);
|
|
248
|
+
return connect(runId, context);
|
|
249
|
+
},
|
|
250
|
+
start: ({ context, message }) => {
|
|
251
|
+
const startController = new AbortController();
|
|
252
|
+
let activeRun;
|
|
253
|
+
void (async () => {
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetchImpl(config.startApi, {
|
|
256
|
+
body: JSON.stringify({ message, ...config.startBody }),
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
...config.startHeaders
|
|
260
|
+
},
|
|
261
|
+
method: config.startMethod ?? "POST",
|
|
262
|
+
signal: startController.signal
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Background SSE start failed: ${response.status} ${response.statusText}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const data = await response.json();
|
|
270
|
+
const runId = (config.resolveRunId ?? resolveRunId)(data);
|
|
271
|
+
context.setRunId(runId);
|
|
272
|
+
activeRun = connect(runId, context);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (!isAbortError(error)) {
|
|
275
|
+
context.fail(toError(error));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
return {
|
|
280
|
+
close: () => {
|
|
281
|
+
startController.abort();
|
|
282
|
+
void activeRun?.close?.();
|
|
283
|
+
},
|
|
284
|
+
stop: () => {
|
|
285
|
+
startController.abort();
|
|
286
|
+
activeRun?.stop?.();
|
|
287
|
+
},
|
|
288
|
+
runId: null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function defaultParseWebSocketMessage(data) {
|
|
294
|
+
if (typeof data !== "string") {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const parsed = JSON.parse(data);
|
|
298
|
+
return parsed;
|
|
299
|
+
}
|
|
300
|
+
function createWebSocketTransport(config) {
|
|
301
|
+
const parseMessage = config.parseMessage ?? defaultParseWebSocketMessage;
|
|
302
|
+
const serializeMessage = config.serializeMessage ?? JSON.stringify;
|
|
303
|
+
let resolvedRunId = null;
|
|
304
|
+
const applyIncomingEvents = (parsed, context) => {
|
|
305
|
+
const events = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
306
|
+
for (const item of events) {
|
|
307
|
+
const nextRunId = config.resolveRunId?.(item) ?? null;
|
|
308
|
+
if (nextRunId) {
|
|
309
|
+
resolvedRunId = nextRunId;
|
|
310
|
+
context.setRunId(nextRunId);
|
|
311
|
+
}
|
|
312
|
+
context.emit(item);
|
|
313
|
+
if (item.type === "done") {
|
|
314
|
+
context.finish();
|
|
315
|
+
} else if (item.type === "error") {
|
|
316
|
+
context.fail(
|
|
317
|
+
new Error(
|
|
318
|
+
"message" in item && typeof item.message === "string" ? item.message : "Stream error"
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
resume: ({ context, runId }) => {
|
|
326
|
+
context.setRunId(runId);
|
|
327
|
+
context.ensureAssistantMessage();
|
|
328
|
+
const socket = new WebSocket(
|
|
329
|
+
typeof config.url === "function" ? config.url(runId) : config.url,
|
|
330
|
+
config.protocols
|
|
331
|
+
);
|
|
332
|
+
let manuallyClosed = false;
|
|
333
|
+
let sentResumePayload = false;
|
|
334
|
+
const sendResumePayload = () => {
|
|
335
|
+
if (sentResumePayload || socket.readyState !== WebSocket.OPEN) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
sentResumePayload = true;
|
|
339
|
+
const payload = config.buildResumePayload?.(runId) ?? { [config.runIdKey ?? "runId"]: runId };
|
|
340
|
+
socket.send(serializeMessage(payload));
|
|
341
|
+
};
|
|
342
|
+
socket.onopen = sendResumePayload;
|
|
343
|
+
socket.onmessage = (event) => {
|
|
344
|
+
try {
|
|
345
|
+
applyIncomingEvents(parseMessage(event.data), context);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
context.fail(toError(error));
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
socket.onerror = () => {
|
|
351
|
+
context.fail(new Error("WebSocket connection failed"));
|
|
352
|
+
};
|
|
353
|
+
socket.onclose = () => {
|
|
354
|
+
if (!manuallyClosed) {
|
|
355
|
+
context.finish();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
queueMicrotask(sendResumePayload);
|
|
359
|
+
return {
|
|
360
|
+
close: () => {
|
|
361
|
+
manuallyClosed = true;
|
|
362
|
+
socket.close();
|
|
363
|
+
},
|
|
364
|
+
stop: () => {
|
|
365
|
+
manuallyClosed = true;
|
|
366
|
+
const stopRunId = resolvedRunId ?? runId;
|
|
367
|
+
if (stopRunId && config.cancelUrl) {
|
|
368
|
+
void fetch(resolveUrl(config.cancelUrl, stopRunId), {
|
|
369
|
+
method: "POST"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
socket.close();
|
|
373
|
+
},
|
|
374
|
+
runId
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
start: ({ context, message }) => {
|
|
378
|
+
const runId = config.runId ?? config.getResumeKey?.() ?? null;
|
|
379
|
+
const socket = new WebSocket(
|
|
380
|
+
typeof config.url === "function" ? config.url(runId) : config.url,
|
|
381
|
+
config.protocols
|
|
382
|
+
);
|
|
383
|
+
let manuallyClosed = false;
|
|
384
|
+
let sentStartPayload = false;
|
|
385
|
+
const sendStartPayload = () => {
|
|
386
|
+
if (sentStartPayload || socket.readyState !== WebSocket.OPEN) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
sentStartPayload = true;
|
|
390
|
+
context.ensureAssistantMessage();
|
|
391
|
+
const payload = {
|
|
392
|
+
message,
|
|
393
|
+
...config.body
|
|
394
|
+
};
|
|
395
|
+
if (runId) {
|
|
396
|
+
payload[config.runIdKey ?? "runId"] = runId;
|
|
397
|
+
}
|
|
398
|
+
socket.send(serializeMessage(payload));
|
|
399
|
+
};
|
|
400
|
+
socket.onopen = sendStartPayload;
|
|
401
|
+
socket.onmessage = (event) => {
|
|
402
|
+
try {
|
|
403
|
+
applyIncomingEvents(parseMessage(event.data), context);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
context.fail(toError(error));
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
socket.onerror = () => {
|
|
409
|
+
context.fail(new Error("WebSocket connection failed"));
|
|
410
|
+
};
|
|
411
|
+
socket.onclose = () => {
|
|
412
|
+
if (!manuallyClosed) {
|
|
413
|
+
context.finish();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
queueMicrotask(sendStartPayload);
|
|
417
|
+
return {
|
|
418
|
+
close: () => {
|
|
419
|
+
manuallyClosed = true;
|
|
420
|
+
socket.close();
|
|
421
|
+
},
|
|
422
|
+
stop: () => {
|
|
423
|
+
manuallyClosed = true;
|
|
424
|
+
const stopRunId = resolvedRunId ?? runId;
|
|
425
|
+
if (stopRunId && config.cancelUrl) {
|
|
426
|
+
void fetch(resolveUrl(config.cancelUrl, stopRunId), {
|
|
427
|
+
method: "POST"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
socket.close();
|
|
431
|
+
},
|
|
432
|
+
runId
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function resolveTransport(options) {
|
|
438
|
+
if (typeof options.transport === "object" && options.transport) {
|
|
439
|
+
return options.transport;
|
|
440
|
+
}
|
|
441
|
+
const transportKind = options.transport ?? "sse";
|
|
442
|
+
if (transportKind === "background-sse") {
|
|
443
|
+
const config = options.transportOptions?.backgroundSSE;
|
|
444
|
+
if (!config) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
'`transportOptions.backgroundSSE` is required when transport is "background-sse"'
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return createBackgroundSSETransport(config);
|
|
450
|
+
}
|
|
451
|
+
if (transportKind === "websocket") {
|
|
452
|
+
const config = options.transportOptions?.websocket;
|
|
453
|
+
if (!config) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
'`transportOptions.websocket` is required when transport is "websocket"'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return createWebSocketTransport(config);
|
|
459
|
+
}
|
|
460
|
+
const sseConfig = options.transportOptions?.sse ?? {
|
|
461
|
+
api: options.api,
|
|
462
|
+
body: options.body,
|
|
463
|
+
headers: options.headers
|
|
464
|
+
};
|
|
465
|
+
if (!sseConfig.api) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
"`api` or `transportOptions.sse.api` is required when using the default SSE transport"
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return createDirectSSETransport({
|
|
471
|
+
...sseConfig,
|
|
472
|
+
api: sseConfig.api
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/use-stream-chat.ts
|
|
97
477
|
function defaultOnEvent(event, helpers) {
|
|
98
478
|
if (event.type === "text_delta") {
|
|
99
479
|
helpers.appendText(event.delta);
|
|
100
480
|
}
|
|
101
481
|
}
|
|
102
482
|
function useStreamChat(options) {
|
|
103
|
-
const {
|
|
104
|
-
|
|
105
|
-
headers,
|
|
106
|
-
body,
|
|
107
|
-
initialMessages,
|
|
108
|
-
onEvent,
|
|
109
|
-
onMessage,
|
|
110
|
-
onError,
|
|
111
|
-
onFinish
|
|
112
|
-
} = options;
|
|
113
|
-
const [messages, setMessages] = useState2(
|
|
114
|
-
initialMessages ?? []
|
|
115
|
-
);
|
|
483
|
+
const { initialMessages, onEvent, onMessage, onError, onFinish } = options;
|
|
484
|
+
const [messages, setMessages] = useState2(initialMessages ?? []);
|
|
116
485
|
const [isLoading, setIsLoading] = useState2(false);
|
|
117
486
|
const [error, setError] = useState2(null);
|
|
118
|
-
const
|
|
487
|
+
const [runId, setRunId] = useState2(null);
|
|
488
|
+
const runRef = useRef2(null);
|
|
489
|
+
const resumedRunIdRef = useRef2(null);
|
|
490
|
+
const manuallyStoppedRef = useRef2(false);
|
|
119
491
|
const messagesRef = useRef2(messages);
|
|
120
492
|
messagesRef.current = messages;
|
|
493
|
+
const transportOptionsRef = useRef2(options.transportOptions);
|
|
494
|
+
transportOptionsRef.current = options.transportOptions;
|
|
121
495
|
const appendText = useCallback2((delta) => {
|
|
122
496
|
setMessages((prev) => {
|
|
123
497
|
const last = prev[prev.length - 1];
|
|
@@ -133,29 +507,69 @@ function useStreamChat(options) {
|
|
|
133
507
|
} else {
|
|
134
508
|
parts.push({ type: "text", text: delta });
|
|
135
509
|
}
|
|
136
|
-
|
|
137
|
-
return [...prev.slice(0, -1), updated];
|
|
510
|
+
return [...prev.slice(0, -1), { ...last, parts }];
|
|
138
511
|
});
|
|
139
512
|
}, []);
|
|
140
513
|
const appendPart = useCallback2((part) => {
|
|
141
514
|
setMessages((prev) => {
|
|
142
515
|
const last = prev[prev.length - 1];
|
|
143
516
|
if (!last || last.role !== "assistant") return prev;
|
|
144
|
-
|
|
145
|
-
...
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
517
|
+
return [
|
|
518
|
+
...prev.slice(0, -1),
|
|
519
|
+
{
|
|
520
|
+
...last,
|
|
521
|
+
parts: [...last.parts, part]
|
|
522
|
+
}
|
|
523
|
+
];
|
|
149
524
|
});
|
|
150
525
|
}, []);
|
|
526
|
+
const transportRef = useRef2(null);
|
|
527
|
+
if (!transportRef.current) {
|
|
528
|
+
transportRef.current = resolveTransport(options);
|
|
529
|
+
}
|
|
530
|
+
const transport = transportRef.current;
|
|
531
|
+
const eventHandler = onEvent ?? defaultOnEvent;
|
|
532
|
+
const eventHandlerRef = useRef2(eventHandler);
|
|
533
|
+
eventHandlerRef.current = eventHandler;
|
|
534
|
+
const onErrorRef = useRef2(onError);
|
|
535
|
+
onErrorRef.current = onError;
|
|
536
|
+
const onFinishRef = useRef2(onFinish);
|
|
537
|
+
onFinishRef.current = onFinish;
|
|
538
|
+
const onMessageRef = useRef2(onMessage);
|
|
539
|
+
onMessageRef.current = onMessage;
|
|
540
|
+
const transportContext = useMemo(
|
|
541
|
+
() => createChatTransportContext({
|
|
542
|
+
appendPart,
|
|
543
|
+
appendText,
|
|
544
|
+
eventHandler: (event, helpers) => eventHandlerRef.current(event, helpers),
|
|
545
|
+
getMessages: () => messagesRef.current,
|
|
546
|
+
onError: (...args) => onErrorRef.current?.(...args),
|
|
547
|
+
onFinish: (...args) => onFinishRef.current?.(...args),
|
|
548
|
+
onMessage: (...args) => onMessageRef.current?.(...args),
|
|
549
|
+
setError,
|
|
550
|
+
setIsLoading,
|
|
551
|
+
setMessages,
|
|
552
|
+
setRunId: (next) => {
|
|
553
|
+
setRunId(next);
|
|
554
|
+
transportOptionsRef.current?.backgroundSSE?.onRunIdChange?.(next);
|
|
555
|
+
transportOptionsRef.current?.websocket?.onRunIdChange?.(next);
|
|
556
|
+
}
|
|
557
|
+
}),
|
|
558
|
+
[appendPart, appendText]
|
|
559
|
+
);
|
|
151
560
|
const stop = useCallback2(() => {
|
|
152
|
-
|
|
153
|
-
|
|
561
|
+
const activeRun = runRef.current;
|
|
562
|
+
if (!activeRun?.stop) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
manuallyStoppedRef.current = true;
|
|
566
|
+
void activeRun.stop();
|
|
567
|
+
runRef.current = null;
|
|
154
568
|
setIsLoading(false);
|
|
155
569
|
}, []);
|
|
156
570
|
const sendMessage = useCallback2(
|
|
157
571
|
(text) => {
|
|
158
|
-
if (
|
|
572
|
+
if (runRef.current || isLoading) {
|
|
159
573
|
return;
|
|
160
574
|
}
|
|
161
575
|
const userMessage = createMessage("user", [
|
|
@@ -170,85 +584,64 @@ function useStreamChat(options) {
|
|
|
170
584
|
onMessage?.(userMessage);
|
|
171
585
|
setError(null);
|
|
172
586
|
setIsLoading(true);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
181
|
-
(async () => {
|
|
182
|
-
try {
|
|
183
|
-
const response = await fetch(api, {
|
|
184
|
-
method: "POST",
|
|
185
|
-
headers: {
|
|
186
|
-
"Content-Type": "application/json",
|
|
187
|
-
...headers
|
|
188
|
-
},
|
|
189
|
-
body: JSON.stringify({ message: text, ...body }),
|
|
190
|
-
signal: controller.signal
|
|
191
|
-
});
|
|
192
|
-
if (!response.ok) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`SSE request failed: ${response.status} ${response.statusText}`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
if (!response.body) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
"Response body is null \u2014 SSE streaming not supported"
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
for await (const event of parseSSEStream(
|
|
203
|
-
response.body,
|
|
204
|
-
controller.signal
|
|
205
|
-
)) {
|
|
206
|
-
eventHandler(event, helpers);
|
|
207
|
-
}
|
|
208
|
-
const finalMessages = messagesRef.current;
|
|
209
|
-
const lastMessage = finalMessages[finalMessages.length - 1];
|
|
210
|
-
if (lastMessage?.role === "assistant") {
|
|
211
|
-
onMessage?.(lastMessage);
|
|
212
|
-
}
|
|
213
|
-
onFinish?.(finalMessages);
|
|
214
|
-
} catch (err) {
|
|
215
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
219
|
-
setError(error2);
|
|
220
|
-
onError?.(error2);
|
|
221
|
-
} finally {
|
|
222
|
-
abortRef.current = null;
|
|
223
|
-
setIsLoading(false);
|
|
224
|
-
}
|
|
225
|
-
})();
|
|
587
|
+
resumedRunIdRef.current = null;
|
|
588
|
+
manuallyStoppedRef.current = false;
|
|
589
|
+
const run = transport.start({ context: transportContext, message: text });
|
|
590
|
+
runRef.current = run ?? null;
|
|
591
|
+
if (run?.runId) {
|
|
592
|
+
setRunId(run.runId);
|
|
593
|
+
}
|
|
226
594
|
},
|
|
227
|
-
[
|
|
228
|
-
api,
|
|
229
|
-
headers,
|
|
230
|
-
body,
|
|
231
|
-
onEvent,
|
|
232
|
-
onMessage,
|
|
233
|
-
onError,
|
|
234
|
-
onFinish,
|
|
235
|
-
appendText,
|
|
236
|
-
appendPart
|
|
237
|
-
]
|
|
595
|
+
[isLoading, onMessage, transport, transportContext]
|
|
238
596
|
);
|
|
597
|
+
const candidateRunId = options.transportOptions?.backgroundSSE?.runId ?? options.transportOptions?.backgroundSSE?.getResumeKey?.() ?? options.transportOptions?.websocket?.runId ?? options.transportOptions?.websocket?.getResumeKey?.() ?? null;
|
|
598
|
+
useEffect2(() => {
|
|
599
|
+
if (runRef.current) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!candidateRunId) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (manuallyStoppedRef.current) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (resumedRunIdRef.current === candidateRunId) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (!transport.resume) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
resumedRunIdRef.current = candidateRunId;
|
|
615
|
+
setError(null);
|
|
616
|
+
setIsLoading(true);
|
|
617
|
+
const run = transport.resume({
|
|
618
|
+
context: transportContext,
|
|
619
|
+
runId: candidateRunId
|
|
620
|
+
});
|
|
621
|
+
runRef.current = run ?? null;
|
|
622
|
+
setRunId(candidateRunId);
|
|
623
|
+
}, [candidateRunId, transport, transportContext]);
|
|
239
624
|
useEffect2(() => {
|
|
240
625
|
return () => {
|
|
241
|
-
|
|
242
|
-
|
|
626
|
+
void runRef.current?.close?.();
|
|
627
|
+
runRef.current = null;
|
|
243
628
|
};
|
|
244
629
|
}, []);
|
|
630
|
+
const prevIsLoadingRef = useRef2(isLoading);
|
|
631
|
+
useEffect2(() => {
|
|
632
|
+
if (prevIsLoadingRef.current && !isLoading) {
|
|
633
|
+
runRef.current = null;
|
|
634
|
+
}
|
|
635
|
+
prevIsLoadingRef.current = isLoading;
|
|
636
|
+
}, [isLoading]);
|
|
245
637
|
return {
|
|
246
|
-
messages,
|
|
247
|
-
isLoading,
|
|
248
638
|
error,
|
|
639
|
+
isLoading,
|
|
640
|
+
messages,
|
|
641
|
+
runId,
|
|
249
642
|
sendMessage,
|
|
250
|
-
|
|
251
|
-
|
|
643
|
+
setMessages,
|
|
644
|
+
stop
|
|
252
645
|
};
|
|
253
646
|
}
|
|
254
647
|
export {
|