@ykurilov/agent-chat-react 0.71.0 → 0.72.0
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @ykurilov/agent-chat-react
|
|
2
2
|
|
|
3
|
+
## 0.72.0
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 0.71.1
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 519914e: Лента закрепляет только сообщения у текущего окна, пока следует за ответом. При расхождении измеренных высот DOM и оценок виртуального списка больше не монтируется весь остаток длинной истории. Растущий ответ и черновик правки сохраняют отдельное закрепление.
|
|
12
|
+
|
|
13
|
+
Прилипание к низу сохраняется, когда пересчёт высот сдвигает сообщение и прокрутку на одинаковое расстояние вверх, даже при росте списка. Ввод читателя вверх по-прежнему останавливает следование за ответом.
|
|
14
|
+
|
|
15
|
+
Служебные обёртки сообщений не анимируют координаты: глобальное правило уменьшенного движения с ненулевой длительностью перехода больше не создаёт пустоты между строками в Safari.
|
|
16
|
+
|
|
17
|
+
Рост ответа не перезаписывает ещё не обработанный уход читателя вверх: положение учитывается до доводки низа, даже если браузер задержал событие scroll.
|
|
18
|
+
|
|
19
|
+
После открытия и немедленной отправки начальная прокрутка больше не удерживает асинхронную цель внизу: уход читателя вверх сохраняется при дальнейших замерах ответа. Обновление ничего не требует.
|
|
20
|
+
|
|
3
21
|
## 0.71.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @ykurilov/agent-chat-react
|
|
2
2
|
|
|
3
3
|
Поведение чата с агентом для React 19, без внешнего вида. Всё видимое рисует скин —
|
|
4
|
-
объект
|
|
4
|
+
объект компонентов. Это низкоуровневый API: `skin` обязателен.
|
|
5
|
+
|
|
6
|
+
Стандартный экран SDK - `AgentChat`, `ChatWorkspace`, `AgentPanel` из `@ykurilov/agent-chat-skin-oduvanchik/chat`: без `skin` используется Одуванчик, явный скин имеет приоритет. [Подключение CSS и переход](../../skins/oduvanchik/README.md). Существующий импорт из React сам не меняет оформление приложения.
|
|
5
7
|
|
|
6
8
|
```tsx
|
|
7
9
|
import { AgentChat, ChatLabelsProvider, russianLabels } from "@ykurilov/agent-chat-react";
|
|
@@ -4197,7 +4197,10 @@ var SCROLL_KEYS = /* @__PURE__ */ new Set([...JUMP_KEYS, "PageUp", "ArrowUp", "H
|
|
|
4197
4197
|
var READER_INPUT_WINDOW_MS = 250;
|
|
4198
4198
|
var UP_KEYS = /* @__PURE__ */ new Set(["PageUp", "ArrowUp", "Home"]);
|
|
4199
4199
|
function FeedItem({ style, children, ref }) {
|
|
4200
|
-
return /* @__PURE__ */ jsx3("div", { ref, style, "data-feed-item": "", children });
|
|
4200
|
+
return /* @__PURE__ */ jsx3("div", { ref, style: { ...style, transitionProperty: "none" }, "data-feed-item": "", children });
|
|
4201
|
+
}
|
|
4202
|
+
function FeedWindow({ style, children, ref }) {
|
|
4203
|
+
return /* @__PURE__ */ jsx3("div", { ref, style: { ...style, transitionProperty: "none" }, children });
|
|
4201
4204
|
}
|
|
4202
4205
|
function isTextField(target) {
|
|
4203
4206
|
if (!(target instanceof Element)) return false;
|
|
@@ -4214,6 +4217,7 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4214
4217
|
}, []);
|
|
4215
4218
|
const lastHeight = useRef7(0);
|
|
4216
4219
|
const lastTop = useRef7(0);
|
|
4220
|
+
const syncScrollState = useRef7(null);
|
|
4217
4221
|
const shrunkSinceScroll = useRef7(0);
|
|
4218
4222
|
const openedAtBottom = useRef7(false);
|
|
4219
4223
|
const userJumpUntil = useRef7(0);
|
|
@@ -4230,43 +4234,60 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4230
4234
|
const lastIndex = messages.length - 1;
|
|
4231
4235
|
useEffect6(() => setScrollerReady(true), []);
|
|
4232
4236
|
useLayoutEffect(() => {
|
|
4233
|
-
const
|
|
4237
|
+
const scroller2 = scrollRef.current;
|
|
4234
4238
|
const root = rootRef.current;
|
|
4235
|
-
if (!scrollerReady || !
|
|
4239
|
+
if (!scrollerReady || !scroller2 || !root) return;
|
|
4236
4240
|
const measure = () => {
|
|
4237
|
-
const offset = root.getBoundingClientRect().top -
|
|
4241
|
+
const offset = root.getBoundingClientRect().top - scroller2.getBoundingClientRect().top + scroller2.scrollTop;
|
|
4238
4242
|
setStartMargin(Math.max(0, Math.round(offset)));
|
|
4239
4243
|
};
|
|
4240
4244
|
measure();
|
|
4241
4245
|
const observer = new ResizeObserver(measure);
|
|
4242
|
-
observer.observe(
|
|
4246
|
+
observer.observe(scroller2);
|
|
4243
4247
|
return () => observer.disconnect();
|
|
4244
4248
|
}, [scrollRef, scrollerReady]);
|
|
4245
4249
|
useEffect6(() => {
|
|
4246
|
-
const
|
|
4247
|
-
if (!scrollerReady || !
|
|
4248
|
-
lastHeight.current =
|
|
4249
|
-
lastTop.current =
|
|
4250
|
+
const scroller2 = scrollRef.current;
|
|
4251
|
+
if (!scrollerReady || !scroller2) return;
|
|
4252
|
+
lastHeight.current = scroller2.scrollHeight;
|
|
4253
|
+
lastTop.current = scroller2.scrollTop;
|
|
4254
|
+
let anchor;
|
|
4255
|
+
const rememberAnchor = () => {
|
|
4256
|
+
const view = scroller2.getBoundingClientRect();
|
|
4257
|
+
for (const element of rootRef.current?.querySelectorAll("[data-feed-item]") ?? []) {
|
|
4258
|
+
if (element.style.visibility === "hidden") continue;
|
|
4259
|
+
const box = element.getBoundingClientRect();
|
|
4260
|
+
if (box.bottom > view.top && box.top < view.bottom) {
|
|
4261
|
+
anchor = { element, offset: box.top - view.top + scroller2.scrollTop };
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
anchor = void 0;
|
|
4266
|
+
};
|
|
4267
|
+
rememberAnchor();
|
|
4250
4268
|
const onScroll = () => {
|
|
4251
|
-
const height =
|
|
4252
|
-
const top =
|
|
4253
|
-
const near = Math.min(height, lastHeight.current) - top -
|
|
4269
|
+
const height = scroller2.scrollHeight;
|
|
4270
|
+
const top = scroller2.scrollTop;
|
|
4271
|
+
const near = Math.min(height, lastHeight.current) - top - scroller2.clientHeight < STICK_TO_BOTTOM_PX;
|
|
4254
4272
|
const shrunk = shrunkSinceScroll.current + Math.max(0, lastHeight.current - height);
|
|
4255
4273
|
shrunkSinceScroll.current = 0;
|
|
4256
4274
|
const step = top - lastTop.current;
|
|
4257
4275
|
const now = performance.now();
|
|
4258
4276
|
const readerActed = now < readerInputUntil.current || now < userJumpUntil.current;
|
|
4259
|
-
const
|
|
4277
|
+
const anchorShift = anchor && scroller2.contains(anchor.element) ? anchor.element.getBoundingClientRect().top - scroller2.getBoundingClientRect().top + top - anchor.offset : 0;
|
|
4278
|
+
const compensated = anchorShift < 0 && Math.abs(step - anchorShift) <= 1;
|
|
4279
|
+
const movedByLayout = step < 0 && (-step <= 2 * shrunk || compensated) && !readerActed;
|
|
4260
4280
|
lastTop.current = top;
|
|
4261
4281
|
lastHeight.current = height;
|
|
4282
|
+
rememberAnchor();
|
|
4262
4283
|
if (movedByLayout) {
|
|
4263
|
-
if (stuck.current)
|
|
4284
|
+
if (stuck.current) scroller2.scrollTop = height;
|
|
4264
4285
|
return;
|
|
4265
4286
|
}
|
|
4266
4287
|
if (step < 0) stick(readerActed ? false : near);
|
|
4267
4288
|
else if (step > 0) {
|
|
4268
4289
|
readerUpUntil.current = 0;
|
|
4269
|
-
if (near && (step <=
|
|
4290
|
+
if (near && (step <= scroller2.clientHeight || performance.now() < userJumpUntil.current)) stick(true);
|
|
4270
4291
|
}
|
|
4271
4292
|
};
|
|
4272
4293
|
const onKeyDown = (event) => {
|
|
@@ -4295,38 +4316,41 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4295
4316
|
const onPointerUp = () => {
|
|
4296
4317
|
if (userJumpUntil.current > performance.now() + POINTER_JUMP_WINDOW_MS) userJumpUntil.current = performance.now() + POINTER_JUMP_WINDOW_MS;
|
|
4297
4318
|
};
|
|
4298
|
-
|
|
4319
|
+
syncScrollState.current = onScroll;
|
|
4320
|
+
scroller2.addEventListener("scroll", onScroll, { passive: true });
|
|
4299
4321
|
document.addEventListener("keydown", onKeyDown);
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4322
|
+
scroller2.addEventListener("pointerdown", onPointerDown);
|
|
4323
|
+
scroller2.addEventListener("wheel", onWheel, { passive: true });
|
|
4324
|
+
scroller2.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
4325
|
+
scroller2.addEventListener("touchmove", onTouchMove, { passive: true });
|
|
4304
4326
|
window.addEventListener("pointerup", onPointerUp);
|
|
4305
4327
|
window.addEventListener("pointercancel", onPointerUp);
|
|
4306
4328
|
return () => {
|
|
4307
|
-
|
|
4329
|
+
syncScrollState.current = null;
|
|
4330
|
+
scroller2.removeEventListener("scroll", onScroll);
|
|
4308
4331
|
document.removeEventListener("keydown", onKeyDown);
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4332
|
+
scroller2.removeEventListener("pointerdown", onPointerDown);
|
|
4333
|
+
scroller2.removeEventListener("wheel", onWheel);
|
|
4334
|
+
scroller2.removeEventListener("touchstart", onTouchStart);
|
|
4335
|
+
scroller2.removeEventListener("touchmove", onTouchMove);
|
|
4313
4336
|
window.removeEventListener("pointerup", onPointerUp);
|
|
4314
4337
|
window.removeEventListener("pointercancel", onPointerUp);
|
|
4315
4338
|
};
|
|
4316
4339
|
}, [scrollRef, scrollerReady, stick]);
|
|
4317
4340
|
useEffect6(() => {
|
|
4318
|
-
const
|
|
4341
|
+
const scroller2 = scrollRef.current;
|
|
4319
4342
|
const root = rootRef.current;
|
|
4320
|
-
if (!scrollerReady || !
|
|
4343
|
+
if (!scrollerReady || !scroller2 || !root) return;
|
|
4321
4344
|
const settleAtBottom = () => {
|
|
4322
|
-
|
|
4323
|
-
const
|
|
4345
|
+
if (scroller2.scrollTop < lastTop.current) syncScrollState.current?.();
|
|
4346
|
+
const height = scroller2.scrollHeight;
|
|
4347
|
+
const clientHeight = scroller2.clientHeight;
|
|
4324
4348
|
shrunkSinceScroll.current += Math.max(0, lastHeight.current - height);
|
|
4325
4349
|
lastHeight.current = height;
|
|
4326
4350
|
const needed = height > settledHeight.current || clientHeight < settledClientHeight.current;
|
|
4327
4351
|
settledHeight.current = height;
|
|
4328
4352
|
settledClientHeight.current = clientHeight;
|
|
4329
|
-
if (stuck.current && needed && performance.now() >= readerUpUntil.current)
|
|
4353
|
+
if (stuck.current && needed && performance.now() >= readerUpUntil.current) scroller2.scrollTop = height;
|
|
4330
4354
|
};
|
|
4331
4355
|
let followUp = 0;
|
|
4332
4356
|
const follow = () => {
|
|
@@ -4341,7 +4365,7 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4341
4365
|
const listObserver = new MutationObserver(follow);
|
|
4342
4366
|
listObserver.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["style"] });
|
|
4343
4367
|
const scrollerObserver = new ResizeObserver(follow);
|
|
4344
|
-
scrollerObserver.observe(
|
|
4368
|
+
scrollerObserver.observe(scroller2);
|
|
4345
4369
|
return () => {
|
|
4346
4370
|
listObserver.disconnect();
|
|
4347
4371
|
scrollerObserver.disconnect();
|
|
@@ -4356,8 +4380,8 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4356
4380
|
setPositioned(false);
|
|
4357
4381
|
return;
|
|
4358
4382
|
}
|
|
4359
|
-
const
|
|
4360
|
-
if (!scrollerReady || !
|
|
4383
|
+
const scroller2 = scrollRef.current;
|
|
4384
|
+
if (!scrollerReady || !scroller2 || openedAtBottom.current) return;
|
|
4361
4385
|
openedAtBottom.current = true;
|
|
4362
4386
|
const focusIndex = openAtMessageId === void 0 ? -1 : messages.findIndex((message) => message.id === openAtMessageId);
|
|
4363
4387
|
stick(focusIndex < 0 || focusIndex === lastIndex);
|
|
@@ -4365,12 +4389,14 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4365
4389
|
virtualizerRef.current?.scrollToIndex(focusIndex, { align: "start" });
|
|
4366
4390
|
openingScrollUntil.current = performance.now() + OPENING_SCROLL_QUIET_MS;
|
|
4367
4391
|
} else {
|
|
4368
|
-
|
|
4392
|
+
queueMicrotask(() => {
|
|
4393
|
+
if (rootRef.current?.isConnected && scrollRef.current === scroller2 && stuck.current) scroller2.scrollTop = scroller2.scrollHeight;
|
|
4394
|
+
});
|
|
4369
4395
|
openingScrollUntil.current = 0;
|
|
4370
4396
|
}
|
|
4371
4397
|
let frame = 0;
|
|
4372
4398
|
const cancel = () => {
|
|
4373
|
-
|
|
4399
|
+
scroller2.removeEventListener("scroll", settle);
|
|
4374
4400
|
cancelAnimationFrame(frame);
|
|
4375
4401
|
cancelSettle.current = null;
|
|
4376
4402
|
};
|
|
@@ -4379,7 +4405,7 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4379
4405
|
setPositioned(true);
|
|
4380
4406
|
};
|
|
4381
4407
|
cancelSettle.current = cancel;
|
|
4382
|
-
|
|
4408
|
+
scroller2.addEventListener("scroll", settle, { once: true });
|
|
4383
4409
|
frame = requestAnimationFrame(() => {
|
|
4384
4410
|
frame = requestAnimationFrame(settle);
|
|
4385
4411
|
});
|
|
@@ -4389,19 +4415,22 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4389
4415
|
stick(true);
|
|
4390
4416
|
if (performance.now() < openingScrollUntil.current) virtualizerRef.current?.scrollTo(BOTTOM_SCROLL_TARGET);
|
|
4391
4417
|
openingScrollUntil.current = 0;
|
|
4392
|
-
const
|
|
4393
|
-
if (
|
|
4418
|
+
const scroller2 = scrollRef.current;
|
|
4419
|
+
if (scroller2) scroller2.scrollTop = scroller2.scrollHeight;
|
|
4394
4420
|
}, [scrollRef, stick]);
|
|
4395
4421
|
useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
|
|
4396
4422
|
const virtualizer = virtualizerRef.current;
|
|
4397
4423
|
const editedIndex = keepMountedMessageId === void 0 ? -1 : messages.findIndex((message) => message.id === keepMountedMessageId);
|
|
4398
|
-
const
|
|
4424
|
+
const scroller = scrollRef.current;
|
|
4425
|
+
const keepBuffer = positioned && followingBottom && virtualizer && scroller && lastIndex >= 0;
|
|
4426
|
+
const bufferStart = keepBuffer ? Math.max(0, virtualizer.findItemIndex(scroller.scrollTop - BUFFER_PX)) : 0;
|
|
4427
|
+
const bufferEnd = keepBuffer ? Math.min(lastIndex, virtualizer.findItemIndex(scroller.scrollTop + scroller.clientHeight)) : -1;
|
|
4399
4428
|
const keepMounted = useMemo6(() => {
|
|
4400
|
-
const kept = new Set(Array.from({ length: Math.max(0,
|
|
4429
|
+
const kept = new Set(Array.from({ length: Math.max(0, bufferEnd - bufferStart + 1) }, (_, offset) => bufferStart + offset));
|
|
4401
4430
|
if (keepLastMounted && lastIndex >= 0) kept.add(lastIndex);
|
|
4402
4431
|
if (editedIndex >= 0) kept.add(editedIndex);
|
|
4403
4432
|
return [...kept].sort((a, b) => a - b);
|
|
4404
|
-
}, [editedIndex, keepLastMounted, lastIndex
|
|
4433
|
+
}, [bufferEnd, bufferStart, editedIndex, keepLastMounted, lastIndex]);
|
|
4405
4434
|
return /* @__PURE__ */ jsx3("div", { ref: rootRef, children: scrollerReady && /* @__PURE__ */ jsx3(
|
|
4406
4435
|
Virtualizer,
|
|
4407
4436
|
{
|
|
@@ -4411,6 +4440,7 @@ function MessageFeedView({ scrollRef, messages, renderMessage, keepLastMounted,
|
|
|
4411
4440
|
itemSize: UNMEASURED_MESSAGE_HEIGHT_PX,
|
|
4412
4441
|
bufferSize: positioned ? BUFFER_PX : 0,
|
|
4413
4442
|
item: FeedItem,
|
|
4443
|
+
as: FeedWindow,
|
|
4414
4444
|
keepMounted,
|
|
4415
4445
|
startMargin,
|
|
4416
4446
|
children: (message, index) => renderMessage(message, index)
|
|
@@ -7194,4 +7224,4 @@ export {
|
|
|
7194
7224
|
activeChatTitle,
|
|
7195
7225
|
useWorkspaceListSync
|
|
7196
7226
|
};
|
|
7197
|
-
//# sourceMappingURL=chunk-
|
|
7227
|
+
//# sourceMappingURL=chunk-WJJY2WLQ.js.map
|