@jskit-ai/assistant-core 0.2.3 → 0.2.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/package.json
CHANGED
|
@@ -447,6 +447,7 @@ let retainedReaderScrollSnapshot = null;
|
|
|
447
447
|
let loadMoreRequestVersion = 0;
|
|
448
448
|
let pendingTailFollow = false;
|
|
449
449
|
let previousScrollTop = 0;
|
|
450
|
+
let previousScrollHeight = 0;
|
|
450
451
|
let userScrollIntentUpward = false;
|
|
451
452
|
let lastTouchY = null;
|
|
452
453
|
const USER_SCROLL_INTENT_RESET_MS = 600;
|
|
@@ -881,11 +882,17 @@ function updateLatestFollowFromScroll(event = {}) {
|
|
|
881
882
|
const target = event?.currentTarget || bodyElement.value;
|
|
882
883
|
if (retainedReaderScrollSnapshot?.restoring) {
|
|
883
884
|
previousScrollTop = target?.scrollTop || 0;
|
|
885
|
+
previousScrollHeight = target?.scrollHeight || 0;
|
|
884
886
|
return;
|
|
885
887
|
}
|
|
886
888
|
const scrollTop = target?.scrollTop || 0;
|
|
889
|
+
const scrollHeight = target?.scrollHeight || 0;
|
|
887
890
|
const scrolledUp = scrollTop < previousScrollTop;
|
|
891
|
+
// Layout changes such as scroll anchoring move scrollTop together with the
|
|
892
|
+
// content height; moving up over unchanged content is the reader's own.
|
|
893
|
+
const readerMovedUp = scrolledUp && scrollHeight === previousScrollHeight;
|
|
888
894
|
previousScrollTop = scrollTop;
|
|
895
|
+
previousScrollHeight = scrollHeight;
|
|
889
896
|
// Keep following the reader while the request is pending, before rows prepend.
|
|
890
897
|
if (loadMoreScrollSnapshot && !loadMoreScrollSnapshot.completed) {
|
|
891
898
|
loadMoreScrollSnapshot.scrollTop = scrollTop;
|
|
@@ -897,7 +904,7 @@ function updateLatestFollowFromScroll(event = {}) {
|
|
|
897
904
|
const userScrollsUp = userScrollIntent.value && userScrollIntentUpward;
|
|
898
905
|
const shouldFollow = scrollElementNearBottom(target) &&
|
|
899
906
|
!(userScrollsUp && scrollDistanceFromBottom(target) > 1);
|
|
900
|
-
if (!shouldFollow && !userScrollIntent.value && followingLatest.value) {
|
|
907
|
+
if (!shouldFollow && !userScrollIntent.value && !readerMovedUp && followingLatest.value) {
|
|
901
908
|
return;
|
|
902
909
|
}
|
|
903
910
|
followingLatest.value = shouldFollow;
|
|
@@ -51,6 +51,30 @@ async function scrollGesture(body, lastEventType, perform) {
|
|
|
51
51
|
await body.evaluate(element => element.gestureScrollEnded);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Chromium animates a keyboard scroll towards a target fixed at the key press
|
|
55
|
+
// while content-visibility rows keep changing the transcript's height, so the
|
|
56
|
+
// animation can stop short of the end or not move at all. Keep the real key
|
|
57
|
+
// event for the transcript's own handling, replace only the browser's scroll
|
|
58
|
+
// with a move to `target` ("end", "top" or a pixel offset), and wait until the
|
|
59
|
+
// transcript has handled that scroll.
|
|
60
|
+
async function pressScrollKey(body, key, target) {
|
|
61
|
+
await body.evaluate((element, [scrollKey, scrollTarget]) => {
|
|
62
|
+
element.keyScrolled = new Promise(resolve => {
|
|
63
|
+
element.addEventListener("keydown", function move(event) {
|
|
64
|
+
if (event.key !== scrollKey) return;
|
|
65
|
+
element.removeEventListener("keydown", move);
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
element.addEventListener("scroll", () => resolve(), { once: true });
|
|
68
|
+
element.scrollTop = scrollTarget === "end"
|
|
69
|
+
? element.scrollHeight
|
|
70
|
+
: scrollTarget === "top" ? 0 : element.scrollTop + scrollTarget;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}, [key, target]);
|
|
74
|
+
await body.press(key);
|
|
75
|
+
await body.evaluate(element => element.keyScrolled);
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
const VIEWPORTS = Object.freeze([
|
|
55
79
|
Object.freeze({ name: "phone", width: 390, height: 844 }),
|
|
56
80
|
Object.freeze({ name: "compact", width: 800, height: 900 }),
|
|
@@ -428,13 +452,12 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
428
452
|
await expect(requests).toHaveText("History requests: 1");
|
|
429
453
|
|
|
430
454
|
// Keyboard navigation also requests older history.
|
|
431
|
-
await body
|
|
432
|
-
await body.press("Home");
|
|
455
|
+
await pressScrollKey(body, "Home", "top");
|
|
433
456
|
await expect(requests).toHaveText("History requests: 2");
|
|
434
457
|
await external("Fail history load");
|
|
435
458
|
await expect(page.getByText("History unavailable", { exact: true })).toBeVisible();
|
|
436
|
-
await body
|
|
437
|
-
await body
|
|
459
|
+
await pressScrollKey(body, "ArrowDown", 40);
|
|
460
|
+
await pressScrollKey(body, "Home", "top");
|
|
438
461
|
await expect(requests).toHaveText("History requests: 2");
|
|
439
462
|
await page.getByRole("button", { name: "Load older messages", exact: true }).click();
|
|
440
463
|
await expect(requests).toHaveText("History requests: 3");
|
|
@@ -445,16 +468,12 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
445
468
|
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
|
|
446
469
|
await expect(requests).toHaveText("History requests: 3");
|
|
447
470
|
await external("Exhaust history");
|
|
448
|
-
// Removing the history control re-anchors the scroller
|
|
449
|
-
//
|
|
471
|
+
// Removing the history control re-anchors the scroller; press Home once
|
|
472
|
+
// it settles.
|
|
450
473
|
await expect(page.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
|
|
451
474
|
await settleLayout(body);
|
|
452
|
-
await
|
|
453
|
-
|
|
454
|
-
return body.evaluate(element => document.activeElement === element);
|
|
455
|
-
}).toBe(true);
|
|
456
|
-
await body.press("Home");
|
|
457
|
-
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(0);
|
|
475
|
+
await pressScrollKey(body, "Home", "top");
|
|
476
|
+
expect(await body.evaluate(element => element.scrollTop)).toBe(0);
|
|
458
477
|
await expect(requests).toHaveText("History requests: 3");
|
|
459
478
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
460
479
|
assert.deepEqual(errors, []);
|
|
@@ -519,24 +538,21 @@ test("a reader's own upward scrolling leaves the latest message from its first f
|
|
|
519
538
|
expect(await fromBottom()).toBeGreaterThan(1);
|
|
520
539
|
|
|
521
540
|
// Returning to the end resumes following new replies.
|
|
522
|
-
await body
|
|
523
|
-
await expect.poll(fromBottom).toBeLessThanOrEqual(1);
|
|
541
|
+
await pressScrollKey(body, "End", "end");
|
|
524
542
|
await appendReply();
|
|
525
543
|
expect(await fromBottom()).toBeLessThanOrEqual(1);
|
|
526
544
|
|
|
527
|
-
// The first frame of an upward scroll lands inside the follow
|
|
528
|
-
// the rest of it (a fling or smooth scroll) moves the reader
|
|
529
|
-
// without further input events.
|
|
530
|
-
await body
|
|
531
|
-
await
|
|
532
|
-
|
|
533
|
-
expect(await fromBottom()).toBeLessThanOrEqual(48);
|
|
545
|
+
// The first frame of an upward keyboard scroll lands inside the follow
|
|
546
|
+
// threshold; the rest of it (a fling or smooth scroll) moves the reader
|
|
547
|
+
// into history without further input events.
|
|
548
|
+
await pressScrollKey(body, "ArrowUp", -10);
|
|
549
|
+
const firstFrame = await fromBottom();
|
|
550
|
+
assert.ok(firstFrame > 1 && firstFrame <= 48, `first frame ${firstFrame}px from the end`);
|
|
534
551
|
await body.evaluate(element => { element.scrollTop = Math.round(element.scrollTop / 2); });
|
|
535
552
|
await settleLayout(body);
|
|
536
553
|
await appendReply();
|
|
537
554
|
expect(await fromBottom()).toBeGreaterThan(1_000);
|
|
538
|
-
await body
|
|
539
|
-
await expect.poll(fromBottom).toBeLessThanOrEqual(1);
|
|
555
|
+
await pressScrollKey(body, "End", "end");
|
|
540
556
|
await appendReply();
|
|
541
557
|
expect(await fromBottom()).toBeLessThanOrEqual(1);
|
|
542
558
|
|
|
@@ -546,8 +562,7 @@ test("a reader's own upward scrolling leaves the latest message from its first f
|
|
|
546
562
|
await body.evaluate(element => {
|
|
547
563
|
element.addEventListener("keydown", () => element.dispatchEvent(new Event("scroll")), { once: true });
|
|
548
564
|
});
|
|
549
|
-
await body
|
|
550
|
-
await expect.poll(() => body.evaluate(element => element.scrollTop)).toBe(0);
|
|
565
|
+
await pressScrollKey(body, "Home", "top");
|
|
551
566
|
await appendReply();
|
|
552
567
|
expect(await body.evaluate(element => element.scrollTop)).toBe(0);
|
|
553
568
|
assert.deepEqual(errors, []);
|
|
@@ -559,6 +574,57 @@ test("a reader's own upward scrolling leaves the latest message from its first f
|
|
|
559
574
|
}
|
|
560
575
|
});
|
|
561
576
|
|
|
577
|
+
test("a reader moved away from the end without a scroll direction stays there", {
|
|
578
|
+
skip: !RUN_BROWSER_TEST,
|
|
579
|
+
timeout: 60_000
|
|
580
|
+
}, async () => {
|
|
581
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
582
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
583
|
+
try {
|
|
584
|
+
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
585
|
+
const errors = [];
|
|
586
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
587
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1`);
|
|
588
|
+
const body = page.locator(".assistant-transcript__body");
|
|
589
|
+
const fromBottom = () => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop);
|
|
590
|
+
// Move the reader and wait until the transcript has handled the scroll.
|
|
591
|
+
const moveReader = target => body.evaluate((element, by) => new Promise(resolve => {
|
|
592
|
+
element.addEventListener("scroll", () => resolve(), { once: true });
|
|
593
|
+
element.scrollTop = by === "end" ? element.scrollHeight : element.scrollTop + by;
|
|
594
|
+
}), target);
|
|
595
|
+
const appendReply = async () => {
|
|
596
|
+
const replies = page.getByText("New reply.", { exact: false });
|
|
597
|
+
const count = await replies.count();
|
|
598
|
+
await page.getByRole("button", { name: "Append reply", exact: true }).evaluate(element => element.click());
|
|
599
|
+
await expect(replies).toHaveCount(count + 1);
|
|
600
|
+
await settleLayout(body);
|
|
601
|
+
};
|
|
602
|
+
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
|
|
603
|
+
await page.getByRole("button", { name: "Exhaust history", exact: true }).evaluate(element => element.click());
|
|
604
|
+
await settleLayout(body);
|
|
605
|
+
expect(await fromBottom()).toBeLessThanOrEqual(1);
|
|
606
|
+
|
|
607
|
+
// A scrollbar drag starts on the scroller itself and carries no scroll
|
|
608
|
+
// direction; its frames, the first inside the follow threshold, then move
|
|
609
|
+
// the reader without further input. Headless Chromium hides scrollbars, so
|
|
610
|
+
// the drag's pointerdown is dispatched directly.
|
|
611
|
+
await body.evaluate(element => element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })));
|
|
612
|
+
await moveReader(-10);
|
|
613
|
+
await moveReader(-300);
|
|
614
|
+
await appendReply();
|
|
615
|
+
expect(await fromBottom()).toBeGreaterThan(300);
|
|
616
|
+
|
|
617
|
+
// Returning to the end resumes following new replies.
|
|
618
|
+
await moveReader("end");
|
|
619
|
+
await appendReply();
|
|
620
|
+
expect(await fromBottom()).toBeLessThanOrEqual(1);
|
|
621
|
+
assert.deepEqual(errors, []);
|
|
622
|
+
} finally {
|
|
623
|
+
await browser.close();
|
|
624
|
+
await stopProcess(vite);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
562
628
|
test("retained hidden transcripts preserve readers and follow only at the latest message", {
|
|
563
629
|
skip: !RUN_BROWSER_TEST,
|
|
564
630
|
timeout: 90_000
|