From a415bcdb88b6db403363efe9f828aab8fc9b16bf Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 30 Oct 2026 12:53:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=8B=E6=9C=BA=E9=94=AE?= =?UTF-8?q?=E7=9B=98=E5=BC=B9=E5=87=BA=E6=97=B6=E9=A1=B5=E9=9D=A2=E4=BD=8D?= =?UTF-8?q?=E7=A7=BB=E4=B8=8E=E7=84=A6=E7=82=B9=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/chat.css | 25 ++++++--- static/js/chat.js | 57 +++++++++++++++---- static/js/voice.js | 2 +- templates/index.html | 4 +- tests/frontend/chat.test.cjs | 102 +++++++++++++++++++++++++++++++--- tests/frontend/voice.test.cjs | 13 ++++- 6 files changed, 171 insertions(+), 32 deletions(-) diff --git a/static/css/chat.css b/static/css/chat.css index 881e73c..11488ba 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -1,5 +1,5 @@ /* Customer workspace: navigation, welcome, conversation and composer. */ -body { padding: 20px; height: 100dvh; overflow: hidden; } +body { padding: 20px; height: 100vh; height: 100dvh; overflow: hidden; } .chat-container { display: flex; width: min(1440px, 100%); height: 100%; margin: auto; overflow: hidden; position: relative; border: 1px solid var(--border); @@ -44,7 +44,7 @@ body { padding: 20px; height: 100dvh; overflow: hidden; } .clear-all-btn:hover { color: var(--danger); } .sidebar-note { display: flex; align-items: center; gap: 7px; margin-top: 10px; color: var(--muted); font-size: 10px; } .left-panel-hide { width: 0 !important; padding-left: 0 !important; padding-right: 0 !important; visibility: hidden; border: 0; } -.main-chat { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; overflow: hidden; } +.main-chat { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; overflow: hidden; } .chat-title { flex-shrink: 0; min-height: 75px; padding: 16px 26px; display: flex; align-items: center; gap: 13px; border-bottom: 1px solid var(--border); background: var(--surface); z-index: 31; } .header-copy { min-width: 0; flex: 1; } .header-kicker { font-size: 10px; color: var(--muted); letter-spacing: .06em; margin-bottom: 3px; } @@ -205,17 +205,24 @@ body { padding: 20px; height: 100dvh; overflow: hidden; } .suggest-copy small { font-size: 11px; } } @media (max-width: 900px) { - body { padding: 0; } - .chat-container { border: 0; border-radius: 0; height: 100dvh; } - .left-panel { position: absolute; left: 0; top: 72px; bottom: 0; width: 270px; max-width: 85vw; z-index: 30; box-shadow: 15px 0 40px rgba(20, 35, 60, .12); } + body { padding: 0; position: fixed; inset: 0; width: 100%; overscroll-behavior: none; } + .chat-container { --chat-header-height: 72px; position: fixed; left: 0; right: 0; top: 0; margin: 0; width: 100%; border: 0; border-radius: 0; height: 100vh; height: 100dvh; padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left); } + .left-panel { position: absolute; left: 0; top: calc(var(--chat-header-height) + env(safe-area-inset-top)); bottom: 0; width: 270px; max-width: 85vw; z-index: 30; box-shadow: 15px 0 40px rgba(20, 35, 60, .12); } .left-panel .brand { display: none; } .side-closed .left-panel { transform: translateX(-105%); visibility: hidden; pointer-events: none; } - .chat-container:not(.side-closed) .sidebar-backdrop { display: block; position: absolute; inset: 72px 0 0; border: 0; z-index: 20; background: rgba(16, 30, 52, .25); } + .chat-container:not(.side-closed) .sidebar-backdrop { display: block; position: absolute; inset: calc(var(--chat-header-height) + env(safe-area-inset-top)) 0 0; border: 0; z-index: 20; background: rgba(16, 30, 52, .25); } .main-chat { width: 100%; } - .chat-title { min-height: 72px; height: 72px; padding: 13px 18px; gap: 11px; } - #chatDom { padding: 24px 20px 15px; } + .chat-title { min-height: var(--chat-header-height); height: var(--chat-header-height); padding: 13px 18px; gap: 11px; } + #chatDom { padding: 24px 20px 15px; overscroll-behavior-y: contain; } .ai-welcome-card { padding-top: 20px; } - .composer { width: calc(100% - 40px); } + .composer { width: calc(100% - 40px); padding-bottom: calc(10px + env(safe-area-inset-bottom)); } + .chat-container.is-composing { --chat-header-height: 56px; } + .is-composing .chat-title { padding-top: 8px; padding-bottom: 8px; } + .is-composing .composer { padding-top: 6px; padding-bottom: 6px; } + .is-composing .composer-footer { display: none; } + #input-text, .history-search { font-size: max(16px, 1em); } + .history-search { height: 44px; } + .history-search-wrap svg { top: 14px; } .user-message, .robot-message { max-width: 94%; } .del-history-btn { opacity: 1; } .voice-toggle { display: grid; place-items: center; flex: 0 0 40px; height: 36px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--brand-text); font-size: 20px; } diff --git a/static/js/chat.js b/static/js/chat.js index a546933..57295cf 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -6,6 +6,11 @@ const sendBtn = document.getElementById("send-button"); const uploadBtn = document.getElementById("upload-btn"); const uploadInput = document.getElementById("upload-input"); const composerStatus = document.getElementById("composer-status"); +function focusComposer(force){ + if(force || !isNarrow() || document.activeElement === inputDom){ + inputDom.focus({preventScroll:true}); + } +} let uploadPending = false; let uploadVersion = 0; let uploadController = null; @@ -121,12 +126,12 @@ uploadInput.onchange = function(){ remove.className = "attachment-remove"; remove.textContent = "×"; remove.setAttribute("aria-label", t("removeAttachment")); - remove.onclick = function(){ clearAttachment(); inputDom.focus(); }; + remove.onclick = function(){ clearAttachment(); focusComposer(true); }; prev.append(copy, remove); var area = document.querySelector(".input-area"); area.before(prev); setComposerStatus(""); - inputDom.focus(); + focusComposer(); } }) .catch(function(error){ @@ -1140,7 +1145,7 @@ async function returnToAI(){ stopHumanMode(); setComposerStatus(''); addMsg(t('humanClosed'), false); - inputDom.focus(); + focusComposer(); }catch(error){ if(version === conversationVersion) setComposerStatus(t('humanReturnFailed'), true); }finally{ @@ -1166,7 +1171,7 @@ async function sendHumanMessage(content){ if(inputDom.value === draft){ inputDom.value = ''; autoGrow(); } if(window.__pendingImg === attachment) clearAttachment(); setComposerStatus(t(humanStatus === 'active' ? 'humanSent' : 'humanWaiting')); - inputDom.focus(); + focusComposer(); return true; }catch(error){ if(version !== conversationVersion || humanSendingToken !== token) return; @@ -1471,7 +1476,7 @@ async function doSend(retryText){ var tag = window.__pendingTag || "[图片]"; const draft = inputDom.value, attachmentVersion = uploadVersion; if(!sendWord && !imgUrl){ - inputDom.focus(); + focusComposer(true); return; } if(uploadPending){ setComposerStatus(t("uploading")); return; } @@ -1504,7 +1509,7 @@ async function doSend(retryText){ addMsg(fullContent, true); if(inputDom.value === draft){ inputDom.value = ""; autoGrow(); } if(attachmentVersion === uploadVersion) clearAttachment(); - inputDom.focus(); + focusComposer(); } const aiBubble = addMsg(t("thinking"), false); @@ -1537,7 +1542,6 @@ async function doSend(retryText){ setBusy(false); aiBubble.classList.remove("typing"); if(activeBubble === aiBubble) activeBubble = null; - inputDom.focus(); let raw = aiBubble.dataset.raw || ""; // 模型有时会把卡片标记原样输出,这里兜底转成时间轴 @@ -1719,7 +1723,10 @@ function autoGrow(){ return; } inputDom.style.height = "auto"; - inputDom.style.height = Math.min(inputDom.scrollHeight, 140) + "px"; + const viewport = window.visualViewport; + const height = viewport && viewport.scale === 1 ? viewport.height : window.innerHeight; + const limit = isNarrow() && height > 0 ? Math.min(140, Math.max(44, Math.floor(height / 4))) : 140; + inputDom.style.height = Math.min(inputDom.scrollHeight, limit) + "px"; } inputDom.addEventListener("input", autoGrow); @@ -1777,12 +1784,30 @@ renderSidebar(); function syncChatViewport(){ const viewport = window.visualViewport; - chatContainer.style.height = viewport && isNarrow() && viewport.scale === 1 - ? Math.round(viewport.height) + 'px' : ''; + const narrow = isNarrow(); + const unzoomed = !viewport || Math.abs(viewport.scale - 1) < 0.01; + if(narrow && unzoomed){ + const height = viewport ? viewport.height : window.innerHeight; + if(Number.isFinite(height) && height > 0){ + chatContainer.style.height = Math.round(height) + 'px'; + // Keyboard focus may pan the visual viewport without changing its height. + chatContainer.style.top = Math.max(0, Math.round(viewport && viewport.offsetTop || 0)) + 'px'; + } + }else{ + chatContainer.style.height = ''; + chatContainer.style.top = ''; + } + chatContainer.classList.toggle('is-composing', narrow && document.activeElement === inputDom); autoGrow(); if(!chatDom.querySelector('.ai-welcome-card')) scrollChatToEnd(); updateToBottom(); } +let viewportSyncQueued = false; +function scheduleChatViewport(){ + if(viewportSyncQueued) return; + viewportSyncQueued = true; + requestAnimationFrame(function(){ viewportSyncQueued = false; syncChatViewport(); }); +} toggleBtn.onclick = function(){ sideOpen = !sideOpen; @@ -1810,7 +1835,17 @@ window.addEventListener("resize", function(){ renderSidebar(); syncChatViewport(); }); -if(window.visualViewport) window.visualViewport.addEventListener('resize', syncChatViewport); +if(window.visualViewport){ + window.visualViewport.addEventListener('resize', scheduleChatViewport); + window.visualViewport.addEventListener('scroll', scheduleChatViewport); +} +window.addEventListener('pageshow', scheduleChatViewport); +inputDom.addEventListener('focus', scheduleChatViewport); +inputDom.addEventListener('blur', scheduleChatViewport); +// Keep an already open keyboard while tapping Send; keyboard navigation is unchanged. +sendBtn.addEventListener('pointerdown', function(event){ + if(event.button === 0 && document.activeElement === inputDom) event.preventDefault(); +}); syncChatViewport(); voiceInput = ChatVoice.create({ diff --git a/static/js/voice.js b/static/js/voice.js index f723ef9..7f195a3 100644 --- a/static/js/voice.js +++ b/static/js/voice.js @@ -168,7 +168,7 @@ var ChatVoice = { toggle.onclick = function(){ if(!mode && !supported()) return; setMode(!mode); - if(!mode) input.focus(); + if(!mode) input.focus({preventScroll:true}); }; if(window.PointerEvent){ hold.addEventListener('pointerdown', function(event){ diff --git a/templates/index.html b/templates/index.html index 45c122b..3f2a7a0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,7 +2,7 @@
- +