WIP: work in progress on smooth-text-fade

This commit is contained in:
dobby-d-elf committed 2026-05-12 14:46:17 -06:00
1 parent c9c70fc1c5
commit d039270c23
3 files changed
+65 -5

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
# PR Notes: Opt-in Smooth Streaming Text Fade
## Summary
Adds an opt-in `Fade text effect` preference for live assistant responses. When enabled, newly revealed words fade in during streaming for a smoother ChatGPT/Codex-like feel while preserving the existing default streaming path when disabled.
## User-facing behavior
- New setting: `Settings -> Preferences -> Fade text effect`
- Default: off
- Runtime flag: `window._fadeTextEffect`
- Fade mode uses a playout buffer so fast backend chunks do not land as large paragraph pops.
- Visual reveal rate is intentionally capped for readability, especially with very fast models.
- Live cursor is hidden while fade mode is active.
- Reduced-motion users get non-animated text.
## Implementation notes
- Fade mode is locked per stream to avoid mid-stream preference toggle rewind/duplication.
- Uses Hermes' existing incremental `streaming-markdown` parser with a custom renderer instead of full markdown re-renders.
- Only newly appended words are wrapped and animated.
- Animated spans are replaced with plain text by a delegated `animationend` handler, avoiding long-lived wrapper buildup without per-word listeners.
- Reduced-motion preference is cached with a media-query listener instead of checked for every appended text node.
- Unsafe streamed `href`/`src` values are blocked in the fade renderer `set_attr` path.
- On `done`, fade mode drains buffered text, ends the parser to flush pending markdown, and waits for the final fade/stagger window before the final `renderMessages()` replacement.
## Performance/readability tuning
- Normal fade duration: `160ms`
- Word stagger: `12ms`
- Done drain wait cap: `220ms`
- Visual playback is capped at `100 wps`.
- Reveals at most `2 words/frame`, or `3 words/frame` only with very large backlog.
- Reveal steps pause briefly after sentence punctuation and paragraph breaks so very fast models feel less overwhelming.
- Target word counts reuse the already-visible word count plus backlog count, avoiding repeated full-response word scans on each tick.
## Verification
```bash
node --check static/messages.js static/panels.js static/boot.js static/i18n.js
git diff --check
/Users/agent/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_smooth_text_fade.py tests/test_1003_preferences_autosave.py tests/test_streaming_markdown.py -q
/Users/agent/.hermes/hermes-agent/venv/bin/python -m py_compile api/config.py
```
Latest focused result:
```text
68 passed
```
## Manual QA suggested
- Hard refresh after deployment.
- Enable `Settings -> Preferences -> Fade text effect`.
- Test short normal response.
- Test long markdown response with headings, lists, links, code blocks, and tables.
- Test very fast model output around 200-400 tok/s.
- Test tool-call-heavy response.
- Test OS/browser reduced-motion mode if available.
+3 -3
View File
@@ -538,9 +538,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
let _streamFadeHoldUntilMs=0;
let _streamFadeReduceMotionMql=null;
let _streamFadeReduceMotion=false;
const _STREAM_FADE_MS=160;
const _STREAM_FADE_STAGGER_MS=12;
const _STREAM_FADE_DONE_MAX_MS=220;
const _STREAM_FADE_MS=240;
const _STREAM_FADE_STAGGER_MS=16;
const _STREAM_FADE_DONE_MAX_MS=320;
const _streamFadeEnabledForStream=window._fadeTextEffect===true;
// rAF-throttled rendering: buffer tokens, render at most once per frame
+2 -2
View File
@@ -3746,8 +3746,8 @@ main.main.showing-logs > #mainLogs{display:flex;}
/* OpenWebUI-style streaming word fade (opt-in via Settings → Preferences).
Opacity-only fade with JS-paced word/paragraph reveal. */
.stream-fade-active .stream-fade-word{display:inline;}
.stream-fade-word.is-new{animation:stream-fade-word-in var(--stream-fade-ms,160ms) cubic-bezier(.2,.7,.2,1) both;will-change:opacity;}
@keyframes stream-fade-word-in{from{opacity:0;}to{opacity:1;}}
.stream-fade-word.is-new{animation:stream-fade-word-in var(--stream-fade-ms,240ms) cubic-bezier(.2,.7,.2,1) both;will-change:opacity;}
@keyframes stream-fade-word-in{0%{opacity:0;}45%{opacity:.45;}100%{opacity:1;}}
@media (prefers-reduced-motion: reduce){.stream-fade-word.is-new{animation:none;will-change:auto;}}
[data-live-assistant="1"]:last-child .msg-body.stream-fade-active > :last-child::after,
[data-live-assistant="1"]:last-child .msg-body.stream-fade-active:not(:has(> *))::after{display:none;content:none;}