-
-
Notifications
You must be signed in to change notification settings - Fork 12.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
💄 style: Optimize smooth output #5824
Conversation
@sxjeru is attempting to deploy a commit to the LobeHub Team on Vercel. A member of the Team first needs to authorize it. |
👍 @sxjeru Thank you for raising your pull request and contributing to our Community |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5824 +/- ##
==========================================
- Coverage 91.63% 91.62% -0.01%
==========================================
Files 699 699
Lines 65828 65850 +22
Branches 4458 4656 +198
==========================================
+ Hits 60319 60334 +15
- Misses 5509 5516 +7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
要是影响到英文用户聊天就不太好了吧🤔 |
It would be bad if it affects English user chats 🤔 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
看起来没有问题
@sxjeru 和 sonnet 3.5 聊了下,看下有没有必要做这里提到的后台切 1. 页面可见性问题在浏览器中,当页面切换到后台时,
代码中通过以下几个关键机制来处理这个问题: 2. 基于时间的动画系统const updateText = (timestamp: number) => {
if (!lastFrameTime) {
lastFrameTime = timestamp;
}
// 1. 计算实际经过的时间
const frameDuration = timestamp - lastFrameTime;
lastFrameTime = timestamp;
// 2. 基于实际时间计算应该处理的字符数
const targetFrameDuration = Math.floor(1000 / 60); // 理想帧时长
charAccumulator += (frameDuration / targetFrameDuration) * charsPerFrame;
const charsToProcess = Math.floor(charAccumulator);
if (charsToProcess > 0) {
charAccumulator -= charsToProcess;
// 处理字符...
}
} 这种基于时间的实现有以下优势:
3. 改进版本:增加页面可见性处理以下是一个更健壮的实现方案,增加了对页面可见性的处理: const createSmoothMessage = (params: {
onTextUpdate: (delta: string, text: string) => void;
startSpeed?: number;
}) => {
let buffer = '';
let outputQueue: string[] = [];
let isAnimationActive = false;
let animationFrameId: number | null = null;
let lastFrameTime = 0;
let charAccumulator = 0;
// 添加计时器相关变量
let lastProcessTime = 0;
let intervalId: number | null = null;
// 页面可见性变化处理
const handleVisibilityChange = () => {
if (document.hidden) {
// 页面进入后台,切换到 setInterval
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (isAnimationActive && !intervalId) {
lastProcessTime = Date.now();
intervalId = window.setInterval(updateTextInterval, 1000 / 30); // 30fps in background
}
} else {
// 页面回到前台,切换回 requestAnimationFrame
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
if (isAnimationActive) {
lastFrameTime = 0; // 重置帧时间
animationFrameId = requestAnimationFrame(updateText);
}
}
};
// 后台处理文本的函数
const updateTextInterval = () => {
const now = Date.now();
const duration = now - lastProcessTime;
lastProcessTime = now;
const targetFrameDuration = 1000 / 60;
charAccumulator += (duration / targetFrameDuration) * (params.startSpeed || 4);
const charsToProcess = Math.floor(charAccumulator);
if (charsToProcess > 0) {
charAccumulator -= charsToProcess;
const actualChars = Math.min(charsToProcess, outputQueue.length);
const charsToAdd = outputQueue.splice(0, actualChars).join('');
buffer += charsToAdd;
params.onTextUpdate(charsToAdd, buffer);
}
if (outputQueue.length === 0) {
isAnimationActive = false;
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
};
// 前台动画更新函数
const updateText = (timestamp: number) => {
if (!isAnimationActive) {
animationFrameId = null;
return;
}
if (!lastFrameTime) lastFrameTime = timestamp;
const frameDuration = timestamp - lastFrameTime;
lastFrameTime = timestamp;
const targetFrameDuration = Math.floor(1000 / 60);
charAccumulator += (frameDuration / targetFrameDuration) * (params.startSpeed || 4);
const charsToProcess = Math.floor(charAccumulator);
if (charsToProcess > 0) {
charAccumulator -= charsToProcess;
const actualChars = Math.min(charsToProcess, outputQueue.length);
const charsToAdd = outputQueue.splice(0, actualChars).join('');
buffer += charsToAdd;
params.onTextUpdate(charsToAdd, buffer);
}
if (outputQueue.length > 0) {
animationFrameId = requestAnimationFrame(updateText);
} else {
isAnimationActive = false;
animationFrameId = null;
}
};
// 启动动画
const startAnimation = () => {
if (isAnimationActive) return;
isAnimationActive = true;
lastFrameTime = 0;
charAccumulator = 0;
if (document.hidden) {
lastProcessTime = Date.now();
intervalId = window.setInterval(updateTextInterval, 1000 / 30);
} else {
animationFrameId = requestAnimationFrame(updateText);
}
};
// 注册页面可见性变化监听
document.addEventListener('visibilitychange', handleVisibilityChange);
return {
destroy: () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
if (intervalId !== null) clearInterval(intervalId);
},
isAnimationActive: () => isAnimationActive,
pushToQueue: (text: string) => {
outputQueue.push(...text.split(''));
},
startAnimation,
stopAnimation: () => {
isAnimationActive = false;
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
};
}; 4. 关键改进点
这种实现方式能够保证:
这样的设计让动画系统在各种场景下都能保持稳定的表现,提供了更好的用户体验。 |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
其实这里的问题是不一次输出完没法 save content 到 message 里。或者更合适的方案是不做加速逻辑,一次性全部更新掉? |
In fact, the problem here is that you cannot save content into the message if you output it once. Or is a more suitable solution to not do acceleration logic and update them all at once? |
一次性全部输出确实不错,既简单又符合直觉。不过输出时最好关闭页面自动下滑到底部。 今天测试了很久,感觉页面自动滚到底还是有问题,常常不会自动跟随,有待进一步优化。 输出效果还是不错的。 |
The output at one time is really good, both simple and intuitive. However, it is best to close the page and automatically slide to the bottom when outputting. I have been testing it for a long time today, and I feel that there is still a problem with the page automatically rolling to the end. I often don’t follow it automatically and need further optimization. |
英文二倍速显示后,英文的输出速度与中文基本保持一致,在中英文混杂时会有明显感觉。 这里的输出速度是指观感上打字机从左往右刷新的速度,吐字符的速度。 default.mp4 |
After the English double speed is displayed, the English output speed is basically the same as that in Chinese, and there will be obvious feelings in the situation shown in the figure. The output speed here refers to the speed of the typewriter refreshing from left to right and the speed of the character ejaculation on the visual sense. |
调整了一下代码,现在用 Gemini 等比较快的模型,会自动加速输出了。也有一些问题:
对于 openrouter 这种流式返回非常快又非常零碎的 api,还是起不到平滑输出的效果。 对于 Qwen 这种慢速 api,效果还是可以的。 |
After adjusting the code, I now use relatively fast models such as Gemini to automatically accelerate the output. There are also some problems:
|
不需要,speed 参数之前是因为没有自适应速度调节,所以才不得不加的参数。应该可以直接去掉?或者看下是否在哪些场景会有希望限速的诉求
问题不大,反正现在也是加速输出的,如果输出速度更快的话,算是体验提升。除非新的输出速度明显比之前的慢了很多,才有必要这么做。 |
No, the speed parameter was previously due to the lack of adaptive speed adjustment, so the parameters had to be added. Should it be removed directly? Or see if there is a hope for speed limit in which scenarios
There is no big problem. Anyway, it is accelerating output now. If the output speed is faster, it will be considered an improvement in experience. This is necessary unless the new output speed is significantly slower than before. |
// 更平滑的速度调整 | ||
const targetSpeed = Math.max(speed, outputQueue.length); | ||
// 根据队列长度变化调整速度变化率 | ||
const speedChangeRate = Math.abs(outputQueue.length - lastQueueLength) * 0.0008 + 0.005; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里的 0.0008 和 0.005 是什么意思?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0.0008 对应输出速度的变化速度,越小速度变化越平缓。0.005 是速度变化最小值,避免速度变化的太慢。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个最好抽取来变成常数,并且加上注释。以及为什么它的单位是 0.000 ? 是有什么含义吗?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个其实我也不太清楚,两个数都是 AI 给出的,我这边测试效果不错就没有改动。
单测挂了要修一下? |
If you fail a single test, you need to repair it? |
只从单测结果看,貌似当 reason 有很多内容,而 text 内容很短时,输出完 text 后就会结束,不再输出所有 reason 。 另外这个单测有概率失败,可能因为速度平滑调整的不确定性,很难定位 text 的 "hi" 会在何时开始输出,考虑把它删了。 现在 text 有内容时就会直接输出,而不是等待 reason 输出完后再继续。 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我先合并解决后台输出的问题吧,感觉目前的 smoothing 算法仍然不是最优解,后续要继续看下高速 token 输出下的优化
❤️ Great PR @sxjeru ❤️ The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our discord and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world. |
### [Version 1.66.4](v1.66.3...v1.66.4) <sup>Released on **2025-02-28**</sup> #### 💄 Styles - **misc**: Optimize smooth output. <br/> <details> <summary><kbd>Improvements and Fixes</kbd></summary> #### Styles * **misc**: Optimize smooth output, closes [#5824](#5824) ([7a84ad9](7a84ad9)) </details> <div align="right"> [](#readme-top) </div>
🎉 This PR is included in version 1.66.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
### [Version 1.107.2](v1.107.1...v1.107.2) <sup>Released on **2025-02-28**</sup> #### 💄 Styles - **misc**: Improve portal style, Optimize smooth output. <br/> <details> <summary><kbd>Improvements and Fixes</kbd></summary> #### Styles * **misc**: Improve portal style, closes [lobehub#6588](https://github.com/bentwnghk/lobe-chat/issues/6588) ([55b5416](55b5416)) * **misc**: Optimize smooth output, closes [lobehub#5824](https://github.com/bentwnghk/lobe-chat/issues/5824) ([7a84ad9](7a84ad9)) </details> <div align="right"> [](#readme-top) </div>
💻 变更类型 | Change Type
🔀 变更说明 | Description of Change
将smoothing.speed
定义为每秒输出 x 个字符;现在 speed 为起始速度。
取消后段加速输出,这个可以讨论下,个人感觉如果输出速度匹配阅读速度时,突然加速完全是破坏体验的。立刻输出剩余内容时,不会自动跳转到底部;最好是做成开关,是否需要自动下滑。
为英文数字字符加速输出(待讨论,可以考虑环境变量控制启用)已不再需要。第二个请求是因为中日韩等字符的信息量显著高于英文字母,加速输出可以减少代码等输出的等待时间。
(可能会影响英文用户正常聊天?)
📝 补充信息 | Additional Information