@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
- объект из десяти компонентов. Готовый скин Тандема — пакет `@ykurilov/agent-chat-skin-tandem`.
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 scroller = scrollRef.current;
4237
+ const scroller2 = scrollRef.current;
4234
4238
  const root = rootRef.current;
4235
- if (!scrollerReady || !scroller || !root) return;
4239
+ if (!scrollerReady || !scroller2 || !root) return;
4236
4240
  const measure = () => {
4237
- const offset = root.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
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(scroller);
4246
+ observer.observe(scroller2);
4243
4247
  return () => observer.disconnect();
4244
4248
  }, [scrollRef, scrollerReady]);
4245
4249
  useEffect6(() => {
4246
- const scroller = scrollRef.current;
4247
- if (!scrollerReady || !scroller) return;
4248
- lastHeight.current = scroller.scrollHeight;
4249
- lastTop.current = scroller.scrollTop;
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 = scroller.scrollHeight;
4252
- const top = scroller.scrollTop;
4253
- const near = Math.min(height, lastHeight.current) - top - scroller.clientHeight < STICK_TO_BOTTOM_PX;
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 movedByLayout = step < 0 && -step <= 2 * shrunk && !readerActed;
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) scroller.scrollTop = height;
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 <= scroller.clientHeight || performance.now() < userJumpUntil.current)) stick(true);
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
- scroller.addEventListener("scroll", onScroll, { passive: true });
4319
+ syncScrollState.current = onScroll;
4320
+ scroller2.addEventListener("scroll", onScroll, { passive: true });
4299
4321
  document.addEventListener("keydown", onKeyDown);
4300
- scroller.addEventListener("pointerdown", onPointerDown);
4301
- scroller.addEventListener("wheel", onWheel, { passive: true });
4302
- scroller.addEventListener("touchstart", onTouchStart, { passive: true });
4303
- scroller.addEventListener("touchmove", onTouchMove, { passive: true });
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
- scroller.removeEventListener("scroll", onScroll);
4329
+ syncScrollState.current = null;
4330
+ scroller2.removeEventListener("scroll", onScroll);
4308
4331
  document.removeEventListener("keydown", onKeyDown);
4309
- scroller.removeEventListener("pointerdown", onPointerDown);
4310
- scroller.removeEventListener("wheel", onWheel);
4311
- scroller.removeEventListener("touchstart", onTouchStart);
4312
- scroller.removeEventListener("touchmove", onTouchMove);
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 scroller = scrollRef.current;
4341
+ const scroller2 = scrollRef.current;
4319
4342
  const root = rootRef.current;
4320
- if (!scrollerReady || !scroller || !root) return;
4343
+ if (!scrollerReady || !scroller2 || !root) return;
4321
4344
  const settleAtBottom = () => {
4322
- const height = scroller.scrollHeight;
4323
- const clientHeight = scroller.clientHeight;
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) scroller.scrollTop = height;
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(scroller);
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 scroller = scrollRef.current;
4360
- if (!scrollerReady || !scroller || openedAtBottom.current) return;
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
- virtualizerRef.current?.scrollTo(BOTTOM_SCROLL_TARGET);
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
- scroller.removeEventListener("scroll", settle);
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
- scroller.addEventListener("scroll", settle, { once: true });
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 scroller = scrollRef.current;
4393
- if (scroller) scroller.scrollTop = scroller.scrollHeight;
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 tailStart = positioned && followingBottom && virtualizer && lastIndex >= 0 ? Math.min(lastIndex, Math.max(0, virtualizer.findItemIndex(virtualizer.scrollOffset - BUFFER_PX))) : lastIndex + 1;
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, lastIndex + 1 - tailStart) }, (_, offset) => tailStart + offset));
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, tailStart]);
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-W5VBPNFF.js.map
7227
+ //# sourceMappingURL=chunk-WJJY2WLQ.js.map