@agents24/chat-react 0.1.4 → 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 +13 -1
- package/dist/index.cjs +79 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +78 -16
- 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
|
|
|
@@ -25,3 +25,15 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
|
|
|
25
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
26
|
|
|
27
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
|
@@ -41,6 +41,7 @@ __export(index_exports, {
|
|
|
41
41
|
isScrollable: () => isScrollable,
|
|
42
42
|
latestContextWindowFromThread: () => latestContextWindowFromThread,
|
|
43
43
|
mergeReasoningSteps: () => mergeReasoningSteps,
|
|
44
|
+
nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
|
|
44
45
|
parseSseBlock: () => parseSseBlock,
|
|
45
46
|
partsFromResponseBlocks: () => partsFromResponseBlocks,
|
|
46
47
|
reasoningStepsFromParts: () => reasoningStepsFromParts,
|
|
@@ -1328,6 +1329,14 @@ var shouldUseTopOriginTimeline = ({
|
|
|
1328
1329
|
itemCount,
|
|
1329
1330
|
topOriginMaxItems = 4
|
|
1330
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
|
+
};
|
|
1331
1340
|
function useLatestThreadViewport({
|
|
1332
1341
|
itemCount,
|
|
1333
1342
|
hasOlder,
|
|
@@ -1341,12 +1350,19 @@ function useLatestThreadViewport({
|
|
|
1341
1350
|
const olderPagePreserveRef = (0, import_react3.useRef)(null);
|
|
1342
1351
|
const olderPageRequestInFlightRef = (0, import_react3.useRef)(false);
|
|
1343
1352
|
const intrinsicResizePreserveRef = (0, import_react3.useRef)(null);
|
|
1344
|
-
const
|
|
1353
|
+
const followStateRef = (0, import_react3.useRef)("following");
|
|
1345
1354
|
const programmaticScrollRef = (0, import_react3.useRef)(false);
|
|
1346
1355
|
const programmaticScrollTimeoutRef = (0, import_react3.useRef)(null);
|
|
1347
1356
|
const activeStreamKeyRef = (0, import_react3.useRef)(null);
|
|
1357
|
+
const observedScrollHeightRef = (0, import_react3.useRef)(null);
|
|
1348
1358
|
const [isAtLatest, setIsAtLatest] = (0, import_react3.useState)(true);
|
|
1359
|
+
const [followState, setFollowState] = (0, import_react3.useState)("following");
|
|
1349
1360
|
const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
|
|
1361
|
+
const setLatestFollowState = (0, import_react3.useCallback)((next) => {
|
|
1362
|
+
if (followStateRef.current === next) return;
|
|
1363
|
+
followStateRef.current = next;
|
|
1364
|
+
setFollowState(next);
|
|
1365
|
+
}, []);
|
|
1350
1366
|
const markProgrammaticScroll = (0, import_react3.useCallback)(() => {
|
|
1351
1367
|
programmaticScrollRef.current = true;
|
|
1352
1368
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
@@ -1355,13 +1371,26 @@ function useLatestThreadViewport({
|
|
|
1355
1371
|
programmaticScrollTimeoutRef.current = null;
|
|
1356
1372
|
}, 80);
|
|
1357
1373
|
}, []);
|
|
1358
|
-
const scrollToLatest = (0, import_react3.useCallback)(() => {
|
|
1374
|
+
const scrollToLatest = (0, import_react3.useCallback)((options) => {
|
|
1359
1375
|
const element = scrollContainerRef.current;
|
|
1360
1376
|
if (!element) return;
|
|
1377
|
+
if (options?.reattach !== false) setLatestFollowState("following");
|
|
1361
1378
|
markProgrammaticScroll();
|
|
1362
1379
|
element.scrollTop = getLatestScrollTop(element, isTopOrigin);
|
|
1363
1380
|
setIsAtLatest(true);
|
|
1364
|
-
|
|
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]);
|
|
1365
1394
|
(0, import_react3.useEffect)(() => {
|
|
1366
1395
|
return () => {
|
|
1367
1396
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
@@ -1373,6 +1402,7 @@ function useLatestThreadViewport({
|
|
|
1373
1402
|
const element = scrollContainerRef.current;
|
|
1374
1403
|
const preserve = olderPagePreserveRef.current;
|
|
1375
1404
|
if (!element) return;
|
|
1405
|
+
observedScrollHeightRef.current = element.scrollHeight;
|
|
1376
1406
|
if (!preserve) {
|
|
1377
1407
|
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1378
1408
|
return;
|
|
@@ -1389,7 +1419,13 @@ function useLatestThreadViewport({
|
|
|
1389
1419
|
const element = event.currentTarget;
|
|
1390
1420
|
const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
|
|
1391
1421
|
setIsAtLatest(atLatest);
|
|
1392
|
-
|
|
1422
|
+
setLatestFollowState(
|
|
1423
|
+
nextLatestFollowStateOnScroll({
|
|
1424
|
+
atLatest,
|
|
1425
|
+
current: followStateRef.current,
|
|
1426
|
+
isProgrammatic: programmaticScrollRef.current
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1393
1429
|
if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
|
|
1394
1430
|
return;
|
|
1395
1431
|
}
|
|
@@ -1406,7 +1442,7 @@ function useLatestThreadViewport({
|
|
|
1406
1442
|
});
|
|
1407
1443
|
});
|
|
1408
1444
|
},
|
|
1409
|
-
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
|
|
1445
|
+
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
|
|
1410
1446
|
);
|
|
1411
1447
|
const preserveIntrinsicResize = (0, import_react3.useCallback)(() => {
|
|
1412
1448
|
const element = scrollContainerRef.current;
|
|
@@ -1443,31 +1479,54 @@ function useLatestThreadViewport({
|
|
|
1443
1479
|
activeStreamKeyRef.current = key;
|
|
1444
1480
|
return;
|
|
1445
1481
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
activeStreamKeyRef.current = key;
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
const frame = requestAnimationFrame(scrollToLatest);
|
|
1482
|
+
setLatestFollowState("following");
|
|
1483
|
+
const frame = requestAnimationFrame(() => scrollToLatest());
|
|
1452
1484
|
activeStreamKeyRef.current = key;
|
|
1453
1485
|
return () => cancelAnimationFrame(frame);
|
|
1454
|
-
}, [activeStreamKey,
|
|
1486
|
+
}, [activeStreamKey, scrollToLatest, setLatestFollowState]);
|
|
1455
1487
|
(0, import_react3.useLayoutEffect)(() => {
|
|
1456
|
-
if (
|
|
1457
|
-
if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
|
|
1488
|
+
if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
|
|
1458
1489
|
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
1459
1490
|
const frame = requestAnimationFrame(() => {
|
|
1460
|
-
if (
|
|
1491
|
+
if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1492
|
+
scrollToLatest({ reattach: false });
|
|
1493
|
+
}
|
|
1461
1494
|
});
|
|
1462
1495
|
return () => cancelAnimationFrame(frame);
|
|
1463
|
-
}, [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]);
|
|
1464
1519
|
return {
|
|
1465
1520
|
scrollContainerRef,
|
|
1466
1521
|
isAtLatest,
|
|
1522
|
+
isFollowingLatest: followState === "following",
|
|
1467
1523
|
isTopOrigin,
|
|
1468
1524
|
shouldShowLatestButton: !isAtLatest,
|
|
1469
1525
|
handleScroll,
|
|
1526
|
+
handleUserScrollIntent,
|
|
1470
1527
|
scrollToLatest,
|
|
1528
|
+
detachFromLatest,
|
|
1529
|
+
reattachToLatest,
|
|
1471
1530
|
preserveIntrinsicResize
|
|
1472
1531
|
};
|
|
1473
1532
|
}
|
|
@@ -1495,7 +1554,10 @@ function LatestThreadViewport({
|
|
|
1495
1554
|
{
|
|
1496
1555
|
ref: viewport.scrollContainerRef,
|
|
1497
1556
|
className,
|
|
1557
|
+
onKeyDown: viewport.handleUserScrollIntent,
|
|
1498
1558
|
onScroll: viewport.handleScroll,
|
|
1559
|
+
onTouchMove: viewport.handleUserScrollIntent,
|
|
1560
|
+
onWheel: viewport.handleUserScrollIntent,
|
|
1499
1561
|
role: "log",
|
|
1500
1562
|
style: { overflowAnchor: "none" },
|
|
1501
1563
|
children: [
|
|
@@ -1529,6 +1591,7 @@ function LatestThreadViewport({
|
|
|
1529
1591
|
isScrollable,
|
|
1530
1592
|
latestContextWindowFromThread,
|
|
1531
1593
|
mergeReasoningSteps,
|
|
1594
|
+
nextLatestFollowStateOnScroll,
|
|
1532
1595
|
parseSseBlock,
|
|
1533
1596
|
partsFromResponseBlocks,
|
|
1534
1597
|
reasoningStepsFromParts,
|