fix(tutor): the log stopped following a reply sent from answer mode

Submitting from answer mode brings back the earlier messages, the composer
and the nav in one layout. That reset the log's scrollTop to 0, and the
scroll event it fired read as the learner scrolling up — undoing the follow
send() had just armed, so the reply landed out of view, 1,812px below. A
move made while the log's content or box changed size is now taken for
layout, not for the learner. Scrolling up during a reply still lets go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-16 22:50:37 +02:00
parent 467d9d1d7c
commit 5d844efa09

View File

@@ -206,8 +206,9 @@ export function TutorTab() {
const log = useRef<HTMLDivElement>(null);
/** The log follows new text while this is set; see the follow effect. */
const stick = useRef(true);
/** Where the log was last scrolled to; see onLogScroll. */
/** Where the log was last scrolled to, and how tall it was; see onLogScroll. */
const lastTop = useRef(0);
const lastHeights = useRef({ content: 0, box: 0 });
const menuButton = useRef<HTMLButtonElement>(null);
/* ── the keyboard's field, and answer mode ── */
@@ -716,15 +717,24 @@ export function TutorTab() {
stream has added more than STICK_PX below it — judged by distance
alone, that read as the learner scrolling away, and the log stopped
following mid-reply. Growing content never moves scrollTop up; a
finger does. */
finger does.
And a finger does not change the log's height. Leaving answer mode
brings back the earlier messages, the composer and the nav in one
layout, which reset scrollTop to 0 — an upward jump that read as
scrolling away, just as the answer was sent. A move made while the
content or the box changed size is layout, not the learner. */
const onLogScroll = useCallback(() => {
const el = log.current;
if (!el || !active) return;
const top = el.scrollTop;
const resized =
el.scrollHeight !== lastHeights.current.content || el.clientHeight !== lastHeights.current.box;
if (el.scrollHeight - top - el.clientHeight < STICK_PX) stick.current = true;
else if (top < lastTop.current - 1) stick.current = false;
else if (top < lastTop.current - 1 && !resized) stick.current = false;
lastTop.current = top;
lastHeights.current = { content: el.scrollHeight, box: el.clientHeight };
}, [active]);
const follow = useCallback(() => {
@@ -732,6 +742,7 @@ export function TutorTab() {
if (!el || !stick.current) return;
el.scrollTop = el.scrollHeight;
lastTop.current = el.scrollTop;
lastHeights.current = { content: el.scrollHeight, box: el.clientHeight };
}, []);
useEffect(() => {