From 9946823f0a26bd10efcf8f064a41b567ca2146cc Mon Sep 17 00:00:00 2001 From: lhl Date: Sat, 12 Sep 2026 15:48:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E9=94=99=E9=85=8D=E7=99=BD=E5=B1=8F(no-store?= =?UTF-8?q?+=E8=B5=84=E6=BA=90=E7=89=88=E6=9C=AC=E5=8F=B7+setText=E7=A9=BA?= =?UTF-8?q?=E5=80=BC=E9=98=B2=E5=BE=A1)=20+=20=E8=B1=86=E5=8C=85=E5=BC=8F?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E5=8C=BA(=E5=8E=BB=E5=88=86=E5=89=B2?= =?UTF-8?q?=E7=BA=BF/=E8=BE=93=E5=85=A5=E6=A1=86=E9=93=BA=E6=BB=A1/?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=8F=91=E9=80=81/textarea=E8=87=AA=E5=A2=9E?= =?UTF-8?q?=E9=AB=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-09-12-doubao-layout-fix.md | 22 +++++++++++ src/genesis/server/app.py | 10 ++++- src/genesis/server/static/chat.css | 21 +++++----- src/genesis/server/static/chat.html | 12 +++--- src/genesis/server/static/chat.js | 39 ++++++++++++------- 5 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-12-doubao-layout-fix.md diff --git a/docs/superpowers/plans/2026-09-12-doubao-layout-fix.md b/docs/superpowers/plans/2026-09-12-doubao-layout-fix.md new file mode 100644 index 0000000..f6e7d2f --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-doubao-layout-fix.md @@ -0,0 +1,22 @@ +# 豆包式布局修复计划(崩溃 + 布局) + +日期:2026-09-12 + +## Fix A — 修复 `Cannot set properties of null`(新旧资源缓存错配) +根因:`/` 返回启动时缓存、无缓存头的 HTML;静态资源无版本号 → 浏览器缓存旧 chat.js/chat.html 之一后新旧混用,`getElementById(...)` 为 null 时 `.textContent` 抛错。 +修复: +1. `app.py`:启动时按 static 文件 `st_mtime_ns` 计算 `_ASSET_VER`,注入 `chat.html` 的 `__ASSET_VER__`;`index()` 返回 `HTMLResponse(..., headers={"Cache-Control":"no-store","Pragma":"no-cache"})`。 +2. `chat.html`:4 个资源 URL 加 `?v=__ASSET_VER__`。 +3. `chat.js`:新增 `setText(el, v)`,关键 `.textContent` 赋值 null 安全(防白屏)。 +4. 部署后强刷一次清掉旧 HTML 缓存。 + +## Fix B — 布局(去线 / 铺满 / 图标发送 / 省空间) +- `#composer`:去 `border-top`、背景透明、内边距收紧(与聊天区无缝)。 +- `#composer-inner` / `#composer-toolbar` / `#upload-panel`:去 880px 限宽,输入框铺满右侧。 +- `#send`:SVG 上箭头图标 + 40px 圆形按钮。 +- `header`:高度 56→48、padding 收紧。 +- 输入框改 ` +
@@ -121,8 +121,8 @@
- - - + + + diff --git a/src/genesis/server/static/chat.js b/src/genesis/server/static/chat.js index 284999b..476c263 100644 --- a/src/genesis/server/static/chat.js +++ b/src/genesis/server/static/chat.js @@ -43,6 +43,12 @@ function addMsg(role, html) { return m; } function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } +function setText(el, v) { if (el) el.textContent = v; } +function autoGrowInput() { + if (!inputEl) return; + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; +} function showTyping() { return addMsg('assistant', '思考中…'); } async function api(method, url, body) { @@ -121,7 +127,7 @@ async function loadProjects() { } function applyProjectContext() { - psName.textContent = draftProject || '未选择项目'; + setText(psName, draftProject || '未选择项目'); } function renderProjectSwitcher() { @@ -370,6 +376,7 @@ function insertPrompt(text) { inputEl.value = text; inputEl.focus(); try { inputEl.setSelectionRange(text.length, text.length); } catch (e) { /* 忽略 */ } + autoGrowInput(); } function renderEmptyOrWelcome() { @@ -422,7 +429,7 @@ function startRenameSession(el, s) { if (save && newName && newName !== (s.name || '')) { try { const r = await api('PATCH', '/api/sessions/' + encodeURIComponent(s.session_id), { name: newName }); - if (s.session_id === sid) sessionTitle.textContent = r.name || newName; + if (s.session_id === sid) setText(sessionTitle, r.name || newName); } catch (e) { addMsg('error', '重命名失败:' + esc(String(e))); } @@ -444,7 +451,7 @@ async function newSession() { // 保留 draftProject(当前顶栏已选项目),使新会话继承项目绑定(§4.2) try { localStorage.removeItem('genesis_session'); } catch (e) { /* 忽略 */ } chatEl.innerHTML = ''; - sessionTitle.textContent = '未创建会话'; + setText(sessionTitle, '未创建会话'); updateUploadBar(); await refreshRagStats(); await refreshSessions(); @@ -460,7 +467,7 @@ async function loadSession(id) { // 分层;若不一致,由提示条让用户主动选择。 // draftProject = rec.project || null; // 删除:原行为会悄悄改用户偏好 applyProjectContext(); - sessionTitle.textContent = (rec.name || '新会话'); + setText(sessionTitle, (rec.name || '新会话')); if (progressWs) { try { progressWs.close(); } catch (e) {} } progressWs = GenesisWS.connectProgressWs(id, { onProgress: (e) => GenesisWS.applyProgressEvent(e, (role, text) => addMsg(role, text)), @@ -540,7 +547,7 @@ async function send() { const d = await api('POST', '/api/sessions', { user_id: 'default', project: draftProject || null }); sid = d.session_id; activeProject = draftProject || null; - sessionTitle.textContent = (d.name || '新会话'); + setText(sessionTitle, (d.name || '新会话')); try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ } await refreshSessions(); } catch (e) { @@ -554,6 +561,7 @@ async function send() { onClose: () => {}, }); inputEl.value = ''; + inputEl.style.height = ''; addMsg('user', esc(text)); const typing = showTyping(); sendBtn.disabled = true; @@ -585,7 +593,10 @@ async function send() { } document.getElementById('send').addEventListener('click', send); -inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') send(); }); +inputEl.addEventListener('keydown', e => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } +}); +inputEl.addEventListener('input', autoGrowInput); document.getElementById('new-chat').addEventListener('click', () => newSession().catch(e => addMsg('error', esc(String(e))))); document.getElementById('sidebar-toggle').addEventListener('click', () => toggleSidebar()); document.getElementById('ps-current').addEventListener('click', (e) => { e.stopPropagation(); lastTriggerEl = e.currentTarget; toggleProjectSwitcher(); }); @@ -636,9 +647,9 @@ async function refreshRagStats() { stats = { chunks: 0, rag_enabled: false }; } const n = Number(stats.chunks) || 0; - ragStatsEl.textContent = n > 0 + setText(ragStatsEl, n > 0 ? 'RAG 已索引 ' + n + ' 片段' - : 'RAG 0 片段(请在项目配置中设置代码库目录)'; + : 'RAG 0 片段(请在项目配置中设置代码库目录)'); ragStatsEl.classList.toggle('ready', n > 0); ragStatsEl.classList.toggle('zero', n === 0); // 开关可用性:必须先有 RAG 索引才能用 @@ -661,9 +672,9 @@ function updateImpactButton() { if (!sid) { impactBtnEl.disabled = true; return; } const hasRag = (Number(ragStatsEl.dataset.chunks || 0) > 0) || !ragEnabledEl.disabled; impactBtnEl.disabled = !hasRag; - impactBtnEl.textContent = ragEnabledEl.checked + setText(impactBtnEl, ragEnabledEl.checked ? '开始影响调查(启用 RAG)' - : '开始影响调查'; + : '开始影响调查'); } impactBtnEl.addEventListener('click', async () => { @@ -753,16 +764,16 @@ document.getElementById('upload-btn').addEventListener('click', async () => { fd.append('file_type', ft); fd.append('file', file); const statusEl = document.getElementById('upload-status'); - statusEl.textContent = '上传中…'; + setText(statusEl, '上传中…'); try { const r = await fetch('/api/sessions/' + encodeURIComponent(sid) + '/files', { method: 'POST', body: fd }); const d = await r.json(); if (!r.ok) throw new Error(d.detail?.message || r.status); - statusEl.textContent = ''; + setText(statusEl, ''); // B5 / P1‑F:上传成功后同步 activeProject、刷新上传条与侧边栏状态 const rec = await api('GET', '/api/sessions/' + encodeURIComponent(sid)); activeProject = rec.project || null; - sessionTitle.textContent = (rec.name || '新会话'); + setText(sessionTitle, (rec.name || '新会话')); await refreshSessions(); updateUploadBar(); // P1‑F:上传成功后同步上传条(防状态滞后) await refreshRagStats(); // P0‑A:上传 existing_system 后 RAG 索引已建 @@ -770,7 +781,7 @@ document.getElementById('upload-btn').addEventListener('click', async () => { addMsg('user', '[上传] ' + ft + ':' + d.file_name); addMsg('progress', '上传完成:' + esc(d.file_name) + '(' + d.size + 'B)'); } catch (e) { - statusEl.textContent = ''; + setText(statusEl, ''); // F1:失败保留 file-input 值,展示后端明细 addMsg('error', '上传失败:' + esc(String(e))); }