From 2ec347d9059c1b41a6f265f6a107bd621cf530dc Mon Sep 17 00:00:00 2001 From: lhl Date: Sat, 12 Sep 2026 14:36:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E8=B1=86=E5=8C=85=E5=BC=8F?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E2=80=94=E9=A1=B6=E6=A0=8F=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=90=8D/=E7=A9=BA=E6=80=81=E6=9C=89=E4=BB=80=E4=B9=88?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E5=B8=AE=E5=88=B0=E4=BD=A0=E7=9A=84/?= =?UTF-8?q?=E5=BA=95=E9=83=A8=E5=B7=A5=E5=85=B7=E6=A0=8F(=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE+=E6=96=B0=E5=BB=BA+=E4=B8=8A=E4=BC=A0+=E5=BD=B1?= =?UTF-8?q?=E5=93=8D=E8=B0=83=E6=9F=A5)/=E5=8E=86=E5=8F=B2=E9=A1=B9?= =?UTF-8?q?=E2=8B=AF=E9=87=8D=E5=91=BD=E5=90=8D/=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8E=BBzip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/genesis/server/static/chat.css | 57 +++++++++++- src/genesis/server/static/chat.html | 106 +++++++++++----------- src/genesis/server/static/chat.js | 111 +++++++++++++++++++++--- src/genesis/server/static/chat_state.js | 14 ++- tests/test_chat_state.js | 10 +++ 5 files changed, 232 insertions(+), 66 deletions(-) diff --git a/src/genesis/server/static/chat.css b/src/genesis/server/static/chat.css index 8d18b2a..ed3447e 100644 --- a/src/genesis/server/static/chat.css +++ b/src/genesis/server/static/chat.css @@ -172,7 +172,7 @@ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -.02em; margin: 0 .ps-current .ps-name { max-width: 160px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ps-current .ps-caret { font-size: 10px; color: var(--text-muted); } .ps-menu { - position: absolute; top: calc(100% + 6px); left: 0; min-width: 240px; + position: absolute; bottom: calc(100% + 6px); top: auto; left: 0; min-width: 240px; background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: var(--r-md); box-shadow: 0 8px 32px rgba(0,0,0,.4); padding: 6px; z-index: 20; display: none; @@ -410,3 +410,58 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px #sid-badge { max-width: 24%; } #proj-drawer { width: 100vw; } } + +/* === 顶栏会话名 === */ +#session-title { font-size: 15px; font-weight: 700; letter-spacing: -.02em; color: var(--text-primary); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } + +/* === 聊天区改为纵向 flex(空态垂直居中) === */ +#chat { display: flex; flex-direction: column; } + +/* === 空态(豆包式) === */ +.empty-state { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; + justify-content: center; text-align: center; padding: 32px 16px; gap: 10px; min-height: 0; } +.empty-state .es-logo { width: 56px; height: 56px; border-radius: 16px; display: flex; align-items: center; + justify-content: center; font-size: 26px; font-weight: 900; color: var(--bg-base); + background: var(--accent); box-shadow: 0 0 32px var(--accent-soft); } +.empty-state .es-title { font-size: 24px; font-weight: 700; margin: 6px 0 0; letter-spacing: -.02em; } +.empty-state .es-sub { font-size: 13px; color: var(--text-secondary); line-height: 1.7; margin: 0; max-width: 560px; } +.empty-state .suggests { display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); gap: 10px; + margin-top: 16px; width: 100%; max-width: 620px; } +.suggest-card { text-align: left; padding: 12px 16px; border-radius: var(--r-md); + background: var(--bg-surface); border: 1px solid var(--border-subtle); color: var(--text-primary); + font-size: 13px; font-family: var(--font-ui); cursor: pointer; transition: all var(--dur) var(--ease); } +.suggest-card:hover { border-color: var(--accent); background: var(--bg-elevated); transform: translateY(-1px); } +.suggest-card::before { content: "\2192 "; color: var(--accent); font-weight: 700; } + +/* === 底部工具栏 === */ +#composer-toolbar { max-width: 880px; margin: 10px auto 0; display: flex; align-items: center; + gap: 8px; flex-wrap: wrap; } +.tb-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; + background: transparent; border: 1px solid var(--border-strong); border-radius: var(--r-md); + color: var(--text-secondary); font-size: 13px; font-weight: 500; font-family: var(--font-ui); + cursor: pointer; transition: all var(--dur) var(--ease); } +.tb-btn:hover { border-color: var(--accent); color: var(--accent); } +.tb-btn .tb-ic { font-size: 14px; line-height: 1; } +.tb-btn .tb-caret { font-size: 10px; color: var(--text-muted); } +.proj-switcher { position: relative; } +#upload-panel { max-width: 880px; margin: 10px auto 0; } + +/* === 会话项「⋯」与重命名菜单 === */ +.sess .kebab { flex-shrink: 0; opacity: 0; transition: opacity .15s ease; background: transparent; + border: none; color: var(--text-muted); cursor: pointer; width: 24px; height: 24px; border-radius: 6px; + font-size: 15px; line-height: 1; display: flex; align-items: center; justify-content: center; } +.sess:hover .kebab { opacity: .7; } +.sess .kebab:hover { opacity: 1 !important; background: var(--bg-base); color: var(--accent); } +.rename-input { flex: 1; min-width: 0; background: var(--bg-input); border: 1px solid var(--accent); + border-radius: var(--r-sm); color: var(--text-primary); font-size: 13px; padding: 3px 6px; + font-family: var(--font-ui); outline: none; } +.sess-menu { position: fixed; z-index: 120; background: var(--bg-surface); border: 1px solid var(--border-strong); + border-radius: var(--r-md); box-shadow: 0 8px 32px rgba(0,0,0,.5); padding: 4px; min-width: 140px; } +.sess-menu .sm-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 12px; + background: transparent; border: none; border-radius: var(--r-sm); color: var(--text-secondary); + font-size: 13px; font-family: var(--font-ui); cursor: pointer; text-align: left; } +.sess-menu .sm-item:hover { background: var(--bg-elevated); color: var(--accent); } +.sess-menu .sm-ic { color: var(--accent); } +#sidebar.collapsed .sess .kebab { display: none; } +@media (max-width: 640px) { .empty-state .suggests { grid-template-columns: 1fr; } } diff --git a/src/genesis/server/static/chat.html b/src/genesis/server/static/chat.html index 13bf807..00ca2d7 100644 --- a/src/genesis/server/static/chat.html +++ b/src/genesis/server/static/chat.html @@ -20,30 +20,39 @@
-

Genesis 概要设计书生成

- -
- - -
- - 未创建会话 + 未创建会话
-
+
+
+ + +
+ +
+
+ + +
+ + + +
+ + - -
-
- - -
@@ -78,7 +80,7 @@
-
选择左侧项目以查看与编辑,或点 + 新建项目 创建一个。
+
选择左侧项目以查看与编辑,或点 + 新建项目 创建;新建时请设置该项目的资料目录。
@@ -87,30 +89,26 @@
-
- 高级(可选,服务器端预置路径) -

这些是服务器端预置路径,普通用户无需填写,留空则在每个会话中上传对应文件。仅在管理员/CLI 集成时使用。

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -119,6 +117,10 @@
+ + diff --git a/src/genesis/server/static/chat.js b/src/genesis/server/static/chat.js index cb0e825..284999b 100644 --- a/src/genesis/server/static/chat.js +++ b/src/genesis/server/static/chat.js @@ -22,12 +22,14 @@ const GenesisState = window.GenesisState || { isDrawerDirty: (n, s) => n !== s, buildWelcome: (d) => d === null ? '请新建项目或者选择项目' : '你好!我是 Genesis 概要设计书生成 Agent。请先上传要件定义 xlsx(模板/规则/代码库已由项目「' + d + '」提供)。', + emptyHeadline: () => '有什么可以帮到你的?', + quickPrompts: () => ['生成概要设计书', '影响调查', '用中文生成', '现在什么状态?'], }; const chatEl = document.getElementById('chat'); const inputEl = document.getElementById('input'); const sendBtn = document.getElementById('send'); -const badge = document.getElementById('sid-badge'); +const sessionTitle = document.getElementById('session-title'); const psMenu = document.getElementById('ps-menu'); const psName = document.getElementById('ps-name'); const drawer = document.getElementById('proj-drawer'); @@ -76,10 +78,13 @@ async function refreshSessions() { '
' + esc(s.name || '新会话') + '
' + '
' + esc(s.status) + proj + '
' + '
' + + '' + ''; el.querySelector('.info').onclick = () => loadSession(s.session_id); const delBtn = el.querySelector('.del'); delBtn.onclick = (ev) => { ev.stopPropagation(); deleteSession(s.session_id); }; + const kebab = el.querySelector('.kebab'); + kebab.onclick = (ev) => openSessMenu(ev, s.session_id, s.name || '新会话', el); box.appendChild(el); } } @@ -116,7 +121,7 @@ async function loadProjects() { } function applyProjectContext() { - psName.textContent = draftProject || '未选择'; + psName.textContent = draftProject || '未选择项目'; } function renderProjectSwitcher() { @@ -360,11 +365,75 @@ function updateUploadBar() { } } -// ---------- 空态/欢迎渲染 ---------- +// ---------- 空态/欢迎渲染(豆包式:主标题 + 副提示 + 建议卡) ---------- +function insertPrompt(text) { + inputEl.value = text; + inputEl.focus(); + try { inputEl.setSelectionRange(text.length, text.length); } catch (e) { /* 忽略 */ } +} + function renderEmptyOrWelcome() { chatEl.innerHTML = ''; - // P1‑F:欢迎文由 GenesisState.buildWelcome 统一来源 - addMsg('assistant', GenesisState.buildWelcome(draftProject)); + const wrap = document.createElement('div'); + wrap.className = 'empty-state'; + const cards = GenesisState.quickPrompts().map(t => + '' + ).join(''); + wrap.innerHTML = + '' + + '

' + esc(GenesisState.emptyHeadline()) + '

' + + '

' + GenesisState.buildWelcome(draftProject) + '

' + + '
' + cards + '
'; + chatEl.appendChild(wrap); + wrap.querySelectorAll('.suggest-card').forEach(b => { + b.onclick = () => insertPrompt(b.dataset.prompt); + }); +} + +// ---------- 会话「⋯」菜单 + 重命名 ---------- +let sessMenuTarget = null; +function openSessMenu(ev, sidVal, name, el) { + ev.stopPropagation(); + sessMenuTarget = { sid: sidVal, name: name, el: el }; + const m = document.getElementById('sess-menu'); + if (!m) return; + m.hidden = false; + const r = ev.currentTarget.getBoundingClientRect(); + m.style.top = (r.bottom + 4) + 'px'; + m.style.left = Math.max(8, r.right - 150) + 'px'; +} +function closeSessMenu() { + const m = document.getElementById('sess-menu'); + if (m) m.hidden = true; +} +function startRenameSession(el, s) { + const nameEl = el.querySelector('.name'); + if (!nameEl) return; + const input = document.createElement('input'); + input.className = 'rename-input'; + input.value = s.name || ''; + input.maxLength = 100; + nameEl.replaceWith(input); + input.focus(); input.select(); + let done = false; + const finish = async (save) => { + if (done) return; done = true; + const newName = input.value.trim(); + 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; + } catch (e) { + addMsg('error', '重命名失败:' + esc(String(e))); + } + } + await refreshSessions(); + }; + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); finish(true); } + else if (e.key === 'Escape') { e.preventDefault(); finish(false); } + }); + input.addEventListener('blur', () => finish(true)); } // ---------- 会话创建/加载 ---------- @@ -375,7 +444,7 @@ async function newSession() { // 保留 draftProject(当前顶栏已选项目),使新会话继承项目绑定(§4.2) try { localStorage.removeItem('genesis_session'); } catch (e) { /* 忽略 */ } chatEl.innerHTML = ''; - badge.textContent = '未创建会话'; + sessionTitle.textContent = '未创建会话'; updateUploadBar(); await refreshRagStats(); await refreshSessions(); @@ -391,7 +460,7 @@ async function loadSession(id) { // 分层;若不一致,由提示条让用户主动选择。 // draftProject = rec.project || null; // 删除:原行为会悄悄改用户偏好 applyProjectContext(); - badge.textContent = '会话: ' + (rec.name || '新会话'); + sessionTitle.textContent = (rec.name || '新会话'); if (progressWs) { try { progressWs.close(); } catch (e) {} } progressWs = GenesisWS.connectProgressWs(id, { onProgress: (e) => GenesisWS.applyProgressEvent(e, (role, text) => addMsg(role, text)), @@ -433,7 +502,7 @@ function renderProjectMismatchHint() { wrap.innerHTML = '
' + '该会话属于项目 ' + esc(activeProject) + ',' + - '当前顶栏项目为 ' + esc(draftProject || '(未选择)') + '。' + + '当前项目为 ' + esc(draftProject || '(未选择)') + '。' + '
' + '切到该项目' + '保留当前项目' + @@ -471,7 +540,7 @@ async function send() { const d = await api('POST', '/api/sessions', { user_id: 'default', project: draftProject || null }); sid = d.session_id; activeProject = draftProject || null; - badge.textContent = '会话: ' + (d.name || '新会话'); + sessionTitle.textContent = (d.name || '新会话'); try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ } await refreshSessions(); } catch (e) { @@ -523,6 +592,20 @@ document.getElementById('ps-current').addEventListener('click', (e) => { e.stopP document.getElementById('drawer-close').addEventListener('click', closeProjectDrawer); document.getElementById('pf-save').addEventListener('click', saveDrawerProject); document.getElementById('pf-delete').addEventListener('click', deleteDrawerProject); +document.getElementById('tb-newproject').addEventListener('click', () => { + if (!guardDrawerDirty()) return; + openProjectDrawer(null); +}); +document.getElementById('tb-upload').addEventListener('click', () => { + const panel = document.getElementById('upload-panel'); + if (panel) panel.hidden = !panel.hidden; +}); +document.getElementById('sm-rename').addEventListener('click', (ev) => { + ev.stopPropagation(); + const t = sessMenuTarget; + closeSessMenu(); + if (t) startRenameSession(t.el, { session_id: t.sid, name: t.name }); +}); // ---------- P0‑A:RAG 开关 + 状态徽标 + 开始影响调查 ---------- const ragEnabledEl = document.getElementById('rag-enabled'); @@ -555,7 +638,7 @@ async function refreshRagStats() { const n = Number(stats.chunks) || 0; ragStatsEl.textContent = n > 0 ? 'RAG 已索引 ' + n + ' 片段' - : 'RAG 0 片段(请上传既有系统 zip)'; + : 'RAG 0 片段(请在项目配置中设置代码库目录)'; ragStatsEl.classList.toggle('ready', n > 0); ragStatsEl.classList.toggle('zero', n === 0); // 开关可用性:必须先有 RAG 索引才能用 @@ -563,7 +646,7 @@ async function refreshRagStats() { if (n === 0) { ragEnabledEl.checked = false; writeRagFlag(false); - ragEnabledEl.parentElement.title = '请先在上传区选择"既有系统 zip"并上传'; + ragEnabledEl.parentElement.title = '请在项目配置中设置既有系统代码库目录'; } else { ragEnabledEl.checked = readRagFlag(); ragEnabledEl.parentElement.title = ragEnabledEl.checked @@ -607,9 +690,13 @@ impactBtnEl.addEventListener('click', async () => { document.addEventListener('click', (e) => { // 点击切换器外部关闭下拉 if (!document.getElementById('proj-switcher').contains(e.target)) toggleProjectSwitcher(false); + // 点击「⋯」菜单外部关闭 + const sm = document.getElementById('sess-menu'); + if (sm && !sm.hidden && !sm.contains(e.target)) closeSessMenu(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { + closeSessMenu(); if (psMenu.classList.contains('open')) toggleProjectSwitcher(false); else if (drawerOpen) closeProjectDrawer(); if (lastTriggerEl) { lastTriggerEl.focus(); lastTriggerEl = null; } @@ -675,7 +762,7 @@ document.getElementById('upload-btn').addEventListener('click', async () => { // B5 / P1‑F:上传成功后同步 activeProject、刷新上传条与侧边栏状态 const rec = await api('GET', '/api/sessions/' + encodeURIComponent(sid)); activeProject = rec.project || null; - badge.textContent = '会话: ' + (rec.name || '新会话'); + sessionTitle.textContent = (rec.name || '新会话'); await refreshSessions(); updateUploadBar(); // P1‑F:上传成功后同步上传条(防状态滞后) await refreshRagStats(); // P0‑A:上传 existing_system 后 RAG 索引已建 diff --git a/src/genesis/server/static/chat_state.js b/src/genesis/server/static/chat_state.js index fccdba0..e01788b 100644 --- a/src/genesis/server/static/chat_state.js +++ b/src/genesis/server/static/chat_state.js @@ -53,12 +53,22 @@ return '请新建项目或者选择项目'; } return '你好!我是 Genesis 概要设计书生成 Agent。' - + '请先在上方📎上传要件定义 xlsx' + + '请先在下方📎上传要件定义 xlsx' + '(模板/规则/代码库已由项目「' + draftProject + '」提供),然后告诉我:' + '
· 「生成概要设计书」(自动解析→影响→生成→QA)' + '
· 「用中文生成」 / 「现在什么状态?」'; } + // 空态主标题(豆包式布局:新建会话居中显示) + function emptyHeadline() { + return '有什么可以帮到你的?'; + } + + // 空态建议卡 / 快捷入口提示词(点击填入输入框,不自动发送) + function quickPrompts() { + return ['生成概要设计书', '影响调查', '用中文生成', '现在什么状态?']; + } + return { autoBindProject, shouldHideUploadSelect, @@ -66,5 +76,7 @@ computeDrawerSnapshot, isDrawerDirty, buildWelcome, + emptyHeadline, + quickPrompts, }; }); diff --git a/tests/test_chat_state.js b/tests/test_chat_state.js index a32fa9c..83e4c72 100644 --- a/tests/test_chat_state.js +++ b/tests/test_chat_state.js @@ -47,3 +47,13 @@ test('buildWelcome: 有项目时提示由项目提供模板/规则/代码库', ( assert.match(txt, /项目「A」/); assert.match(txt, /要件定义/); }); + +test('emptyHeadline: 返回空态主标题', () => { + assert.match(S.emptyHeadline(), /有什么可以帮到你的/); +}); + +test('quickPrompts: 返回 4 条快捷提示且含影响调查', () => { + const p = S.quickPrompts(); + assert.equal(p.length, 4); + assert.ok(p.includes('影响调查')); +});