From a62c49d6b1255047eeab02c567c0a776e8dffc52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Thu, 18 Jun 2026 07:39:40 +0200 Subject: [PATCH] fix(web): responsives Layout + zuverlaessiges Auto-Scrollen (mobil) - Layout auf echte Viewport-Hoehe (100dvh, vh-Fallback); nur der Nachrichten- bereich scrollt (min-height:0), Eingabe + Mic-Button bleiben immer sichtbar - Mobile: Menue als kompakte Kopfleiste, Chat fuellt den Rest - Auto-Scroll zentralisiert (scrollToBottom via rAF) und bei token/semantic/done/ neuer Nachricht ausgeloest; reagiert auf Tastatur (visualViewport) + Resize/Focus -> die neueste Antwort ist immer vollstaendig sichtbar Co-Authored-By: Claude Opus 4.8 --- app/web/app.js | 18 ++++++++++++++++-- app/web/style.css | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/web/app.js b/app/web/app.js index 75ad3f1..a1ca26a 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -54,12 +54,17 @@ async function loadUsers() { } // ---------- Nachrichten-UI ---------- +// Nach dem naechsten Layout ans Ende scrollen -> die neueste Antwort ist sichtbar. +function scrollToBottom() { + requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; }); +} + function addMessage(role, text) { const div = document.createElement("div"); div.className = "msg " + role; div.textContent = text; messagesEl.appendChild(div); - messagesEl.scrollTop = messagesEl.scrollHeight; + scrollToBottom(); return div; } @@ -113,11 +118,12 @@ function runTurn(path, onopen) { case "token": if (!answerEl) answerEl = addMessage("assistant", ""); answerEl.textContent += msg.text || ""; - messagesEl.scrollTop = messagesEl.scrollHeight; + scrollToBottom(); break; case "semantic": if (!answerEl) answerEl = addMessage("assistant", ""); if (msg.text) answerEl.textContent = msg.text; + scrollToBottom(); break; case "emergency": addMessage("emergency", "⚠ Notfall erkannt (" + (msg.category || "?") + ")"); @@ -126,6 +132,7 @@ function runTurn(path, onopen) { sampleRate = msg.sample_rate || 24000; playPcm(pcm, sampleRate); statusEl.textContent = ""; + scrollToBottom(); ws.close(); break; case "error": @@ -207,4 +214,11 @@ micBtn.addEventListener("click", () => { $("#reload-users").addEventListener("click", loadUsers); +// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben. +if (window.visualViewport) { + window.visualViewport.addEventListener("resize", scrollToBottom); +} +window.addEventListener("resize", scrollToBottom); +promptEl.addEventListener("focus", () => setTimeout(scrollToBottom, 300)); + loadMe(); diff --git a/app/web/style.css b/app/web/style.css index d7e191a..e25ef46 100644 --- a/app/web/style.css +++ b/app/web/style.css @@ -1,15 +1,19 @@ * { box-sizing: border-box; } +html, body { height: 100%; } body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: #1c1c1c; background: #f6f7f9; } -#layout { display: flex; min-height: 100vh; } +/* Echte (dynamische) Viewport-Hoehe: auf iOS beruecksichtigt 100dvh die + ein-/ausblendende Browserleiste; 100vh als Fallback fuer aeltere Browser. */ +#layout { display: flex; height: 100vh; height: 100dvh; overflow: hidden; } #menu { width: 240px; flex-shrink: 0; + overflow-y: auto; background: #1f2933; color: #e4e7eb; padding: 1rem; @@ -27,8 +31,19 @@ body { #user-list { list-style: none; padding: 0; margin: 0 0 0.5rem; font-size: 0.85rem; } #user-list li { padding: 0.15rem 0; border-bottom: 1px solid #323f4b; } -#chat { flex: 1; display: flex; flex-direction: column; padding: 1rem; max-width: 820px; margin: 0 auto; width: 100%; } -#messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem; padding-bottom: 1rem; } +#chat { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 1rem; max-width: 820px; margin: 0 auto; width: 100%; } +/* min-height:0 erlaubt dem Flex-Kind zu schrumpfen -> nur DIESER Bereich scrollt. */ +#messages { + flex: 1; + min-height: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + gap: 0.6rem; + padding-bottom: 0.5rem; +} .msg { padding: 0.6rem 0.8rem; border-radius: 0.8rem; max-width: 80%; white-space: pre-wrap; line-height: 1.4; } .msg.user { align-self: flex-end; background: #2b6cb0; color: #fff; } @@ -36,17 +51,30 @@ body { .msg.system { align-self: center; background: #fdf3d7; border: 1px solid #f0d98c; font-size: 0.85rem; } .msg.emergency { align-self: center; background: #fde0e0; border: 1px solid #f5a3a3; font-weight: 600; } -#prompt-form { display: flex; gap: 0.5rem; align-items: center; } +/* Eingabe + Mic bleiben immer sichtbar (fester Fuss des Chat-Bereichs). */ +#prompt-form { display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0; padding-top: 0.5rem; } #prompt { flex: 1; padding: 0.7rem; border: 1px solid #cbd2d9; border-radius: 0.6rem; font-size: 1rem; } button { padding: 0.7rem 1rem; border: none; border-radius: 0.6rem; background: #2b6cb0; color: #fff; font-size: 1rem; cursor: pointer; } button:disabled { opacity: 0.5; cursor: default; } #mic { background: #2f855a; } #mic.recording { background: #c53030; animation: pulse 1s infinite; } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } } -#status { min-height: 1.2rem; margin-top: 0.4rem; } +#status { min-height: 1.2rem; margin-top: 0.4rem; flex-shrink: 0; } @media (max-width: 640px) { #layout { flex-direction: column; } - #menu { width: 100%; flex-direction: row; flex-wrap: wrap; align-items: center; } - #menu h2 { flex: 1; } + /* Menue als kompakte, nicht scrollende Kopfleiste; der Chat fuellt den Rest. */ + #menu { + width: 100%; + flex-shrink: 0; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + } + #menu h2 { flex: 1; font-size: 1rem; } + #menu h3 { display: none; } + #admin { width: 100%; order: 3; } + #chat { flex: 1; min-height: 0; padding: 0.6rem; } }