type
Post
status
Published
date
Dec 13, 2024
slug
内容自动跟随
summary
做 AI 聊天产品最容易翻车的细节之一,是「自动滚到底部」那个小逻辑。看着两行代码搞定,实际上踩坑能踩半年。这篇聊聊我做这一类功能时的几次反复。
tags
工具
开发
思考
category
技术分享
icon
password
synced
paired_with
3551d487-a2a1-81d8-ad0f-f1ba4443cdcb
source_hash
5702b96809e03a4e24bfe5b036f3fdf1e567dbe2ba8d9355b3b6a63fe6adee76
translation_locked
translation_locked
做 AI 聊天产品有一个细节最容易翻车,那就是"自动滚到底部"。AI 在流式输出,新字一个一个往外蹦,理想状态下窗口跟着滚到最新的位置。但如果用户中途想往上看一眼之前的内容,它又得识相地停下来。这个判断"用户现在到底想不想跟"的小逻辑,看着两行代码就能写完,实际上踩坑能踩半年。这篇聊聊我做这一类功能时的几次反复。
最简单的版本
第一版我和大多数人一样,用阈值。距离底部多少像素之内算"用户想跟",超出就停。
跑起来很丝滑。但只要用户想往上回看一段比较长的回答,就会发现一个尴尬的现象。AI 还在生成,每来一段新内容,scrollHeight 变大,distance 跟着变大,于是
shouldAutoScroll 翻成 false,看起来像"它停了"。但只要 distance 再次掉到阈值以内(比如用户继续往下滚),它又会自动跳到底部,把用户当前看的内容一下顶飞。第二版,加状态机
意识到这是状态问题之后,我把它改成显式的"跟随模式"。
关键是把"用户主动滚动"和"内容增长导致的位置变化"分开来。前者影响模式,后者只在 following 模式下才触发自动滚。
但还有个细节。怎么区分"用户主动滚"和"我自己滚的"?因为每次
scrollTop = scrollHeight 也会触发一次 scroll 事件。解决办法是写完之后立刻设个 flag长对话的性能问题
问题不止在状态切换。当一段对话长到几千行,DOM 节点全部留着,光是滚动本身就开始卡。这时候得上虚拟滚动,只渲染视窗附近的几屏内容。
但虚拟滚动给"自动跟到底部"这件事增加了难度。
scrollHeight 是按"假装所有 item 都渲染了"算的虚拟高度,所以 scrollTop = scrollHeight 不再等于"滚到最后一条"。要专门写一个 scrollToIndex(lastIndex) 才行。那些更隐蔽的边界情况
写这种自动滚的时候,能掉进的坑远不止上面几个。
- 窗口缩放。用户拉大浏览器窗口,clientHeight 变了,distance 算法重新跑一遍判断
- 字号变化。用户按 Ctrl 加号放大字体,每个 item 的高度变了,虚拟滚动里假定的 itemHeight 失效
- 图片加载。AI 输出里嵌了图,图加载完之前 DOM 里它的高度是 0,加载完后突然撑高,跟随逻辑乱跳
- 键盘事件。用户按 Page Up 翻页,应该被识别为"想自由浏览",进入 free 模式
- 触摸滑动。手机端用户用手指滑屏,wheel 事件不会触发,得听 touchmove
每一个都不难,但加起来够你写一个礼拜的。
写在最后
最后我学到一件事,跟用户意图打交道的功能,不要去预测用户想要什么,而是老老实实记录用户做了什么。所谓"智能跟随",本质上是把用户的滚动行为分类(主动离开 / 主动回归 / 被动跳变)然后按规则响应。机器学习不是这件事的好答案,状态机才是。
📎 参考文章
- 作者:LeoQin
- 链接:https://leoqin.com/article/%E5%86%85%E5%AE%B9%E8%87%AA%E5%8A%A8%E8%B7%9F%E9%9A%8F
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。