@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.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 && isAtBottomRef.current) {
51
- el.scrollTo({ top: el.scrollHeight, behavior });
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 abortRef = (0, import_react2.useRef)(null);
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 appendText = (0, import_react2.useCallback)((delta) => {
146
- setMessages((prev) => {
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
- const updated = { ...last, parts };
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
- setMessages((prev) => {
624
+ flushPendingText();
625
+ updateMessages((prev) => {
166
626
  const last = prev[prev.length - 1];
167
627
  if (!last || last.role !== "assistant") return prev;
168
- const updated = {
169
- ...last,
170
- parts: [...last.parts, part]
171
- };
172
- return [...prev.slice(0, -1), updated];
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
- abortRef.current?.abort();
177
- abortRef.current = null;
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 (abortRef.current) {
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
- setMessages((prev) => {
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
- const controller = new AbortController();
198
- abortRef.current = controller;
199
- const eventHandler = onEvent ?? defaultOnEvent;
200
- const helpers = {
201
- appendText,
202
- appendPart,
203
- setMessages
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
- abortRef.current?.abort();
266
- abortRef.current = null;
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
- stop,
275
- setMessages
760
+ setMessages: updateMessages,
761
+ stop
276
762
  };
277
763
  }
278
764
  // Annotate the CommonJS export names for ESM import in node: