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