@deltakit/react 0.2.0 → 0.2.3
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 +587 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -6
- package/dist/index.d.ts +151 -6
- package/dist/index.js +588 -102
- package/dist/index.js.map +1 -1
- package/package.json +56 -49
package/dist/index.cjs
CHANGED
|
@@ -42,25 +42,70 @@ function useAutoScroll(dependencies, options) {
|
|
|
42
42
|
const isAtBottomRef = (0, import_react.useRef)(true);
|
|
43
43
|
const [isAtBottom, setIsAtBottom] = (0, import_react.useState)(true);
|
|
44
44
|
const rafRef = (0, import_react.useRef)(null);
|
|
45
|
+
const smoothRafRef = (0, import_react.useRef)(null);
|
|
46
|
+
const smoothTargetRef = (0, import_react.useRef)(null);
|
|
47
|
+
const lastAutoScrollHeightRef = (0, import_react.useRef)(null);
|
|
48
|
+
const cancelSmoothScroll = (0, import_react.useCallback)(() => {
|
|
49
|
+
if (smoothRafRef.current != null) {
|
|
50
|
+
cancelAnimationFrame(smoothRafRef.current);
|
|
51
|
+
smoothRafRef.current = null;
|
|
52
|
+
}
|
|
53
|
+
smoothTargetRef.current = null;
|
|
54
|
+
}, []);
|
|
55
|
+
const runSmoothScroll = (0, import_react.useCallback)(() => {
|
|
56
|
+
if (smoothRafRef.current != null) return;
|
|
57
|
+
const tick = () => {
|
|
58
|
+
const el = ref.current;
|
|
59
|
+
if (!el || !isAtBottomRef.current) {
|
|
60
|
+
cancelSmoothScroll();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const target = smoothTargetRef.current ?? el.scrollHeight;
|
|
64
|
+
const delta = target - el.scrollTop;
|
|
65
|
+
if (delta <= 1) {
|
|
66
|
+
el.scrollTop = target;
|
|
67
|
+
lastAutoScrollHeightRef.current = target;
|
|
68
|
+
smoothRafRef.current = null;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const step = Math.min(Math.max(delta * 0.35, 12), 120);
|
|
72
|
+
el.scrollTop = Math.min(target, el.scrollTop + step);
|
|
73
|
+
smoothRafRef.current = requestAnimationFrame(tick);
|
|
74
|
+
};
|
|
75
|
+
smoothRafRef.current = requestAnimationFrame(tick);
|
|
76
|
+
}, [cancelSmoothScroll]);
|
|
45
77
|
const scheduleScroll = (0, import_react.useCallback)(() => {
|
|
46
78
|
if (rafRef.current != null) return;
|
|
47
79
|
rafRef.current = requestAnimationFrame(() => {
|
|
48
80
|
rafRef.current = null;
|
|
49
81
|
const el = ref.current;
|
|
50
|
-
if (el
|
|
51
|
-
|
|
82
|
+
if (!el || !isAtBottomRef.current) return;
|
|
83
|
+
const nextHeight = el.scrollHeight;
|
|
84
|
+
if (lastAutoScrollHeightRef.current === nextHeight) return;
|
|
85
|
+
lastAutoScrollHeightRef.current = nextHeight;
|
|
86
|
+
if (behavior === "smooth") {
|
|
87
|
+
smoothTargetRef.current = nextHeight;
|
|
88
|
+
runSmoothScroll();
|
|
89
|
+
return;
|
|
52
90
|
}
|
|
91
|
+
cancelSmoothScroll();
|
|
92
|
+
el.scrollTop = nextHeight;
|
|
53
93
|
});
|
|
54
|
-
}, [behavior]);
|
|
94
|
+
}, [behavior, cancelSmoothScroll, runSmoothScroll]);
|
|
55
95
|
(0, import_react.useEffect)(() => {
|
|
56
96
|
const el = ref.current;
|
|
57
97
|
if (!el || !enabled) return;
|
|
58
98
|
const handleScroll = () => {
|
|
59
99
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
|
|
60
100
|
isAtBottomRef.current = atBottom;
|
|
101
|
+
if (!atBottom) {
|
|
102
|
+
lastAutoScrollHeightRef.current = null;
|
|
103
|
+
cancelSmoothScroll();
|
|
104
|
+
}
|
|
61
105
|
setIsAtBottom((prev) => prev === atBottom ? prev : atBottom);
|
|
62
106
|
};
|
|
63
107
|
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
108
|
+
handleScroll();
|
|
64
109
|
return () => el.removeEventListener("scroll", handleScroll);
|
|
65
110
|
}, [enabled, threshold]);
|
|
66
111
|
(0, import_react.useEffect)(() => {
|
|
@@ -96,21 +141,25 @@ function useAutoScroll(dependencies, options) {
|
|
|
96
141
|
cancelAnimationFrame(rafRef.current);
|
|
97
142
|
rafRef.current = null;
|
|
98
143
|
}
|
|
144
|
+
cancelSmoothScroll();
|
|
99
145
|
};
|
|
100
|
-
}, []);
|
|
146
|
+
}, [cancelSmoothScroll]);
|
|
101
147
|
const scrollToBottom = (0, import_react.useCallback)(() => {
|
|
102
148
|
const el = ref.current;
|
|
103
149
|
if (!el) return;
|
|
150
|
+
cancelSmoothScroll();
|
|
104
151
|
isAtBottomRef.current = true;
|
|
152
|
+
lastAutoScrollHeightRef.current = el.scrollHeight;
|
|
105
153
|
setIsAtBottom(true);
|
|
106
154
|
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
107
|
-
}, [behavior]);
|
|
155
|
+
}, [behavior, cancelSmoothScroll]);
|
|
108
156
|
return { ref, scrollToBottom, isAtBottom };
|
|
109
157
|
}
|
|
110
158
|
|
|
111
159
|
// src/use-stream-chat.ts
|
|
112
|
-
var import_core = require("@deltakit/core");
|
|
113
160
|
var import_react2 = require("react");
|
|
161
|
+
|
|
162
|
+
// src/chat-controller.ts
|
|
114
163
|
var counter = 0;
|
|
115
164
|
function generateId() {
|
|
116
165
|
return `msg_${Date.now()}_${++counter}`;
|
|
@@ -118,6 +167,385 @@ function generateId() {
|
|
|
118
167
|
function createMessage(role, parts) {
|
|
119
168
|
return { id: generateId(), role, parts };
|
|
120
169
|
}
|
|
170
|
+
function createChatTransportContext(options) {
|
|
171
|
+
const helpers = {
|
|
172
|
+
appendPart: options.appendPart,
|
|
173
|
+
appendText: options.appendText,
|
|
174
|
+
setMessages: options.setMessages
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
emit: (event) => {
|
|
178
|
+
options.eventHandler(event, helpers);
|
|
179
|
+
},
|
|
180
|
+
ensureAssistantMessage: () => {
|
|
181
|
+
options.setMessages((prev) => {
|
|
182
|
+
const last = prev[prev.length - 1];
|
|
183
|
+
if (last?.role === "assistant") {
|
|
184
|
+
return prev;
|
|
185
|
+
}
|
|
186
|
+
return [...prev, createMessage("assistant", [])];
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
fail: (error) => {
|
|
190
|
+
options.setError(error);
|
|
191
|
+
options.onError?.(error);
|
|
192
|
+
options.setIsLoading(false);
|
|
193
|
+
options.setRunId(null);
|
|
194
|
+
},
|
|
195
|
+
finish: () => {
|
|
196
|
+
options.setIsLoading(false);
|
|
197
|
+
options.setRunId(null);
|
|
198
|
+
const finalMessages = options.getMessages();
|
|
199
|
+
const lastMessage = finalMessages[finalMessages.length - 1];
|
|
200
|
+
if (lastMessage?.role === "assistant") {
|
|
201
|
+
options.onMessage?.(lastMessage);
|
|
202
|
+
}
|
|
203
|
+
options.onFinish?.(finalMessages);
|
|
204
|
+
},
|
|
205
|
+
getMessages: options.getMessages,
|
|
206
|
+
setRunId: (runId) => {
|
|
207
|
+
options.setRunId(runId);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/transports.ts
|
|
213
|
+
var import_core = require("@deltakit/core");
|
|
214
|
+
function toError(error) {
|
|
215
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
216
|
+
}
|
|
217
|
+
function isAbortError(error) {
|
|
218
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
219
|
+
}
|
|
220
|
+
function resolveRunId(response) {
|
|
221
|
+
if (!response || typeof response !== "object") {
|
|
222
|
+
throw new Error("Background SSE start response did not contain a run id");
|
|
223
|
+
}
|
|
224
|
+
const maybeRunId = "runId" in response ? response.runId : "job_id" in response ? response.job_id : null;
|
|
225
|
+
if (typeof maybeRunId !== "string" || maybeRunId.length === 0) {
|
|
226
|
+
throw new Error("Background SSE start response did not contain a run id");
|
|
227
|
+
}
|
|
228
|
+
return maybeRunId;
|
|
229
|
+
}
|
|
230
|
+
function resolveUrl(url, runId) {
|
|
231
|
+
return typeof url === "function" ? url(runId) : url.replace(":runId", runId);
|
|
232
|
+
}
|
|
233
|
+
async function streamFetchSSE(response, context, signal) {
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`SSE request failed: ${response.status} ${response.statusText}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (!response.body) {
|
|
240
|
+
throw new Error("Response body is null \u2014 SSE streaming not supported");
|
|
241
|
+
}
|
|
242
|
+
for await (const event of (0, import_core.parseSSEStream)(response.body, signal)) {
|
|
243
|
+
context.emit(event);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function createDirectSSETransport(config) {
|
|
247
|
+
return {
|
|
248
|
+
start: ({ context, message }) => {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const run = {
|
|
251
|
+
close: () => {
|
|
252
|
+
controller.abort();
|
|
253
|
+
},
|
|
254
|
+
stop: () => {
|
|
255
|
+
controller.abort();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
259
|
+
void (async () => {
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetchImpl(config.api, {
|
|
262
|
+
body: JSON.stringify({ message, ...config.body }),
|
|
263
|
+
headers: {
|
|
264
|
+
"Content-Type": "application/json",
|
|
265
|
+
...config.headers
|
|
266
|
+
},
|
|
267
|
+
method: config.method ?? "POST",
|
|
268
|
+
signal: controller.signal
|
|
269
|
+
});
|
|
270
|
+
await streamFetchSSE(response, context, controller.signal);
|
|
271
|
+
context.finish();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!isAbortError(error)) {
|
|
274
|
+
context.fail(toError(error));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})();
|
|
278
|
+
return run;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function createBackgroundSSETransport(config) {
|
|
283
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
284
|
+
const connect = (runId, context) => {
|
|
285
|
+
const controller = new AbortController();
|
|
286
|
+
void (async () => {
|
|
287
|
+
try {
|
|
288
|
+
context.ensureAssistantMessage();
|
|
289
|
+
const response = await fetchImpl(resolveUrl(config.eventsApi, runId), {
|
|
290
|
+
headers: config.eventHeaders,
|
|
291
|
+
method: "GET",
|
|
292
|
+
signal: controller.signal
|
|
293
|
+
});
|
|
294
|
+
await streamFetchSSE(response, context, controller.signal);
|
|
295
|
+
context.finish();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!isAbortError(error)) {
|
|
298
|
+
context.fail(toError(error));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
})();
|
|
302
|
+
return {
|
|
303
|
+
close: () => {
|
|
304
|
+
controller.abort();
|
|
305
|
+
},
|
|
306
|
+
stop: () => {
|
|
307
|
+
controller.abort();
|
|
308
|
+
if (config.cancelApi) {
|
|
309
|
+
void fetchImpl(resolveUrl(config.cancelApi, runId), {
|
|
310
|
+
method: "POST"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
runId
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
return {
|
|
318
|
+
resume: ({ context, runId }) => {
|
|
319
|
+
context.setRunId(runId);
|
|
320
|
+
return connect(runId, context);
|
|
321
|
+
},
|
|
322
|
+
start: ({ context, message }) => {
|
|
323
|
+
const startController = new AbortController();
|
|
324
|
+
let activeRun;
|
|
325
|
+
void (async () => {
|
|
326
|
+
try {
|
|
327
|
+
const response = await fetchImpl(config.startApi, {
|
|
328
|
+
body: JSON.stringify({ message, ...config.startBody }),
|
|
329
|
+
headers: {
|
|
330
|
+
"Content-Type": "application/json",
|
|
331
|
+
...config.startHeaders
|
|
332
|
+
},
|
|
333
|
+
method: config.startMethod ?? "POST",
|
|
334
|
+
signal: startController.signal
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Background SSE start failed: ${response.status} ${response.statusText}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
const runId = (config.resolveRunId ?? resolveRunId)(data);
|
|
343
|
+
context.setRunId(runId);
|
|
344
|
+
activeRun = connect(runId, context);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!isAbortError(error)) {
|
|
347
|
+
context.fail(toError(error));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
})();
|
|
351
|
+
return {
|
|
352
|
+
close: () => {
|
|
353
|
+
startController.abort();
|
|
354
|
+
void activeRun?.close?.();
|
|
355
|
+
},
|
|
356
|
+
stop: () => {
|
|
357
|
+
startController.abort();
|
|
358
|
+
activeRun?.stop?.();
|
|
359
|
+
},
|
|
360
|
+
runId: null
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function defaultParseWebSocketMessage(data) {
|
|
366
|
+
if (typeof data !== "string") {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const parsed = JSON.parse(data);
|
|
370
|
+
return parsed;
|
|
371
|
+
}
|
|
372
|
+
function createWebSocketTransport(config) {
|
|
373
|
+
const parseMessage = config.parseMessage ?? defaultParseWebSocketMessage;
|
|
374
|
+
const serializeMessage = config.serializeMessage ?? JSON.stringify;
|
|
375
|
+
let resolvedRunId = null;
|
|
376
|
+
const applyIncomingEvents = (parsed, context) => {
|
|
377
|
+
const events = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
378
|
+
for (const item of events) {
|
|
379
|
+
const nextRunId = config.resolveRunId?.(item) ?? null;
|
|
380
|
+
if (nextRunId) {
|
|
381
|
+
resolvedRunId = nextRunId;
|
|
382
|
+
context.setRunId(nextRunId);
|
|
383
|
+
}
|
|
384
|
+
context.emit(item);
|
|
385
|
+
if (item.type === "done") {
|
|
386
|
+
context.finish();
|
|
387
|
+
} else if (item.type === "error") {
|
|
388
|
+
context.fail(
|
|
389
|
+
new Error(
|
|
390
|
+
"message" in item && typeof item.message === "string" ? item.message : "Stream error"
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
resume: ({ context, runId }) => {
|
|
398
|
+
context.setRunId(runId);
|
|
399
|
+
context.ensureAssistantMessage();
|
|
400
|
+
const socket = new WebSocket(
|
|
401
|
+
typeof config.url === "function" ? config.url(runId) : config.url,
|
|
402
|
+
config.protocols
|
|
403
|
+
);
|
|
404
|
+
let manuallyClosed = false;
|
|
405
|
+
let sentResumePayload = false;
|
|
406
|
+
const sendResumePayload = () => {
|
|
407
|
+
if (sentResumePayload || socket.readyState !== WebSocket.OPEN) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
sentResumePayload = true;
|
|
411
|
+
const payload = config.buildResumePayload?.(runId) ?? { [config.runIdKey ?? "runId"]: runId };
|
|
412
|
+
socket.send(serializeMessage(payload));
|
|
413
|
+
};
|
|
414
|
+
socket.onopen = sendResumePayload;
|
|
415
|
+
socket.onmessage = (event) => {
|
|
416
|
+
try {
|
|
417
|
+
applyIncomingEvents(parseMessage(event.data), context);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
context.fail(toError(error));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
socket.onerror = () => {
|
|
423
|
+
context.fail(new Error("WebSocket connection failed"));
|
|
424
|
+
};
|
|
425
|
+
socket.onclose = () => {
|
|
426
|
+
if (!manuallyClosed) {
|
|
427
|
+
context.finish();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
queueMicrotask(sendResumePayload);
|
|
431
|
+
return {
|
|
432
|
+
close: () => {
|
|
433
|
+
manuallyClosed = true;
|
|
434
|
+
socket.close();
|
|
435
|
+
},
|
|
436
|
+
stop: () => {
|
|
437
|
+
manuallyClosed = true;
|
|
438
|
+
const stopRunId = resolvedRunId ?? runId;
|
|
439
|
+
if (stopRunId && config.cancelUrl) {
|
|
440
|
+
void fetch(resolveUrl(config.cancelUrl, stopRunId), {
|
|
441
|
+
method: "POST"
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
socket.close();
|
|
445
|
+
},
|
|
446
|
+
runId
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
start: ({ context, message }) => {
|
|
450
|
+
const runId = config.runId ?? config.getResumeKey?.() ?? null;
|
|
451
|
+
const socket = new WebSocket(
|
|
452
|
+
typeof config.url === "function" ? config.url(runId) : config.url,
|
|
453
|
+
config.protocols
|
|
454
|
+
);
|
|
455
|
+
let manuallyClosed = false;
|
|
456
|
+
let sentStartPayload = false;
|
|
457
|
+
const sendStartPayload = () => {
|
|
458
|
+
if (sentStartPayload || socket.readyState !== WebSocket.OPEN) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
sentStartPayload = true;
|
|
462
|
+
context.ensureAssistantMessage();
|
|
463
|
+
const payload = {
|
|
464
|
+
message,
|
|
465
|
+
...config.body
|
|
466
|
+
};
|
|
467
|
+
if (runId) {
|
|
468
|
+
payload[config.runIdKey ?? "runId"] = runId;
|
|
469
|
+
}
|
|
470
|
+
socket.send(serializeMessage(payload));
|
|
471
|
+
};
|
|
472
|
+
socket.onopen = sendStartPayload;
|
|
473
|
+
socket.onmessage = (event) => {
|
|
474
|
+
try {
|
|
475
|
+
applyIncomingEvents(parseMessage(event.data), context);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
context.fail(toError(error));
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
socket.onerror = () => {
|
|
481
|
+
context.fail(new Error("WebSocket connection failed"));
|
|
482
|
+
};
|
|
483
|
+
socket.onclose = () => {
|
|
484
|
+
if (!manuallyClosed) {
|
|
485
|
+
context.finish();
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
queueMicrotask(sendStartPayload);
|
|
489
|
+
return {
|
|
490
|
+
close: () => {
|
|
491
|
+
manuallyClosed = true;
|
|
492
|
+
socket.close();
|
|
493
|
+
},
|
|
494
|
+
stop: () => {
|
|
495
|
+
manuallyClosed = true;
|
|
496
|
+
const stopRunId = resolvedRunId ?? runId;
|
|
497
|
+
if (stopRunId && config.cancelUrl) {
|
|
498
|
+
void fetch(resolveUrl(config.cancelUrl, stopRunId), {
|
|
499
|
+
method: "POST"
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
socket.close();
|
|
503
|
+
},
|
|
504
|
+
runId
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function resolveTransport(options) {
|
|
510
|
+
if (typeof options.transport === "object" && options.transport) {
|
|
511
|
+
return options.transport;
|
|
512
|
+
}
|
|
513
|
+
const transportKind = options.transport ?? "sse";
|
|
514
|
+
if (transportKind === "background-sse") {
|
|
515
|
+
const config = options.transportOptions?.backgroundSSE;
|
|
516
|
+
if (!config) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
'`transportOptions.backgroundSSE` is required when transport is "background-sse"'
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return createBackgroundSSETransport(config);
|
|
522
|
+
}
|
|
523
|
+
if (transportKind === "websocket") {
|
|
524
|
+
const config = options.transportOptions?.websocket;
|
|
525
|
+
if (!config) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
'`transportOptions.websocket` is required when transport is "websocket"'
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return createWebSocketTransport(config);
|
|
531
|
+
}
|
|
532
|
+
const sseConfig = options.transportOptions?.sse ?? {
|
|
533
|
+
api: options.api,
|
|
534
|
+
body: options.body,
|
|
535
|
+
headers: options.headers
|
|
536
|
+
};
|
|
537
|
+
if (!sseConfig.api) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"`api` or `transportOptions.sse.api` is required when using the default SSE transport"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return createDirectSSETransport({
|
|
543
|
+
...sseConfig,
|
|
544
|
+
api: sseConfig.api
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/use-stream-chat.ts
|
|
121
549
|
function defaultOnEvent(event, helpers) {
|
|
122
550
|
if (event.type === "text_delta") {
|
|
123
551
|
helpers.appendText(event.delta);
|
|
@@ -125,25 +553,45 @@ function defaultOnEvent(event, helpers) {
|
|
|
125
553
|
}
|
|
126
554
|
function useStreamChat(options) {
|
|
127
555
|
const {
|
|
128
|
-
api,
|
|
129
|
-
headers,
|
|
130
|
-
body,
|
|
131
556
|
initialMessages,
|
|
132
557
|
onEvent,
|
|
133
558
|
onMessage,
|
|
134
559
|
onError,
|
|
135
|
-
onFinish
|
|
560
|
+
onFinish,
|
|
561
|
+
textBatchMs = 0
|
|
136
562
|
} = options;
|
|
137
|
-
const [messages, setMessages] = (0, import_react2.useState)(
|
|
138
|
-
initialMessages ?? []
|
|
139
|
-
);
|
|
563
|
+
const [messages, setMessages] = (0, import_react2.useState)(initialMessages ?? []);
|
|
140
564
|
const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
|
|
141
565
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
142
|
-
const
|
|
566
|
+
const [runId, setRunId] = (0, import_react2.useState)(null);
|
|
567
|
+
const runRef = (0, import_react2.useRef)(null);
|
|
568
|
+
const resumedRunIdRef = (0, import_react2.useRef)(null);
|
|
569
|
+
const manuallyStoppedRef = (0, import_react2.useRef)(false);
|
|
143
570
|
const messagesRef = (0, import_react2.useRef)(messages);
|
|
144
571
|
messagesRef.current = messages;
|
|
145
|
-
const
|
|
146
|
-
|
|
572
|
+
const updateMessages = (0, import_react2.useCallback)(
|
|
573
|
+
(next) => {
|
|
574
|
+
setMessages((prev) => {
|
|
575
|
+
const resolved = typeof next === "function" ? next(prev) : next;
|
|
576
|
+
messagesRef.current = resolved;
|
|
577
|
+
return resolved;
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
[]
|
|
581
|
+
);
|
|
582
|
+
const transportOptionsRef = (0, import_react2.useRef)(options.transportOptions);
|
|
583
|
+
transportOptionsRef.current = options.transportOptions;
|
|
584
|
+
const pendingTextRef = (0, import_react2.useRef)("");
|
|
585
|
+
const textFlushTimerRef = (0, import_react2.useRef)(null);
|
|
586
|
+
const flushPendingText = (0, import_react2.useCallback)(() => {
|
|
587
|
+
if (textFlushTimerRef.current !== null) {
|
|
588
|
+
clearTimeout(textFlushTimerRef.current);
|
|
589
|
+
textFlushTimerRef.current = null;
|
|
590
|
+
}
|
|
591
|
+
const delta = pendingTextRef.current;
|
|
592
|
+
if (!delta) return;
|
|
593
|
+
pendingTextRef.current = "";
|
|
594
|
+
updateMessages((prev) => {
|
|
147
595
|
const last = prev[prev.length - 1];
|
|
148
596
|
if (!last || last.role !== "assistant") return prev;
|
|
149
597
|
const parts = [...last.parts];
|
|
@@ -157,122 +605,160 @@ function useStreamChat(options) {
|
|
|
157
605
|
} else {
|
|
158
606
|
parts.push({ type: "text", text: delta });
|
|
159
607
|
}
|
|
160
|
-
|
|
161
|
-
return [...prev.slice(0, -1), updated];
|
|
608
|
+
return [...prev.slice(0, -1), { ...last, parts }];
|
|
162
609
|
});
|
|
163
|
-
}, []);
|
|
610
|
+
}, [updateMessages]);
|
|
611
|
+
const appendText = (0, import_react2.useCallback)((delta) => {
|
|
612
|
+
if (textBatchMs <= 0) {
|
|
613
|
+
pendingTextRef.current += delta;
|
|
614
|
+
flushPendingText();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
pendingTextRef.current += delta;
|
|
618
|
+
if (textFlushTimerRef.current !== null) return;
|
|
619
|
+
textFlushTimerRef.current = setTimeout(() => {
|
|
620
|
+
flushPendingText();
|
|
621
|
+
}, textBatchMs);
|
|
622
|
+
}, [flushPendingText, textBatchMs]);
|
|
164
623
|
const appendPart = (0, import_react2.useCallback)((part) => {
|
|
165
|
-
|
|
624
|
+
flushPendingText();
|
|
625
|
+
updateMessages((prev) => {
|
|
166
626
|
const last = prev[prev.length - 1];
|
|
167
627
|
if (!last || last.role !== "assistant") return prev;
|
|
168
|
-
|
|
169
|
-
...
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
628
|
+
return [
|
|
629
|
+
...prev.slice(0, -1),
|
|
630
|
+
{
|
|
631
|
+
...last,
|
|
632
|
+
parts: [...last.parts, part]
|
|
633
|
+
}
|
|
634
|
+
];
|
|
173
635
|
});
|
|
174
|
-
}, []);
|
|
636
|
+
}, [flushPendingText, updateMessages]);
|
|
637
|
+
const transportRef = (0, import_react2.useRef)(null);
|
|
638
|
+
if (!transportRef.current) {
|
|
639
|
+
transportRef.current = resolveTransport(options);
|
|
640
|
+
}
|
|
641
|
+
const transport = transportRef.current;
|
|
642
|
+
const eventHandler = onEvent ?? defaultOnEvent;
|
|
643
|
+
const eventHandlerRef = (0, import_react2.useRef)(eventHandler);
|
|
644
|
+
eventHandlerRef.current = eventHandler;
|
|
645
|
+
const onErrorRef = (0, import_react2.useRef)(onError);
|
|
646
|
+
onErrorRef.current = onError;
|
|
647
|
+
const onFinishRef = (0, import_react2.useRef)(onFinish);
|
|
648
|
+
onFinishRef.current = onFinish;
|
|
649
|
+
const onMessageRef = (0, import_react2.useRef)(onMessage);
|
|
650
|
+
onMessageRef.current = onMessage;
|
|
651
|
+
const transportContext = (0, import_react2.useMemo)(
|
|
652
|
+
() => createChatTransportContext({
|
|
653
|
+
appendPart,
|
|
654
|
+
appendText,
|
|
655
|
+
eventHandler: (event, helpers) => eventHandlerRef.current(event, helpers),
|
|
656
|
+
getMessages: () => {
|
|
657
|
+
flushPendingText();
|
|
658
|
+
return messagesRef.current;
|
|
659
|
+
},
|
|
660
|
+
onError: (...args) => onErrorRef.current?.(...args),
|
|
661
|
+
onFinish: (...args) => onFinishRef.current?.(...args),
|
|
662
|
+
onMessage: (...args) => onMessageRef.current?.(...args),
|
|
663
|
+
setError,
|
|
664
|
+
setIsLoading,
|
|
665
|
+
setMessages: updateMessages,
|
|
666
|
+
setRunId: (next) => {
|
|
667
|
+
setRunId(next);
|
|
668
|
+
transportOptionsRef.current?.backgroundSSE?.onRunIdChange?.(next);
|
|
669
|
+
transportOptionsRef.current?.websocket?.onRunIdChange?.(next);
|
|
670
|
+
}
|
|
671
|
+
}),
|
|
672
|
+
[appendPart, appendText, flushPendingText, updateMessages]
|
|
673
|
+
);
|
|
175
674
|
const stop = (0, import_react2.useCallback)(() => {
|
|
176
|
-
|
|
177
|
-
|
|
675
|
+
const activeRun = runRef.current;
|
|
676
|
+
if (!activeRun?.stop) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
manuallyStoppedRef.current = true;
|
|
680
|
+
void activeRun.stop();
|
|
681
|
+
runRef.current = null;
|
|
178
682
|
setIsLoading(false);
|
|
179
683
|
}, []);
|
|
180
684
|
const sendMessage = (0, import_react2.useCallback)(
|
|
181
685
|
(text) => {
|
|
182
|
-
if (
|
|
686
|
+
if (runRef.current || isLoading) {
|
|
183
687
|
return;
|
|
184
688
|
}
|
|
185
689
|
const userMessage = createMessage("user", [
|
|
186
690
|
{ type: "text", text }
|
|
187
691
|
]);
|
|
188
692
|
const assistantMessage = createMessage("assistant", []);
|
|
189
|
-
|
|
693
|
+
updateMessages((prev) => {
|
|
190
694
|
const next = [...prev, userMessage, assistantMessage];
|
|
191
|
-
messagesRef.current = next;
|
|
192
695
|
return next;
|
|
193
696
|
});
|
|
194
697
|
onMessage?.(userMessage);
|
|
195
698
|
setError(null);
|
|
196
699
|
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
|
-
})();
|
|
700
|
+
resumedRunIdRef.current = null;
|
|
701
|
+
manuallyStoppedRef.current = false;
|
|
702
|
+
const run = transport.start({ context: transportContext, message: text });
|
|
703
|
+
runRef.current = run ?? null;
|
|
704
|
+
if (run?.runId) {
|
|
705
|
+
setRunId(run.runId);
|
|
706
|
+
}
|
|
250
707
|
},
|
|
251
|
-
[
|
|
252
|
-
api,
|
|
253
|
-
headers,
|
|
254
|
-
body,
|
|
255
|
-
onEvent,
|
|
256
|
-
onMessage,
|
|
257
|
-
onError,
|
|
258
|
-
onFinish,
|
|
259
|
-
appendText,
|
|
260
|
-
appendPart
|
|
261
|
-
]
|
|
708
|
+
[isLoading, onMessage, transport, transportContext, updateMessages]
|
|
262
709
|
);
|
|
710
|
+
const candidateRunId = options.transportOptions?.backgroundSSE?.runId ?? options.transportOptions?.backgroundSSE?.getResumeKey?.() ?? options.transportOptions?.websocket?.runId ?? options.transportOptions?.websocket?.getResumeKey?.() ?? null;
|
|
711
|
+
(0, import_react2.useEffect)(() => {
|
|
712
|
+
if (runRef.current) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (!candidateRunId) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (manuallyStoppedRef.current) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (resumedRunIdRef.current === candidateRunId) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (!transport.resume) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
resumedRunIdRef.current = candidateRunId;
|
|
728
|
+
setError(null);
|
|
729
|
+
setIsLoading(true);
|
|
730
|
+
const run = transport.resume({
|
|
731
|
+
context: transportContext,
|
|
732
|
+
runId: candidateRunId
|
|
733
|
+
});
|
|
734
|
+
runRef.current = run ?? null;
|
|
735
|
+
setRunId(candidateRunId);
|
|
736
|
+
}, [candidateRunId, transport, transportContext]);
|
|
263
737
|
(0, import_react2.useEffect)(() => {
|
|
264
738
|
return () => {
|
|
265
|
-
|
|
266
|
-
|
|
739
|
+
if (textFlushTimerRef.current !== null) {
|
|
740
|
+
clearTimeout(textFlushTimerRef.current);
|
|
741
|
+
textFlushTimerRef.current = null;
|
|
742
|
+
}
|
|
743
|
+
void runRef.current?.close?.();
|
|
744
|
+
runRef.current = null;
|
|
267
745
|
};
|
|
268
746
|
}, []);
|
|
747
|
+
const prevIsLoadingRef = (0, import_react2.useRef)(isLoading);
|
|
748
|
+
(0, import_react2.useEffect)(() => {
|
|
749
|
+
if (prevIsLoadingRef.current && !isLoading) {
|
|
750
|
+
runRef.current = null;
|
|
751
|
+
}
|
|
752
|
+
prevIsLoadingRef.current = isLoading;
|
|
753
|
+
}, [isLoading]);
|
|
269
754
|
return {
|
|
270
|
-
messages,
|
|
271
|
-
isLoading,
|
|
272
755
|
error,
|
|
756
|
+
isLoading,
|
|
757
|
+
messages,
|
|
758
|
+
runId,
|
|
273
759
|
sendMessage,
|
|
274
|
-
|
|
275
|
-
|
|
760
|
+
setMessages: updateMessages,
|
|
761
|
+
stop
|
|
276
762
|
};
|
|
277
763
|
}
|
|
278
764
|
// Annotate the CommonJS export names for ESM import in node:
|