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