@agents24/chat-react 0.1.4 → 0.1.6
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 +92 -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 +91 -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;
|
|
@@ -1437,37 +1473,73 @@ function useLatestThreadViewport({
|
|
|
1437
1473
|
};
|
|
1438
1474
|
intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
|
|
1439
1475
|
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1440
|
-
(0, import_react3.
|
|
1476
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1441
1477
|
const key = activeStreamKey || null;
|
|
1442
|
-
|
|
1443
|
-
|
|
1478
|
+
const previousKey = activeStreamKeyRef.current;
|
|
1479
|
+
if (!key) {
|
|
1480
|
+
activeStreamKeyRef.current = null;
|
|
1481
|
+
if (previousKey && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1482
|
+
const frame2 = requestAnimationFrame(() => {
|
|
1483
|
+
if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1484
|
+
scrollToLatest({ reattach: false });
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
return () => cancelAnimationFrame(frame2);
|
|
1488
|
+
}
|
|
1444
1489
|
return;
|
|
1445
1490
|
}
|
|
1446
|
-
|
|
1447
|
-
if (isTopOrigin) {
|
|
1491
|
+
if (previousKey === key) {
|
|
1448
1492
|
activeStreamKeyRef.current = key;
|
|
1449
1493
|
return;
|
|
1450
1494
|
}
|
|
1451
|
-
|
|
1495
|
+
setLatestFollowState("following");
|
|
1496
|
+
const frame = requestAnimationFrame(() => scrollToLatest());
|
|
1452
1497
|
activeStreamKeyRef.current = key;
|
|
1453
1498
|
return () => cancelAnimationFrame(frame);
|
|
1454
|
-
}, [activeStreamKey,
|
|
1499
|
+
}, [activeStreamKey, isLoadingOlder, scrollToLatest, setLatestFollowState, shouldAutoFollow]);
|
|
1455
1500
|
(0, import_react3.useLayoutEffect)(() => {
|
|
1456
|
-
if (
|
|
1457
|
-
if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
|
|
1501
|
+
if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
|
|
1458
1502
|
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
1459
1503
|
const frame = requestAnimationFrame(() => {
|
|
1460
|
-
if (
|
|
1504
|
+
if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1505
|
+
scrollToLatest({ reattach: false });
|
|
1506
|
+
}
|
|
1461
1507
|
});
|
|
1462
1508
|
return () => cancelAnimationFrame(frame);
|
|
1463
|
-
}, [activeStreamKey, isLoadingOlder,
|
|
1509
|
+
}, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
|
|
1510
|
+
(0, import_react3.useEffect)(() => {
|
|
1511
|
+
if (!activeStreamKey) {
|
|
1512
|
+
observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
let frame = null;
|
|
1516
|
+
const watchStreamResize = () => {
|
|
1517
|
+
const element = scrollContainerRef.current;
|
|
1518
|
+
if (!element) return;
|
|
1519
|
+
const previousHeight = observedScrollHeightRef.current;
|
|
1520
|
+
const nextHeight = element.scrollHeight;
|
|
1521
|
+
observedScrollHeightRef.current = nextHeight;
|
|
1522
|
+
if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1523
|
+
scrollToLatest({ reattach: false });
|
|
1524
|
+
}
|
|
1525
|
+
frame = requestAnimationFrame(watchStreamResize);
|
|
1526
|
+
};
|
|
1527
|
+
frame = requestAnimationFrame(watchStreamResize);
|
|
1528
|
+
return () => {
|
|
1529
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
1530
|
+
};
|
|
1531
|
+
}, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
|
|
1464
1532
|
return {
|
|
1465
1533
|
scrollContainerRef,
|
|
1466
1534
|
isAtLatest,
|
|
1535
|
+
isFollowingLatest: followState === "following",
|
|
1467
1536
|
isTopOrigin,
|
|
1468
1537
|
shouldShowLatestButton: !isAtLatest,
|
|
1469
1538
|
handleScroll,
|
|
1539
|
+
handleUserScrollIntent,
|
|
1470
1540
|
scrollToLatest,
|
|
1541
|
+
detachFromLatest,
|
|
1542
|
+
reattachToLatest,
|
|
1471
1543
|
preserveIntrinsicResize
|
|
1472
1544
|
};
|
|
1473
1545
|
}
|
|
@@ -1495,7 +1567,10 @@ function LatestThreadViewport({
|
|
|
1495
1567
|
{
|
|
1496
1568
|
ref: viewport.scrollContainerRef,
|
|
1497
1569
|
className,
|
|
1570
|
+
onKeyDown: viewport.handleUserScrollIntent,
|
|
1498
1571
|
onScroll: viewport.handleScroll,
|
|
1572
|
+
onTouchMove: viewport.handleUserScrollIntent,
|
|
1573
|
+
onWheel: viewport.handleUserScrollIntent,
|
|
1499
1574
|
role: "log",
|
|
1500
1575
|
style: { overflowAnchor: "none" },
|
|
1501
1576
|
children: [
|
|
@@ -1529,6 +1604,7 @@ function LatestThreadViewport({
|
|
|
1529
1604
|
isScrollable,
|
|
1530
1605
|
latestContextWindowFromThread,
|
|
1531
1606
|
mergeReasoningSteps,
|
|
1607
|
+
nextLatestFollowStateOnScroll,
|
|
1532
1608
|
parseSseBlock,
|
|
1533
1609
|
partsFromResponseBlocks,
|
|
1534
1610
|
reasoningStepsFromParts,
|