Commit 3f1bf550 by Michael Pastushkov

fix

parent ecbc30ca
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<style> <style>
:root{ :root{
/* tune these if footer/composer sizes change */
--footer-h: 2.75rem; /* fixed footer height (approx) */ --footer-h: 2.75rem; /* fixed footer height (approx) */
--composer-h: 6.5rem; /* composer block height (approx) */ --composer-h: 6.5rem; /* composer block height (approx) */
--bottom-pad: calc(var(--footer-h) + var(--composer-h) + 1rem); --bottom-pad: calc(var(--footer-h) + var(--composer-h) + 1rem);
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
main{ main{
flex:1 1 auto; flex:1 1 auto;
min-height:0; /* IMPORTANT: allow flex child to actually scroll */ min-height:0; /* allow flex child to actually scroll */
overflow-y:auto; /* scrollable area */ overflow-y:auto; /* scrollable area */
position:relative; position:relative;
padding-bottom:var(--bottom-pad); /* room for composer + fixed footer */ padding-bottom:var(--bottom-pad); /* room for composer + fixed footer */
...@@ -70,30 +71,6 @@ ...@@ -70,30 +71,6 @@
max-width:520px; width:100%; height:auto; max-width:520px; width:100%; height:auto;
filter:drop-shadow(0 6px 16px rgba(0,0,0,.15)); opacity:.98; filter:drop-shadow(0 6px 16px rgba(0,0,0,.15)); opacity:.98;
} }
/* Scroll button: FIXED, toggled by .show */
#scrollBtn{
position:fixed;
right:1rem;
bottom:calc(var(--footer-h) + var(--composer-h) + 1rem); /* sits above composer + footer */
z-index:1100;
display:none; /* hidden by default; JS toggles .show */
align-items:center;
justify-content:center;
border:none;
border-radius:999px;
width:40px;
height:40px;
background:rgba(13,110,253,.95);
color:#fff;
box-shadow:0 4px 14px rgba(0,0,0,.18);
cursor:pointer;
transition:opacity .18s ease-in-out, transform .12s ease-in-out;
opacity:.96;
}
#scrollBtn.show{ display:inline-flex; }
#scrollBtn:hover{ opacity:1; transform:translateY(-1px); }
#scrollBtn svg{ width:22px; height:22px; pointer-events:none; }
</style> </style>
</head> </head>
<body> <body>
...@@ -115,14 +92,6 @@ ...@@ -115,14 +92,6 @@
<div id="thread"></div> <div id="thread"></div>
</main> </main>
<!-- Scroll button: visibility toggled by JS -->
<button id="scrollBtn" type="button" aria-label="Scroll down" title="Scroll down">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<!-- down chevron -->
<path d="M12 16a1 1 0 0 1-.7-.29l-6-6a1 1 0 1 1 1.4-1.42L12 13.59l5.3-5.3a1 1 0 0 1 1.4 1.42l-6 6A1 1 0 0 1 12 16z"/>
</svg>
</button>
<!-- Composer (sticks above fixed footer) --> <!-- Composer (sticks above fixed footer) -->
<div class="composer py-3"> <div class="composer py-3">
<div class="container"> <div class="container">
...@@ -142,7 +111,7 @@ ...@@ -142,7 +111,7 @@
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script> <script>
// --- i18n setup (unchanged) --- // --- i18n setup ---
const DEFAULT_LOCALE='en'; const SUPPORTED=['en','ru']; const DEFAULT_LOCALE='en'; const SUPPORTED=['en','ru'];
let I18N={}; let CURRENT_LOCALE=detectLocale(); let isRunning=false; let I18N={}; let CURRENT_LOCALE=detectLocale(); let isRunning=false;
...@@ -159,7 +128,6 @@ ...@@ -159,7 +128,6 @@
const goBtn=document.getElementById('goBtn'); if(goBtn) goBtn.textContent=t('buttons.go','Go'); const goBtn=document.getElementById('goBtn'); if(goBtn) goBtn.textContent=t('buttons.go','Go');
const stopBtn=document.getElementById('stopBtn'); if(stopBtn) stopBtn.textContent=t('buttons.stop','Stop'); const stopBtn=document.getElementById('stopBtn'); if(stopBtn) stopBtn.textContent=t('buttons.stop','Stop');
const disclaimer=document.getElementById('disclaimer'); if(disclaimer) disclaimer.textContent=t('footer.disclaimer','Beware: Cat can make mistakes.'); const disclaimer=document.getElementById('disclaimer'); if(disclaimer) disclaimer.textContent=t('footer.disclaimer','Beware: Cat can make mistakes.');
const sb=document.getElementById('scrollBtn'); if(sb){ sb.setAttribute('aria-label', t('buttons.scrollDown','Scroll down')); sb.title=t('buttons.scrollDown','Scroll down'); }
setWelcomeImage(); setWelcomeImage();
} }
...@@ -171,8 +139,7 @@ ...@@ -171,8 +139,7 @@
const goBtn=document.getElementById('goBtn'); const goBtn=document.getElementById('goBtn');
const stopBtn=document.getElementById('stopBtn'); const stopBtn=document.getElementById('stopBtn');
const welcomeLogo=document.getElementById('welcomeLogo'); const welcomeLogo=document.getElementById('welcomeLogo');
const mainScroll=document.getElementById('mainScroll'); // intended scroll container const mainScroll=document.getElementById('mainScroll');
const scrollBtn=document.getElementById('scrollBtn'); // the button we toggle & click
let firstRequestDone=false; let firstRequestDone=false;
let controller=null; let controller=null;
...@@ -195,43 +162,11 @@ ...@@ -195,43 +162,11 @@
if(img) targetEl.innerHTML=''; if(img) targetEl.innerHTML='';
} }
// --- figure out what is actually scrolling right now --- // --- bottom helpers ---
function isOverflowing(el){ return (el.scrollHeight - el.clientHeight) > 4; } function atBottom(){
function currentScrollEl(){ return (mainScroll.scrollTop + mainScroll.clientHeight) >= (mainScroll.scrollHeight - 4);
// Prefer main if it overflows, otherwise fall back to the page scroller
if (mainScroll && isOverflowing(mainScroll)) return mainScroll;
return document.scrollingElement || document.documentElement;
}
function atBottom(el){
return (el.scrollTop + el.clientHeight) >= (el.scrollHeight - 4);
}
function updateScrollIndicator(){
if (!scrollBtn) return;
const el = currentScrollEl();
if (isOverflowing(el) && !atBottom(el)) scrollBtn.classList.add('show');
else scrollBtn.classList.remove('show');
} }
// Click: scroll the **actual** scroll container to bottom
if (scrollBtn) {
scrollBtn.addEventListener('click', () => {
const el = currentScrollEl();
const top = el.scrollHeight;
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
window.scrollTo({ top, behavior: 'smooth' });
} else {
el.scrollTo({ top, behavior: 'smooth' });
}
setTimeout(updateScrollIndicator, 400);
});
}
// Keep indicator state in sync (listen to both container + window)
if (mainScroll) mainScroll.addEventListener('scroll', updateScrollIndicator, { passive:true });
window.addEventListener('scroll', updateScrollIndicator, { passive:true });
window.addEventListener('resize', updateScrollIndicator);
// --- composer lock --- // --- composer lock ---
function setComposerLocked(locked){ function setComposerLocked(locked){
isRunning=locked; isRunning=locked;
...@@ -249,8 +184,7 @@ ...@@ -249,8 +184,7 @@
if(!firstRequestDone){ welcomeLogo?.remove(); firstRequestDone=true; } if(!firstRequestDone){ welcomeLogo?.remove(); firstRequestDone=true; }
const elBefore = currentScrollEl(); const auto = atBottom();
const auto = atBottom(elBefore);
const userMsg=document.createElement('div'); const userMsg=document.createElement('div');
userMsg.className='message user-message'; userMsg.className='message user-message';
...@@ -263,7 +197,6 @@ ...@@ -263,7 +197,6 @@
thread.appendChild(assistantMsg); thread.appendChild(assistantMsg);
if (auto) assistantMsg.scrollIntoView({ behavior:'smooth', block:'start' }); if (auto) assistantMsg.scrollIntoView({ behavior:'smooth', block:'start' });
updateScrollIndicator();
inputText.value=''; inputText.value='';
controller=new AbortController(); controller=new AbortController();
...@@ -289,14 +222,9 @@ ...@@ -289,14 +222,9 @@
rafPending=false; rafPending=false;
if(!receivedAny){ clearThinkingCatIfPresent(assistantMsg); receivedAny=true; } if(!receivedAny){ clearThinkingCatIfPresent(assistantMsg); receivedAny=true; }
renderMarkdown(assistantMsg, rawMarkdown); renderMarkdown(assistantMsg, rawMarkdown);
if (auto && atBottom()){
// If user was at bottom before streaming, keep pinned to bottom of the active scroller mainScroll.scrollTop = mainScroll.scrollHeight;
if (auto) {
const el = currentScrollEl();
el.scrollTop = el.scrollHeight;
} }
updateScrollIndicator();
}); });
}; };
...@@ -321,7 +249,6 @@ ...@@ -321,7 +249,6 @@
}finally{ }finally{
setComposerLocked(false); setComposerLocked(false);
controller=null; controller=null;
updateScrollIndicator();
} }
} }
...@@ -331,18 +258,17 @@ ...@@ -331,18 +258,17 @@
} }
// events // events
document.getElementById('promptForm').addEventListener('submit', e => { e.preventDefault(); if (goBtn.style.display!=='none') start(); }); promptForm.addEventListener('submit', e => { e.preventDefault(); if (goBtn.style.display!=='none') start(); });
document.getElementById('inputText').addEventListener('keydown', e => { inputText.addEventListener('keydown', e => {
if (e.key==='Enter' && !e.shiftKey && goBtn.style.display!=='none'){ e.preventDefault(); start(); } if (e.key==='Enter' && !e.shiftKey && goBtn.style.display!=='none'){ e.preventDefault(); start(); }
}); });
document.getElementById('stopBtn').addEventListener('click', stop); stopBtn.addEventListener('click', stop);
// init // init
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
try{ await fetch('/api/clear',{method:'GET'}); }catch(_){} try{ await fetch('/api/clear',{method:'GET'}); }catch(_){}
await loadLocale(CURRENT_LOCALE); await loadLocale(CURRENT_LOCALE);
inputText.focus(); inputText.focus();
updateScrollIndicator(); // initial state
}); });
</script> </script>
</body> </body>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment