@agents24/chat-react 0.1.3 → 0.1.5
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 +18 -1
- package/dist/index.cjs +238 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +229 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agents24 Chat React
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-06-
|
|
3
|
+
Last Updated: 2026-06-17
|
|
4
4
|
|
|
5
5
|
Headless React primitives for Agents24 chat surfaces.
|
|
6
6
|
|
|
@@ -11,6 +11,9 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
|
|
|
11
11
|
- `useAgents24ChatController(options)`
|
|
12
12
|
- `useLatestThreadViewport(options)`
|
|
13
13
|
- `LatestThreadViewport`
|
|
14
|
+
- `useStreamingText(options)`
|
|
15
|
+
- `getActiveStreamingTextPartId(message, streamingMessageId)`
|
|
16
|
+
- `isActiveStreamingTextPart(message, part, streamingMessageId)`
|
|
14
17
|
- `createFetchChatTransport(options)`
|
|
15
18
|
- `consumeSseResponse(response, onEvent)`
|
|
16
19
|
- normalized chat message, part, transport, and storage types
|
|
@@ -19,4 +22,18 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
|
|
|
19
22
|
|
|
20
23
|
`ChatMessage.parts` is the only render model. Backend `response_blocks` are normalized into ordered parts such as `text`, `tool-get_meteo`, `ui-blocks`, `reasoning`, `approval`, `error`, and `data`. Tool UI is client-owned through `renderPart`, `toolRenderers`, and `fallbackToolRenderer`; the package does not infer English tool labels or group tool rows.
|
|
21
24
|
|
|
25
|
+
`useStreamingText` owns reusable visual pacing for active assistant text. It returns `displayedText`, `isAnimating`, `mode`, and `parseIncompleteMarkdown` so host renderers can pass those values into their markdown component without the package depending on any markdown/UI library. The package only chooses text timing, cache behavior, reduced-motion handling, and active text-part helpers.
|
|
26
|
+
|
|
22
27
|
Network access is always injected by the host. This browser package never requires platform API keys.
|
|
28
|
+
|
|
29
|
+
## Latest Viewport Behavior
|
|
30
|
+
|
|
31
|
+
The package owns the standard streaming scroll policy:
|
|
32
|
+
|
|
33
|
+
- a new active stream starts in latest-follow mode
|
|
34
|
+
- streaming text growth stays pinned to latest while the user remains there
|
|
35
|
+
- user scrolling away detaches latest-follow mode
|
|
36
|
+
- manually reaching latest again, or clicking the latest button, reattaches follow mode
|
|
37
|
+
- older-page prepends preserve the visible viewport instead of snapping to latest
|
|
38
|
+
|
|
39
|
+
Hosts should wire `handleScroll` and `scrollToLatest` from `useLatestThreadViewport`, or use `LatestThreadViewport` directly. `handleUserScrollIntent` is available for hosts that need to mark wheel, touch, or keyboard intent before a scroll event lands.
|
package/dist/index.cjs
CHANGED
|
@@ -31,14 +31,17 @@ __export(index_exports, {
|
|
|
31
31
|
consumeSseResponse: () => consumeSseResponse,
|
|
32
32
|
createChatId: () => createChatId,
|
|
33
33
|
createFetchChatTransport: () => createFetchChatTransport,
|
|
34
|
+
getActiveStreamingTextPartId: () => getActiveStreamingTextPartId,
|
|
34
35
|
getLatestScrollTop: () => getLatestScrollTop,
|
|
35
36
|
hasStaleUnfinishedAssistantCache: () => hasStaleUnfinishedAssistantCache,
|
|
36
37
|
hasUnfinishedAssistantMessage: () => hasUnfinishedAssistantMessage,
|
|
38
|
+
isActiveStreamingTextPart: () => isActiveStreamingTextPart,
|
|
37
39
|
isAtTimelineLatestEdge: () => isAtTimelineLatestEdge,
|
|
38
40
|
isRunningThreadStatus: () => isRunningThreadStatus,
|
|
39
41
|
isScrollable: () => isScrollable,
|
|
40
42
|
latestContextWindowFromThread: () => latestContextWindowFromThread,
|
|
41
43
|
mergeReasoningSteps: () => mergeReasoningSteps,
|
|
44
|
+
nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
|
|
42
45
|
parseSseBlock: () => parseSseBlock,
|
|
43
46
|
partsFromResponseBlocks: () => partsFromResponseBlocks,
|
|
44
47
|
reasoningStepsFromParts: () => reasoningStepsFromParts,
|
|
@@ -52,7 +55,8 @@ __export(index_exports, {
|
|
|
52
55
|
titleFromMessage: () => titleFromMessage,
|
|
53
56
|
toolStateFromStatus: () => toolStateFromStatus,
|
|
54
57
|
useAgents24ChatController: () => useAgents24ChatController,
|
|
55
|
-
useLatestThreadViewport: () => useLatestThreadViewport
|
|
58
|
+
useLatestThreadViewport: () => useLatestThreadViewport,
|
|
59
|
+
useStreamingText: () => useStreamingText
|
|
56
60
|
});
|
|
57
61
|
module.exports = __toCommonJS(index_exports);
|
|
58
62
|
|
|
@@ -1093,6 +1097,140 @@ var consumeSseResponse = async (response, onEvent) => {
|
|
|
1093
1097
|
return { threadId, runId };
|
|
1094
1098
|
};
|
|
1095
1099
|
|
|
1100
|
+
// src/streaming-text.ts
|
|
1101
|
+
var import_react2 = require("react");
|
|
1102
|
+
var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
|
|
1103
|
+
var defaultCompletedTextCache = /* @__PURE__ */ new Map();
|
|
1104
|
+
var defaultStreamingTextCache = {
|
|
1105
|
+
get: (id) => defaultCompletedTextCache.get(id),
|
|
1106
|
+
set: (id, text) => {
|
|
1107
|
+
defaultCompletedTextCache.set(id, text);
|
|
1108
|
+
if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {
|
|
1109
|
+
const firstKey = defaultCompletedTextCache.keys().next().value;
|
|
1110
|
+
if (firstKey) defaultCompletedTextCache.delete(firstKey);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
var getActiveStreamingTextPartId = (message, streamingMessageId) => {
|
|
1115
|
+
if (message.role !== "assistant" || streamingMessageId !== message.id) return null;
|
|
1116
|
+
return message.parts.slice().reverse().find((part) => part.kind === "text")?.id || null;
|
|
1117
|
+
};
|
|
1118
|
+
var isActiveStreamingTextPart = (message, part, streamingMessageId) => part.kind === "text" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);
|
|
1119
|
+
function useStreamingText({
|
|
1120
|
+
id,
|
|
1121
|
+
isStreaming,
|
|
1122
|
+
text,
|
|
1123
|
+
cache = defaultStreamingTextCache,
|
|
1124
|
+
charsPerSecond = 72,
|
|
1125
|
+
catchupThreshold = 40,
|
|
1126
|
+
maxCatchupChars = 20
|
|
1127
|
+
}) {
|
|
1128
|
+
const cacheAdapter = cache === false ? null : cache;
|
|
1129
|
+
const [displayedText, setDisplayedText] = (0, import_react2.useState)(() => {
|
|
1130
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1131
|
+
return isStreaming && cachedText !== text ? "" : text;
|
|
1132
|
+
});
|
|
1133
|
+
const targetRef = (0, import_react2.useRef)(text);
|
|
1134
|
+
const displayedRef = (0, import_react2.useRef)(displayedText);
|
|
1135
|
+
const rafRef = (0, import_react2.useRef)(null);
|
|
1136
|
+
const lastFrameAtRef = (0, import_react2.useRef)(null);
|
|
1137
|
+
const idRef = (0, import_react2.useRef)(id);
|
|
1138
|
+
const shouldAnimateRef = (0, import_react2.useRef)(isStreaming);
|
|
1139
|
+
(0, import_react2.useEffect)(() => {
|
|
1140
|
+
if (isStreaming || !text) return;
|
|
1141
|
+
cacheAdapter?.set(id, text);
|
|
1142
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
1143
|
+
(0, import_react2.useEffect)(() => {
|
|
1144
|
+
targetRef.current = text;
|
|
1145
|
+
}, [text]);
|
|
1146
|
+
(0, import_react2.useEffect)(() => {
|
|
1147
|
+
if (idRef.current === id) return;
|
|
1148
|
+
idRef.current = id;
|
|
1149
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1150
|
+
const initial = isStreaming && cachedText !== text ? "" : text;
|
|
1151
|
+
shouldAnimateRef.current = isStreaming && initial !== text;
|
|
1152
|
+
displayedRef.current = initial;
|
|
1153
|
+
setDisplayedText(initial);
|
|
1154
|
+
if (rafRef.current !== null) {
|
|
1155
|
+
cancelAnimationFrame(rafRef.current);
|
|
1156
|
+
rafRef.current = null;
|
|
1157
|
+
}
|
|
1158
|
+
lastFrameAtRef.current = null;
|
|
1159
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
1160
|
+
(0, import_react2.useEffect)(() => {
|
|
1161
|
+
if (typeof window === "undefined") {
|
|
1162
|
+
displayedRef.current = text;
|
|
1163
|
+
setDisplayedText(text);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const reduceMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1167
|
+
if (reduceMotion) {
|
|
1168
|
+
displayedRef.current = text;
|
|
1169
|
+
setDisplayedText(text);
|
|
1170
|
+
shouldAnimateRef.current = false;
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1174
|
+
if (cachedText === text) {
|
|
1175
|
+
displayedRef.current = text;
|
|
1176
|
+
setDisplayedText(text);
|
|
1177
|
+
shouldAnimateRef.current = false;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (isStreaming) {
|
|
1181
|
+
shouldAnimateRef.current = true;
|
|
1182
|
+
}
|
|
1183
|
+
if (!shouldAnimateRef.current) {
|
|
1184
|
+
displayedRef.current = text;
|
|
1185
|
+
setDisplayedText(text);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!text.startsWith(displayedRef.current)) {
|
|
1189
|
+
displayedRef.current = "";
|
|
1190
|
+
setDisplayedText("");
|
|
1191
|
+
}
|
|
1192
|
+
const tick = (timestamp) => {
|
|
1193
|
+
const previousTimestamp = lastFrameAtRef.current ?? timestamp;
|
|
1194
|
+
lastFrameAtRef.current = timestamp;
|
|
1195
|
+
const target = targetRef.current;
|
|
1196
|
+
const current = displayedRef.current;
|
|
1197
|
+
if (current.length >= target.length) {
|
|
1198
|
+
shouldAnimateRef.current = false;
|
|
1199
|
+
rafRef.current = null;
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const elapsedMs = Math.max(8, timestamp - previousTimestamp);
|
|
1203
|
+
const charsFromTime = Math.max(1, Math.floor(elapsedMs / 1e3 * charsPerSecond));
|
|
1204
|
+
const gap = target.length - current.length;
|
|
1205
|
+
const catchupStep = gap > catchupThreshold ? Math.min(maxCatchupChars, Math.ceil(gap / 10)) : charsFromTime;
|
|
1206
|
+
const nextLength = Math.min(
|
|
1207
|
+
target.length,
|
|
1208
|
+
current.length + Math.max(charsFromTime, catchupStep)
|
|
1209
|
+
);
|
|
1210
|
+
const next = target.slice(0, nextLength);
|
|
1211
|
+
displayedRef.current = next;
|
|
1212
|
+
setDisplayedText(next);
|
|
1213
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
1214
|
+
};
|
|
1215
|
+
if (rafRef.current === null && displayedRef.current.length < text.length) {
|
|
1216
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
1217
|
+
}
|
|
1218
|
+
return () => {
|
|
1219
|
+
if (rafRef.current !== null) {
|
|
1220
|
+
cancelAnimationFrame(rafRef.current);
|
|
1221
|
+
rafRef.current = null;
|
|
1222
|
+
}
|
|
1223
|
+
lastFrameAtRef.current = null;
|
|
1224
|
+
};
|
|
1225
|
+
}, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars, text]);
|
|
1226
|
+
return {
|
|
1227
|
+
displayedText,
|
|
1228
|
+
isAnimating: isStreaming,
|
|
1229
|
+
mode: isStreaming ? "streaming" : "static",
|
|
1230
|
+
parseIncompleteMarkdown: isStreaming
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1096
1234
|
// src/transport.ts
|
|
1097
1235
|
var jsonHeaders = (headers) => ({
|
|
1098
1236
|
...headers || {},
|
|
@@ -1174,7 +1312,7 @@ var createFetchChatTransport = ({
|
|
|
1174
1312
|
};
|
|
1175
1313
|
|
|
1176
1314
|
// src/viewport.tsx
|
|
1177
|
-
var
|
|
1315
|
+
var import_react3 = require("react");
|
|
1178
1316
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1179
1317
|
var LATEST_EDGE_THRESHOLD_PX = 2;
|
|
1180
1318
|
var MIN_OLDER_PREFETCH_PX = 320;
|
|
@@ -1191,6 +1329,14 @@ var shouldUseTopOriginTimeline = ({
|
|
|
1191
1329
|
itemCount,
|
|
1192
1330
|
topOriginMaxItems = 4
|
|
1193
1331
|
}) => !hasOlder && itemCount > 0 && itemCount <= topOriginMaxItems;
|
|
1332
|
+
var nextLatestFollowStateOnScroll = ({
|
|
1333
|
+
atLatest,
|
|
1334
|
+
current,
|
|
1335
|
+
isProgrammatic
|
|
1336
|
+
}) => {
|
|
1337
|
+
if (isProgrammatic) return current;
|
|
1338
|
+
return atLatest ? "following" : "detached";
|
|
1339
|
+
};
|
|
1194
1340
|
function useLatestThreadViewport({
|
|
1195
1341
|
itemCount,
|
|
1196
1342
|
hasOlder,
|
|
@@ -1200,17 +1346,24 @@ function useLatestThreadViewport({
|
|
|
1200
1346
|
shouldAutoFollow = true,
|
|
1201
1347
|
topOriginMaxItems = 4
|
|
1202
1348
|
}) {
|
|
1203
|
-
const scrollContainerRef = (0,
|
|
1204
|
-
const olderPagePreserveRef = (0,
|
|
1205
|
-
const olderPageRequestInFlightRef = (0,
|
|
1206
|
-
const intrinsicResizePreserveRef = (0,
|
|
1207
|
-
const
|
|
1208
|
-
const programmaticScrollRef = (0,
|
|
1209
|
-
const programmaticScrollTimeoutRef = (0,
|
|
1210
|
-
const activeStreamKeyRef = (0,
|
|
1211
|
-
const
|
|
1349
|
+
const scrollContainerRef = (0, import_react3.useRef)(null);
|
|
1350
|
+
const olderPagePreserveRef = (0, import_react3.useRef)(null);
|
|
1351
|
+
const olderPageRequestInFlightRef = (0, import_react3.useRef)(false);
|
|
1352
|
+
const intrinsicResizePreserveRef = (0, import_react3.useRef)(null);
|
|
1353
|
+
const followStateRef = (0, import_react3.useRef)("following");
|
|
1354
|
+
const programmaticScrollRef = (0, import_react3.useRef)(false);
|
|
1355
|
+
const programmaticScrollTimeoutRef = (0, import_react3.useRef)(null);
|
|
1356
|
+
const activeStreamKeyRef = (0, import_react3.useRef)(null);
|
|
1357
|
+
const observedScrollHeightRef = (0, import_react3.useRef)(null);
|
|
1358
|
+
const [isAtLatest, setIsAtLatest] = (0, import_react3.useState)(true);
|
|
1359
|
+
const [followState, setFollowState] = (0, import_react3.useState)("following");
|
|
1212
1360
|
const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
|
|
1213
|
-
const
|
|
1361
|
+
const setLatestFollowState = (0, import_react3.useCallback)((next) => {
|
|
1362
|
+
if (followStateRef.current === next) return;
|
|
1363
|
+
followStateRef.current = next;
|
|
1364
|
+
setFollowState(next);
|
|
1365
|
+
}, []);
|
|
1366
|
+
const markProgrammaticScroll = (0, import_react3.useCallback)(() => {
|
|
1214
1367
|
programmaticScrollRef.current = true;
|
|
1215
1368
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1216
1369
|
programmaticScrollTimeoutRef.current = setTimeout(() => {
|
|
@@ -1218,24 +1371,38 @@ function useLatestThreadViewport({
|
|
|
1218
1371
|
programmaticScrollTimeoutRef.current = null;
|
|
1219
1372
|
}, 80);
|
|
1220
1373
|
}, []);
|
|
1221
|
-
const scrollToLatest = (0,
|
|
1374
|
+
const scrollToLatest = (0, import_react3.useCallback)((options) => {
|
|
1222
1375
|
const element = scrollContainerRef.current;
|
|
1223
1376
|
if (!element) return;
|
|
1377
|
+
if (options?.reattach !== false) setLatestFollowState("following");
|
|
1224
1378
|
markProgrammaticScroll();
|
|
1225
1379
|
element.scrollTop = getLatestScrollTop(element, isTopOrigin);
|
|
1226
1380
|
setIsAtLatest(true);
|
|
1227
|
-
|
|
1228
|
-
|
|
1381
|
+
observedScrollHeightRef.current = element.scrollHeight;
|
|
1382
|
+
}, [isTopOrigin, markProgrammaticScroll, setLatestFollowState]);
|
|
1383
|
+
const detachFromLatest = (0, import_react3.useCallback)(() => {
|
|
1384
|
+
setLatestFollowState("detached");
|
|
1385
|
+
}, [setLatestFollowState]);
|
|
1386
|
+
const reattachToLatest = (0, import_react3.useCallback)(() => {
|
|
1387
|
+
scrollToLatest({ reattach: true });
|
|
1388
|
+
}, [scrollToLatest]);
|
|
1389
|
+
const handleUserScrollIntent = (0, import_react3.useCallback)(() => {
|
|
1390
|
+
const element = scrollContainerRef.current;
|
|
1391
|
+
if (!activeStreamKey || !element) return;
|
|
1392
|
+
if (!isAtTimelineLatestEdge(element, isTopOrigin)) detachFromLatest();
|
|
1393
|
+
}, [activeStreamKey, detachFromLatest, isTopOrigin]);
|
|
1394
|
+
(0, import_react3.useEffect)(() => {
|
|
1229
1395
|
return () => {
|
|
1230
1396
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1231
1397
|
const preserve = intrinsicResizePreserveRef.current;
|
|
1232
1398
|
if (preserve?.frame !== null && preserve?.frame !== void 0) cancelAnimationFrame(preserve.frame);
|
|
1233
1399
|
};
|
|
1234
1400
|
}, []);
|
|
1235
|
-
(0,
|
|
1401
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1236
1402
|
const element = scrollContainerRef.current;
|
|
1237
1403
|
const preserve = olderPagePreserveRef.current;
|
|
1238
1404
|
if (!element) return;
|
|
1405
|
+
observedScrollHeightRef.current = element.scrollHeight;
|
|
1239
1406
|
if (!preserve) {
|
|
1240
1407
|
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1241
1408
|
return;
|
|
@@ -1247,12 +1414,18 @@ function useLatestThreadViewport({
|
|
|
1247
1414
|
olderPageRequestInFlightRef.current = false;
|
|
1248
1415
|
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1249
1416
|
}, [isTopOrigin, itemCount, markProgrammaticScroll]);
|
|
1250
|
-
const handleScroll = (0,
|
|
1417
|
+
const handleScroll = (0, import_react3.useCallback)(
|
|
1251
1418
|
(event) => {
|
|
1252
1419
|
const element = event.currentTarget;
|
|
1253
1420
|
const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
|
|
1254
1421
|
setIsAtLatest(atLatest);
|
|
1255
|
-
|
|
1422
|
+
setLatestFollowState(
|
|
1423
|
+
nextLatestFollowStateOnScroll({
|
|
1424
|
+
atLatest,
|
|
1425
|
+
current: followStateRef.current,
|
|
1426
|
+
isProgrammatic: programmaticScrollRef.current
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1256
1429
|
if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
|
|
1257
1430
|
return;
|
|
1258
1431
|
}
|
|
@@ -1269,9 +1442,9 @@ function useLatestThreadViewport({
|
|
|
1269
1442
|
});
|
|
1270
1443
|
});
|
|
1271
1444
|
},
|
|
1272
|
-
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
|
|
1445
|
+
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
|
|
1273
1446
|
);
|
|
1274
|
-
const preserveIntrinsicResize = (0,
|
|
1447
|
+
const preserveIntrinsicResize = (0, import_react3.useCallback)(() => {
|
|
1275
1448
|
const element = scrollContainerRef.current;
|
|
1276
1449
|
if (!element || isTopOrigin) return;
|
|
1277
1450
|
const active = intrinsicResizePreserveRef.current;
|
|
@@ -1300,37 +1473,60 @@ function useLatestThreadViewport({
|
|
|
1300
1473
|
};
|
|
1301
1474
|
intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
|
|
1302
1475
|
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1303
|
-
(0,
|
|
1476
|
+
(0, import_react3.useEffect)(() => {
|
|
1304
1477
|
const key = activeStreamKey || null;
|
|
1305
1478
|
if (!key || activeStreamKeyRef.current === key) {
|
|
1306
1479
|
activeStreamKeyRef.current = key;
|
|
1307
1480
|
return;
|
|
1308
1481
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
activeStreamKeyRef.current = key;
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
const frame = requestAnimationFrame(scrollToLatest);
|
|
1482
|
+
setLatestFollowState("following");
|
|
1483
|
+
const frame = requestAnimationFrame(() => scrollToLatest());
|
|
1315
1484
|
activeStreamKeyRef.current = key;
|
|
1316
1485
|
return () => cancelAnimationFrame(frame);
|
|
1317
|
-
}, [activeStreamKey,
|
|
1318
|
-
(0,
|
|
1319
|
-
if (
|
|
1320
|
-
if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
|
|
1486
|
+
}, [activeStreamKey, scrollToLatest, setLatestFollowState]);
|
|
1487
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1488
|
+
if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
|
|
1321
1489
|
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
1322
1490
|
const frame = requestAnimationFrame(() => {
|
|
1323
|
-
if (
|
|
1491
|
+
if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1492
|
+
scrollToLatest({ reattach: false });
|
|
1493
|
+
}
|
|
1324
1494
|
});
|
|
1325
1495
|
return () => cancelAnimationFrame(frame);
|
|
1326
|
-
}, [activeStreamKey, isLoadingOlder,
|
|
1496
|
+
}, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
|
|
1497
|
+
(0, import_react3.useEffect)(() => {
|
|
1498
|
+
if (!activeStreamKey) {
|
|
1499
|
+
observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
let frame = null;
|
|
1503
|
+
const watchStreamResize = () => {
|
|
1504
|
+
const element = scrollContainerRef.current;
|
|
1505
|
+
if (!element) return;
|
|
1506
|
+
const previousHeight = observedScrollHeightRef.current;
|
|
1507
|
+
const nextHeight = element.scrollHeight;
|
|
1508
|
+
observedScrollHeightRef.current = nextHeight;
|
|
1509
|
+
if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1510
|
+
scrollToLatest({ reattach: false });
|
|
1511
|
+
}
|
|
1512
|
+
frame = requestAnimationFrame(watchStreamResize);
|
|
1513
|
+
};
|
|
1514
|
+
frame = requestAnimationFrame(watchStreamResize);
|
|
1515
|
+
return () => {
|
|
1516
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
1517
|
+
};
|
|
1518
|
+
}, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
|
|
1327
1519
|
return {
|
|
1328
1520
|
scrollContainerRef,
|
|
1329
1521
|
isAtLatest,
|
|
1522
|
+
isFollowingLatest: followState === "following",
|
|
1330
1523
|
isTopOrigin,
|
|
1331
1524
|
shouldShowLatestButton: !isAtLatest,
|
|
1332
1525
|
handleScroll,
|
|
1526
|
+
handleUserScrollIntent,
|
|
1333
1527
|
scrollToLatest,
|
|
1528
|
+
detachFromLatest,
|
|
1529
|
+
reattachToLatest,
|
|
1334
1530
|
preserveIntrinsicResize
|
|
1335
1531
|
};
|
|
1336
1532
|
}
|
|
@@ -1352,13 +1548,16 @@ function LatestThreadViewport({
|
|
|
1352
1548
|
onLoadOlder,
|
|
1353
1549
|
activeStreamKey
|
|
1354
1550
|
});
|
|
1355
|
-
const timelineItems = (0,
|
|
1551
|
+
const timelineItems = (0, import_react3.useMemo)(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
|
|
1356
1552
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1357
1553
|
"div",
|
|
1358
1554
|
{
|
|
1359
1555
|
ref: viewport.scrollContainerRef,
|
|
1360
1556
|
className,
|
|
1557
|
+
onKeyDown: viewport.handleUserScrollIntent,
|
|
1361
1558
|
onScroll: viewport.handleScroll,
|
|
1559
|
+
onTouchMove: viewport.handleUserScrollIntent,
|
|
1560
|
+
onWheel: viewport.handleUserScrollIntent,
|
|
1362
1561
|
role: "log",
|
|
1363
1562
|
style: { overflowAnchor: "none" },
|
|
1364
1563
|
children: [
|
|
@@ -1382,14 +1581,17 @@ function LatestThreadViewport({
|
|
|
1382
1581
|
consumeSseResponse,
|
|
1383
1582
|
createChatId,
|
|
1384
1583
|
createFetchChatTransport,
|
|
1584
|
+
getActiveStreamingTextPartId,
|
|
1385
1585
|
getLatestScrollTop,
|
|
1386
1586
|
hasStaleUnfinishedAssistantCache,
|
|
1387
1587
|
hasUnfinishedAssistantMessage,
|
|
1588
|
+
isActiveStreamingTextPart,
|
|
1388
1589
|
isAtTimelineLatestEdge,
|
|
1389
1590
|
isRunningThreadStatus,
|
|
1390
1591
|
isScrollable,
|
|
1391
1592
|
latestContextWindowFromThread,
|
|
1392
1593
|
mergeReasoningSteps,
|
|
1594
|
+
nextLatestFollowStateOnScroll,
|
|
1393
1595
|
parseSseBlock,
|
|
1394
1596
|
partsFromResponseBlocks,
|
|
1395
1597
|
reasoningStepsFromParts,
|
|
@@ -1403,6 +1605,7 @@ function LatestThreadViewport({
|
|
|
1403
1605
|
titleFromMessage,
|
|
1404
1606
|
toolStateFromStatus,
|
|
1405
1607
|
useAgents24ChatController,
|
|
1406
|
-
useLatestThreadViewport
|
|
1608
|
+
useLatestThreadViewport,
|
|
1609
|
+
useStreamingText
|
|
1407
1610
|
});
|
|
1408
1611
|
//# sourceMappingURL=index.cjs.map
|