From c80fdd86bdf49ef218134e818e0278ee6c1b9c63 Mon Sep 17 00:00:00 2001 From: charan Date: Fri, 21 Aug 2026 03:27:25 +0530 Subject: [PATCH] Smooth streaming scroll, and drop the em dash from the header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scrolling up to re-read while the answer streamed was janky: every token called scrollIntoView and yanked the viewport back to the bottom, fighting the manual scroll. Autoscroll is now sticky — it follows the stream only while the reader is already at the bottom, and stops the moment they scroll up. One scroll listener for the page instead of one added per question. Header now reads "note chunks. Retrieval first" instead of joining the clauses with an em dash. --- rag/bin/rag-web.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rag/bin/rag-web.py b/rag/bin/rag-web.py index 1059125..1167c57 100644 --- a/rag/bin/rag-web.py +++ b/rag/bin/rag-web.py @@ -82,7 +82,7 @@ #ks{color:var(--mut);white-space:nowrap}

CPTS notes assistant

-

Answers from your note chunks — retrieval first, +

Answers from your note chunks. Retrieval first, then the local model.

@@ -110,6 +110,13 @@ document.getElementById('n').textContent=d.chunks; if(d.auth_required){kb.hidden=false; if(!apiKey) ki.focus();} }); +// Sticky autoscroll: follow the stream only while the reader is at the bottom. +// If they scroll up to re-read, stop yanking them back down — that fight is the +// jank. One listener for the whole page, not one per question. +let sticky=true; +const atBottom=()=>window.innerHeight+window.scrollY>=document.documentElement.scrollHeight-60; +const stick=()=>{if(sticky) window.scrollTo(0,document.documentElement.scrollHeight)}; +addEventListener('scroll',()=>{sticky=atBottom()},{passive:true}); function esc(s){return s.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))} function render(s){return esc(s) .replace(/```([\\s\\S]*?)```/g,(m,c)=>'
'+c.replace(/^\\w*\\n/,'')+'
') @@ -121,7 +128,8 @@ const qd=document.createElement('div'); qd.className='msg q'; qd.textContent=question; const ad=document.createElement('div'); ad.className='msg'; const body=document.createElement('div'); body.className='a'; ad.appendChild(body); - log.append(qd,ad); ad.scrollIntoView({block:'end'}); + log.append(qd,ad); + sticky=true; stick(); // a fresh question always jumps to the newest message let text='', t0=Date.now(); try{ const res=await fetch('ask',{method:'POST', @@ -139,9 +147,8 @@ const line=p.split('\\n').find(l=>l.startsWith('data:')); if(!line) continue; const data=line.slice(5).trim(); if(!data||data==='[DONE]') continue; const ev=JSON.parse(data); - if(ev.token){text+=ev.token; body.innerHTML=render(text); - ad.scrollIntoView({block:'end'})} - if(ev.error){body.innerHTML+='
'+esc(ev.error)+'
'} + if(ev.token){text+=ev.token; body.innerHTML=render(text); stick()} + if(ev.error){body.innerHTML+='
'+esc(ev.error)+'
'; stick()} if(ev.sources){ const s=document.createElement('div'); s.className='src'; s.innerHTML='sources'+ev.sources.map(x=>'
'+esc(x)+'
').join(''); @@ -150,7 +157,7 @@ m.textContent=`${ev.chunks} chunks searched · retrieval ${ev.retrieve_ms}ms ` +`· first token ${(ev.ttft_ms/1000).toFixed(1)}s · ${ev.tok_per_s} tok/s ` +`· ${ev.prompt_chars} prompt chars · total ${((Date.now()-t0)/1000).toFixed(1)}s`; - ad.appendChild(m); + ad.appendChild(m); stick(); } } }