Commit 08c1003d by michaelpastushkov

fix

parent 7915bcb3
...@@ -7,7 +7,15 @@ ...@@ -7,7 +7,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<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>
/* Layout: header + main (scrolls) + composer; fixed footer */ :root {
/* tune these if footer/composer sizes change */
--footer-h: 2.75rem;
/* fixed footer height (approx) */
--composer-h: 6.5rem;
/* composer block height (approx) */
--bottom-pad: calc(var(--footer-h) + var(--composer-h) + 1rem);
}
body { body {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
...@@ -26,31 +34,34 @@ ...@@ -26,31 +34,34 @@
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: auto; overflow-y: auto;
position: relative; position: relative;
/* leave room so fixed footer doesn’t overlap last messages */ padding-bottom: var(--bottom-pad);
padding-bottom: 3.5rem; /* leave room for composer + fixed footer */
/* ≈ footer height */
} }
.composer { .composer {
position: sticky; position: sticky;
bottom: 0; bottom: var(--footer-h);
/* sit just above the fixed footer */
z-index: 1020; z-index: 1020;
background: #fff; background: #fff;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }
/* --- Footer from your snippet (fixed, non-scrollable) --- */
footer { footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
z-index: 1010; z-index: 1020;
font-size: .75rem; font-size: .75rem;
color: #6c757d; color: #6c757d;
text-align: center; text-align: center;
padding: .5rem; padding: .5rem;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
background: #fdfdfd; background: #fdfdfd;
height: var(--footer-h);
box-sizing: border-box;
} }
.cat-spinner { .cat-spinner {
...@@ -145,9 +156,10 @@ ...@@ -145,9 +156,10 @@
opacity: .98; opacity: .98;
} }
/* Sticky in-content scroll indicator (same pattern as previous file) */ /* Sticky in-content scroll indicator (same behavior as the earlier file) */
#scrollDownBtn { #scrollDownBtn {
position: sticky; position: sticky;
/* inside scrollable main */
margin-left: auto; margin-left: auto;
bottom: 1rem; bottom: 1rem;
align-self: flex-end; align-self: flex-end;
...@@ -212,7 +224,7 @@ ...@@ -212,7 +224,7 @@
</button> </button>
</main> </main>
<!-- Composer --> <!-- Composer (sticks above fixed footer) -->
<div class="composer py-3"> <div class="composer py-3">
<div class="container"> <div class="container">
<form id="promptForm" class="d-flex flex-column gap-2"> <form id="promptForm" class="d-flex flex-column gap-2">
...@@ -225,13 +237,13 @@ ...@@ -225,13 +237,13 @@
</div> </div>
</div> </div>
<!-- Disclaimer Footer (fixed, non-scrolling) --> <!-- Fixed, non-scrollable footer (from your snippet) -->
<footer id="disclaimer"></footer> <footer id="disclaimer"></footer>
<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 (kept) --- // --- i18n setup (unchanged) ---
const DEFAULT_LOCALE = 'en'; const DEFAULT_LOCALE = 'en';
const SUPPORTED = ['en', 'ru']; const SUPPORTED = ['en', 'ru'];
let I18N = {}; let I18N = {};
...@@ -305,7 +317,7 @@ ...@@ -305,7 +317,7 @@
setWelcomeImage(); setWelcomeImage();
} }
// --- UI elements --- // --- UI refs ---
const ENDPOINT = '/api/stream'; const ENDPOINT = '/api/stream';
const STOPPOINT = '/api/stop'; const STOPPOINT = '/api/stop';
const CAT_GIF = '/static/images/cat-progress.gif'; const CAT_GIF = '/static/images/cat-progress.gif';
...@@ -317,12 +329,11 @@ ...@@ -317,12 +329,11 @@
const welcomeLogo = document.getElementById('welcomeLogo'); const welcomeLogo = document.getElementById('welcomeLogo');
const mainScroll = document.getElementById('mainScroll'); const mainScroll = document.getElementById('mainScroll');
const scrollDownBtn = document.getElementById('scrollDownBtn'); const scrollDownBtn = document.getElementById('scrollDownBtn');
const BOTTOM_EPS = 50;
let firstRequestDone = false; let firstRequestDone = false;
let controller = null; let controller = null;
// --- Content rendering helpers --- // --- helpers ---
function renderMarkdown(targetEl, markdownText) { function renderMarkdown(targetEl, markdownText) {
const html = marked.parse(markdownText, { breaks: true, gfm: true, headerIds: false }); const html = marked.parse(markdownText, { breaks: true, gfm: true, headerIds: false });
targetEl.innerHTML = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }); targetEl.innerHTML = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
...@@ -340,22 +351,13 @@ ...@@ -340,22 +351,13 @@
if (img) targetEl.innerHTML = ''; if (img) targetEl.innerHTML = '';
} }
// --- Scroll indicator logic (mirrors the first script) --- // --- Scroll indicator (same behavior as before) ---
function needsScroll() { function needsScroll() { return mainScroll.scrollHeight - mainScroll.clientHeight > 8; }
return mainScroll.scrollHeight - mainScroll.clientHeight > 8; function atBottom() { return mainScroll.scrollTop + mainScroll.clientHeight >= mainScroll.scrollHeight - 50; }
}
function atBottom() {
return mainScroll.scrollTop + mainScroll.clientHeight >= mainScroll.scrollHeight - BOTTOM_EPS;
}
function isNearBottom() { return atBottom(); } // alias for clarity
function updateScrollIndicator() { function updateScrollIndicator() {
if (needsScroll() && !atBottom()) { if (needsScroll() && !atBottom()) scrollDownBtn.classList.add('show');
scrollDownBtn.classList.add('show'); else scrollDownBtn.classList.remove('show');
} else {
scrollDownBtn.classList.remove('show');
}
} }
// scroll one viewport down on click
scrollDownBtn.addEventListener('click', () => { scrollDownBtn.addEventListener('click', () => {
mainScroll.scrollBy({ top: mainScroll.clientHeight, behavior: 'smooth' }); mainScroll.scrollBy({ top: mainScroll.clientHeight, behavior: 'smooth' });
setTimeout(updateScrollIndicator, 320); setTimeout(updateScrollIndicator, 320);
...@@ -363,26 +365,24 @@ ...@@ -363,26 +365,24 @@
mainScroll.addEventListener('scroll', updateScrollIndicator); mainScroll.addEventListener('scroll', updateScrollIndicator);
window.addEventListener('resize', updateScrollIndicator); window.addEventListener('resize', updateScrollIndicator);
// --- Composer lock / buttons --- // --- Composer lock ---
function setComposerLocked(locked) { function setComposerLocked(locked) {
isRunning = locked; isRunning = locked;
inputText.placeholder = locked inputText.placeholder = locked ? t('status.thinking', 'Thinking…') : t('composer.placeholder', 'What can I help you with?');
? t('status.thinking', 'Thinking…')
: t('composer.placeholder', 'What can I help you with?');
inputText.disabled = locked; inputText.disabled = locked;
goBtn.style.display = locked ? 'none' : 'inline-block'; goBtn.style.display = locked ? 'none' : 'inline-block';
stopBtn.style.display = locked ? 'inline-block' : 'none'; stopBtn.style.display = locked ? 'inline-block' : 'none';
if (locked) inputText.blur(); else inputText.focus(); if (locked) inputText.blur(); else inputText.focus();
} }
// --- App flow --- // --- Flow ---
async function start() { async function start() {
const prompt = inputText.value.trim(); const prompt = inputText.value.trim();
if (!prompt) return; if (!prompt) return;
if (!firstRequestDone) { welcomeLogo?.remove(); firstRequestDone = true; } if (!firstRequestDone) { welcomeLogo?.remove(); firstRequestDone = true; }
const shouldAutoScroll = isNearBottom(); const shouldAutoScroll = atBottom();
const userMsg = document.createElement('div'); const userMsg = document.createElement('div');
userMsg.className = 'message user-message'; userMsg.className = 'message user-message';
...@@ -422,8 +422,7 @@ ...@@ -422,8 +422,7 @@
rafPending = false; rafPending = false;
if (!receivedAny) { clearThinkingCatIfPresent(assistantMsg); receivedAny = true; } if (!receivedAny) { clearThinkingCatIfPresent(assistantMsg); receivedAny = true; }
renderMarkdown(assistantMsg, rawMarkdown); renderMarkdown(assistantMsg, rawMarkdown);
if (shouldAutoScroll && atBottom()) {
if (shouldAutoScroll && isNearBottom()) {
mainScroll.scrollTop = mainScroll.scrollHeight; mainScroll.scrollTop = mainScroll.scrollHeight;
} }
updateScrollIndicator(); updateScrollIndicator();
......
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