WIP: work in progress on smooth-text-fade
This commit is contained in:
1 parent
c9c70fc1c5
commit
d039270c23
3 files changed
+65
-5
No files matched your search
+60
@@ -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
@@ -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
@@ -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;}
|
||||
Reference in new issue
Block a user