@agents24/chat-react 0.1.3 → 0.1.4
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 +6 -1
- package/dist/index.cjs +161 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +152 -15
- 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-14
|
|
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,6 @@ 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.
|
package/dist/index.cjs
CHANGED
|
@@ -31,9 +31,11 @@ __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,
|
|
@@ -52,7 +54,8 @@ __export(index_exports, {
|
|
|
52
54
|
titleFromMessage: () => titleFromMessage,
|
|
53
55
|
toolStateFromStatus: () => toolStateFromStatus,
|
|
54
56
|
useAgents24ChatController: () => useAgents24ChatController,
|
|
55
|
-
useLatestThreadViewport: () => useLatestThreadViewport
|
|
57
|
+
useLatestThreadViewport: () => useLatestThreadViewport,
|
|
58
|
+
useStreamingText: () => useStreamingText
|
|
56
59
|
});
|
|
57
60
|
module.exports = __toCommonJS(index_exports);
|
|
58
61
|
|
|
@@ -1093,6 +1096,140 @@ var consumeSseResponse = async (response, onEvent) => {
|
|
|
1093
1096
|
return { threadId, runId };
|
|
1094
1097
|
};
|
|
1095
1098
|
|
|
1099
|
+
// src/streaming-text.ts
|
|
1100
|
+
var import_react2 = require("react");
|
|
1101
|
+
var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
|
|
1102
|
+
var defaultCompletedTextCache = /* @__PURE__ */ new Map();
|
|
1103
|
+
var defaultStreamingTextCache = {
|
|
1104
|
+
get: (id) => defaultCompletedTextCache.get(id),
|
|
1105
|
+
set: (id, text) => {
|
|
1106
|
+
defaultCompletedTextCache.set(id, text);
|
|
1107
|
+
if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {
|
|
1108
|
+
const firstKey = defaultCompletedTextCache.keys().next().value;
|
|
1109
|
+
if (firstKey) defaultCompletedTextCache.delete(firstKey);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
var getActiveStreamingTextPartId = (message, streamingMessageId) => {
|
|
1114
|
+
if (message.role !== "assistant" || streamingMessageId !== message.id) return null;
|
|
1115
|
+
return message.parts.slice().reverse().find((part) => part.kind === "text")?.id || null;
|
|
1116
|
+
};
|
|
1117
|
+
var isActiveStreamingTextPart = (message, part, streamingMessageId) => part.kind === "text" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);
|
|
1118
|
+
function useStreamingText({
|
|
1119
|
+
id,
|
|
1120
|
+
isStreaming,
|
|
1121
|
+
text,
|
|
1122
|
+
cache = defaultStreamingTextCache,
|
|
1123
|
+
charsPerSecond = 72,
|
|
1124
|
+
catchupThreshold = 40,
|
|
1125
|
+
maxCatchupChars = 20
|
|
1126
|
+
}) {
|
|
1127
|
+
const cacheAdapter = cache === false ? null : cache;
|
|
1128
|
+
const [displayedText, setDisplayedText] = (0, import_react2.useState)(() => {
|
|
1129
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1130
|
+
return isStreaming && cachedText !== text ? "" : text;
|
|
1131
|
+
});
|
|
1132
|
+
const targetRef = (0, import_react2.useRef)(text);
|
|
1133
|
+
const displayedRef = (0, import_react2.useRef)(displayedText);
|
|
1134
|
+
const rafRef = (0, import_react2.useRef)(null);
|
|
1135
|
+
const lastFrameAtRef = (0, import_react2.useRef)(null);
|
|
1136
|
+
const idRef = (0, import_react2.useRef)(id);
|
|
1137
|
+
const shouldAnimateRef = (0, import_react2.useRef)(isStreaming);
|
|
1138
|
+
(0, import_react2.useEffect)(() => {
|
|
1139
|
+
if (isStreaming || !text) return;
|
|
1140
|
+
cacheAdapter?.set(id, text);
|
|
1141
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
1142
|
+
(0, import_react2.useEffect)(() => {
|
|
1143
|
+
targetRef.current = text;
|
|
1144
|
+
}, [text]);
|
|
1145
|
+
(0, import_react2.useEffect)(() => {
|
|
1146
|
+
if (idRef.current === id) return;
|
|
1147
|
+
idRef.current = id;
|
|
1148
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1149
|
+
const initial = isStreaming && cachedText !== text ? "" : text;
|
|
1150
|
+
shouldAnimateRef.current = isStreaming && initial !== text;
|
|
1151
|
+
displayedRef.current = initial;
|
|
1152
|
+
setDisplayedText(initial);
|
|
1153
|
+
if (rafRef.current !== null) {
|
|
1154
|
+
cancelAnimationFrame(rafRef.current);
|
|
1155
|
+
rafRef.current = null;
|
|
1156
|
+
}
|
|
1157
|
+
lastFrameAtRef.current = null;
|
|
1158
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
1159
|
+
(0, import_react2.useEffect)(() => {
|
|
1160
|
+
if (typeof window === "undefined") {
|
|
1161
|
+
displayedRef.current = text;
|
|
1162
|
+
setDisplayedText(text);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const reduceMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1166
|
+
if (reduceMotion) {
|
|
1167
|
+
displayedRef.current = text;
|
|
1168
|
+
setDisplayedText(text);
|
|
1169
|
+
shouldAnimateRef.current = false;
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const cachedText = cacheAdapter?.get(id);
|
|
1173
|
+
if (cachedText === text) {
|
|
1174
|
+
displayedRef.current = text;
|
|
1175
|
+
setDisplayedText(text);
|
|
1176
|
+
shouldAnimateRef.current = false;
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (isStreaming) {
|
|
1180
|
+
shouldAnimateRef.current = true;
|
|
1181
|
+
}
|
|
1182
|
+
if (!shouldAnimateRef.current) {
|
|
1183
|
+
displayedRef.current = text;
|
|
1184
|
+
setDisplayedText(text);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (!text.startsWith(displayedRef.current)) {
|
|
1188
|
+
displayedRef.current = "";
|
|
1189
|
+
setDisplayedText("");
|
|
1190
|
+
}
|
|
1191
|
+
const tick = (timestamp) => {
|
|
1192
|
+
const previousTimestamp = lastFrameAtRef.current ?? timestamp;
|
|
1193
|
+
lastFrameAtRef.current = timestamp;
|
|
1194
|
+
const target = targetRef.current;
|
|
1195
|
+
const current = displayedRef.current;
|
|
1196
|
+
if (current.length >= target.length) {
|
|
1197
|
+
shouldAnimateRef.current = false;
|
|
1198
|
+
rafRef.current = null;
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const elapsedMs = Math.max(8, timestamp - previousTimestamp);
|
|
1202
|
+
const charsFromTime = Math.max(1, Math.floor(elapsedMs / 1e3 * charsPerSecond));
|
|
1203
|
+
const gap = target.length - current.length;
|
|
1204
|
+
const catchupStep = gap > catchupThreshold ? Math.min(maxCatchupChars, Math.ceil(gap / 10)) : charsFromTime;
|
|
1205
|
+
const nextLength = Math.min(
|
|
1206
|
+
target.length,
|
|
1207
|
+
current.length + Math.max(charsFromTime, catchupStep)
|
|
1208
|
+
);
|
|
1209
|
+
const next = target.slice(0, nextLength);
|
|
1210
|
+
displayedRef.current = next;
|
|
1211
|
+
setDisplayedText(next);
|
|
1212
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
1213
|
+
};
|
|
1214
|
+
if (rafRef.current === null && displayedRef.current.length < text.length) {
|
|
1215
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
1216
|
+
}
|
|
1217
|
+
return () => {
|
|
1218
|
+
if (rafRef.current !== null) {
|
|
1219
|
+
cancelAnimationFrame(rafRef.current);
|
|
1220
|
+
rafRef.current = null;
|
|
1221
|
+
}
|
|
1222
|
+
lastFrameAtRef.current = null;
|
|
1223
|
+
};
|
|
1224
|
+
}, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars, text]);
|
|
1225
|
+
return {
|
|
1226
|
+
displayedText,
|
|
1227
|
+
isAnimating: isStreaming,
|
|
1228
|
+
mode: isStreaming ? "streaming" : "static",
|
|
1229
|
+
parseIncompleteMarkdown: isStreaming
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1096
1233
|
// src/transport.ts
|
|
1097
1234
|
var jsonHeaders = (headers) => ({
|
|
1098
1235
|
...headers || {},
|
|
@@ -1174,7 +1311,7 @@ var createFetchChatTransport = ({
|
|
|
1174
1311
|
};
|
|
1175
1312
|
|
|
1176
1313
|
// src/viewport.tsx
|
|
1177
|
-
var
|
|
1314
|
+
var import_react3 = require("react");
|
|
1178
1315
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1179
1316
|
var LATEST_EDGE_THRESHOLD_PX = 2;
|
|
1180
1317
|
var MIN_OLDER_PREFETCH_PX = 320;
|
|
@@ -1200,17 +1337,17 @@ function useLatestThreadViewport({
|
|
|
1200
1337
|
shouldAutoFollow = true,
|
|
1201
1338
|
topOriginMaxItems = 4
|
|
1202
1339
|
}) {
|
|
1203
|
-
const scrollContainerRef = (0,
|
|
1204
|
-
const olderPagePreserveRef = (0,
|
|
1205
|
-
const olderPageRequestInFlightRef = (0,
|
|
1206
|
-
const intrinsicResizePreserveRef = (0,
|
|
1207
|
-
const autoFollowLatestRef = (0,
|
|
1208
|
-
const programmaticScrollRef = (0,
|
|
1209
|
-
const programmaticScrollTimeoutRef = (0,
|
|
1210
|
-
const activeStreamKeyRef = (0,
|
|
1211
|
-
const [isAtLatest, setIsAtLatest] = (0,
|
|
1340
|
+
const scrollContainerRef = (0, import_react3.useRef)(null);
|
|
1341
|
+
const olderPagePreserveRef = (0, import_react3.useRef)(null);
|
|
1342
|
+
const olderPageRequestInFlightRef = (0, import_react3.useRef)(false);
|
|
1343
|
+
const intrinsicResizePreserveRef = (0, import_react3.useRef)(null);
|
|
1344
|
+
const autoFollowLatestRef = (0, import_react3.useRef)(true);
|
|
1345
|
+
const programmaticScrollRef = (0, import_react3.useRef)(false);
|
|
1346
|
+
const programmaticScrollTimeoutRef = (0, import_react3.useRef)(null);
|
|
1347
|
+
const activeStreamKeyRef = (0, import_react3.useRef)(null);
|
|
1348
|
+
const [isAtLatest, setIsAtLatest] = (0, import_react3.useState)(true);
|
|
1212
1349
|
const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
|
|
1213
|
-
const markProgrammaticScroll = (0,
|
|
1350
|
+
const markProgrammaticScroll = (0, import_react3.useCallback)(() => {
|
|
1214
1351
|
programmaticScrollRef.current = true;
|
|
1215
1352
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1216
1353
|
programmaticScrollTimeoutRef.current = setTimeout(() => {
|
|
@@ -1218,21 +1355,21 @@ function useLatestThreadViewport({
|
|
|
1218
1355
|
programmaticScrollTimeoutRef.current = null;
|
|
1219
1356
|
}, 80);
|
|
1220
1357
|
}, []);
|
|
1221
|
-
const scrollToLatest = (0,
|
|
1358
|
+
const scrollToLatest = (0, import_react3.useCallback)(() => {
|
|
1222
1359
|
const element = scrollContainerRef.current;
|
|
1223
1360
|
if (!element) return;
|
|
1224
1361
|
markProgrammaticScroll();
|
|
1225
1362
|
element.scrollTop = getLatestScrollTop(element, isTopOrigin);
|
|
1226
1363
|
setIsAtLatest(true);
|
|
1227
1364
|
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1228
|
-
(0,
|
|
1365
|
+
(0, import_react3.useEffect)(() => {
|
|
1229
1366
|
return () => {
|
|
1230
1367
|
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1231
1368
|
const preserve = intrinsicResizePreserveRef.current;
|
|
1232
1369
|
if (preserve?.frame !== null && preserve?.frame !== void 0) cancelAnimationFrame(preserve.frame);
|
|
1233
1370
|
};
|
|
1234
1371
|
}, []);
|
|
1235
|
-
(0,
|
|
1372
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1236
1373
|
const element = scrollContainerRef.current;
|
|
1237
1374
|
const preserve = olderPagePreserveRef.current;
|
|
1238
1375
|
if (!element) return;
|
|
@@ -1247,7 +1384,7 @@ function useLatestThreadViewport({
|
|
|
1247
1384
|
olderPageRequestInFlightRef.current = false;
|
|
1248
1385
|
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1249
1386
|
}, [isTopOrigin, itemCount, markProgrammaticScroll]);
|
|
1250
|
-
const handleScroll = (0,
|
|
1387
|
+
const handleScroll = (0, import_react3.useCallback)(
|
|
1251
1388
|
(event) => {
|
|
1252
1389
|
const element = event.currentTarget;
|
|
1253
1390
|
const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
|
|
@@ -1271,7 +1408,7 @@ function useLatestThreadViewport({
|
|
|
1271
1408
|
},
|
|
1272
1409
|
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
|
|
1273
1410
|
);
|
|
1274
|
-
const preserveIntrinsicResize = (0,
|
|
1411
|
+
const preserveIntrinsicResize = (0, import_react3.useCallback)(() => {
|
|
1275
1412
|
const element = scrollContainerRef.current;
|
|
1276
1413
|
if (!element || isTopOrigin) return;
|
|
1277
1414
|
const active = intrinsicResizePreserveRef.current;
|
|
@@ -1300,7 +1437,7 @@ function useLatestThreadViewport({
|
|
|
1300
1437
|
};
|
|
1301
1438
|
intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
|
|
1302
1439
|
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1303
|
-
(0,
|
|
1440
|
+
(0, import_react3.useEffect)(() => {
|
|
1304
1441
|
const key = activeStreamKey || null;
|
|
1305
1442
|
if (!key || activeStreamKeyRef.current === key) {
|
|
1306
1443
|
activeStreamKeyRef.current = key;
|
|
@@ -1315,7 +1452,7 @@ function useLatestThreadViewport({
|
|
|
1315
1452
|
activeStreamKeyRef.current = key;
|
|
1316
1453
|
return () => cancelAnimationFrame(frame);
|
|
1317
1454
|
}, [activeStreamKey, isTopOrigin, scrollToLatest]);
|
|
1318
|
-
(0,
|
|
1455
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1319
1456
|
if (isTopOrigin) return;
|
|
1320
1457
|
if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
|
|
1321
1458
|
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
@@ -1352,7 +1489,7 @@ function LatestThreadViewport({
|
|
|
1352
1489
|
onLoadOlder,
|
|
1353
1490
|
activeStreamKey
|
|
1354
1491
|
});
|
|
1355
|
-
const timelineItems = (0,
|
|
1492
|
+
const timelineItems = (0, import_react3.useMemo)(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
|
|
1356
1493
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1357
1494
|
"div",
|
|
1358
1495
|
{
|
|
@@ -1382,9 +1519,11 @@ function LatestThreadViewport({
|
|
|
1382
1519
|
consumeSseResponse,
|
|
1383
1520
|
createChatId,
|
|
1384
1521
|
createFetchChatTransport,
|
|
1522
|
+
getActiveStreamingTextPartId,
|
|
1385
1523
|
getLatestScrollTop,
|
|
1386
1524
|
hasStaleUnfinishedAssistantCache,
|
|
1387
1525
|
hasUnfinishedAssistantMessage,
|
|
1526
|
+
isActiveStreamingTextPart,
|
|
1388
1527
|
isAtTimelineLatestEdge,
|
|
1389
1528
|
isRunningThreadStatus,
|
|
1390
1529
|
isScrollable,
|
|
@@ -1403,6 +1542,7 @@ function LatestThreadViewport({
|
|
|
1403
1542
|
titleFromMessage,
|
|
1404
1543
|
toolStateFromStatus,
|
|
1405
1544
|
useAgents24ChatController,
|
|
1406
|
-
useLatestThreadViewport
|
|
1545
|
+
useLatestThreadViewport,
|
|
1546
|
+
useStreamingText
|
|
1407
1547
|
});
|
|
1408
1548
|
//# sourceMappingURL=index.cjs.map
|