diff --git a/src/genesis/server/static/chat.html b/src/genesis/server/static/chat.html index 22bb32a..673fa0f 100644 --- a/src/genesis/server/static/chat.html +++ b/src/genesis/server/static/chat.html @@ -485,8 +485,9 @@ async function api(method, url, body) { // ---------- 会话列表 ---------- async function refreshSessions() { - const list = await api('GET', '/api/sessions?user_id=default'); const box = document.getElementById('session-list'); + if (draftProject === null) { box.innerHTML = ''; return; } // 未选项目 → 无历史 + const list = await api('GET', '/api/sessions?user_id=default&project=' + encodeURIComponent(draftProject)); box.innerHTML = ''; list.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || '')); for (const s of list) { @@ -561,6 +562,7 @@ function renderProjectSwitcher() { psMenu.innerHTML = html; psMenu.querySelectorAll('.ps-item[data-name]').forEach(it => { it.onclick = () => { + if (!guardDrawerDirty()) return; currentProject = it.dataset.name === '' ? null : it.dataset.name; applyProjectContext(); renderProjectSwitcher(); @@ -570,6 +572,8 @@ function renderProjectSwitcher() { }); psMenu.querySelectorAll('.ps-item[data-manage]').forEach(it => { it.onclick = () => { + if (!guardDrawerDirty()) return; + lastTriggerEl = it; toggleProjectSwitcher(false); openProjectDrawer(drawerSelected); }; @@ -590,6 +594,7 @@ function openProjectDrawer(projectName) { } function closeProjectDrawer() { + if (!guardDrawerDirty()) return; drawerOpen = false; drawer.classList.remove('open'); } @@ -622,9 +627,32 @@ function loadDrawerForm(projectName) { tplEl.value = ''; wrEl.value = ''; rulesEl.value = ''; codeEl.value = ''; designEl.value = ''; delBtn.style.display = 'none'; } + // B6:记录字段快照用于脏检测 + drawerSnapshot = drawerSnapshotNow(); renderDrawerList(); } +// B6:抽屉未保存变更守卫 +function drawerSnapshotNow() { + return [ + document.getElementById('pf-name').value.trim(), + document.getElementById('pf-display').value.trim(), + document.getElementById('pf-template').value.trim(), + document.getElementById('pf-write').value.trim(), + document.getElementById('pf-rules').value.trim(), + document.getElementById('pf-code').value.trim(), + document.getElementById('pf-design').value.trim(), + ].join('\u0001'); +} +function isDrawerDirty() { + if (!drawerOpen || drawerSnapshot === null) return false; + return drawerSnapshotNow() !== drawerSnapshot; +} +function guardDrawerDirty() { + if (isDrawerDirty()) return confirm('有未保存的修改,确定放弃?'); + return true; +} + function renderDrawerList() { let html = '
' + '+新建项目
'; @@ -636,6 +664,7 @@ function renderDrawerList() { box.innerHTML = html; box.querySelectorAll('.drawer-item').forEach(it => { it.onclick = () => { + if (!guardDrawerDirty()) return; const n = it.dataset.name === '' ? null : it.dataset.name; loadDrawerForm(n); }; @@ -754,7 +783,21 @@ async function loadSession(id) { async function send() { const text = inputEl.value.trim(); - if (!text || !sid) return; + if (!text) return; + if (!sid) { + // 首条消息:先落库会话(D-v4) + try { + const d = await api('POST', '/api/sessions', { user_id: 'default', project: draftProject || null }); + sid = d.session_id; + activeProject = draftProject || null; + badge.textContent = '会话: ' + (d.name || '新会话'); + try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ } + await refreshSessions(); + } catch (e) { + addMsg('error', '创建会话失败:' + esc(String(e))); + return; + } + } inputEl.value = ''; addMsg('user', esc(text)); const typing = showTyping(); @@ -789,7 +832,7 @@ document.getElementById('send').addEventListener('click', send); inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') send(); }); 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(); toggleProjectSwitcher(); }); +document.getElementById('ps-current').addEventListener('click', (e) => { e.stopPropagation(); lastTriggerEl = e.currentTarget; toggleProjectSwitcher(); }); document.getElementById('drawer-close').addEventListener('click', closeProjectDrawer); document.getElementById('pf-save').addEventListener('click', saveDrawerProject); document.getElementById('pf-delete').addEventListener('click', deleteDrawerProject); @@ -801,14 +844,15 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (psMenu.classList.contains('open')) toggleProjectSwitcher(false); else if (drawerOpen) closeProjectDrawer(); + if (lastTriggerEl) { lastTriggerEl.focus(); lastTriggerEl = null; } } }); document.getElementById('upload-btn').addEventListener('click', async () => { + if (!sid) { addMsg('error', '请先发送一条消息以创建会话'); inputEl.focus(); return; } let ft = document.getElementById('file-type').value; - if (activeProject || currentProject) ft = 'requirements'; + if (activeProject || draftProject) ft = 'requirements'; const file = document.getElementById('file-input').files[0]; - if (!sid) { addMsg('error', '请先创建会话'); return; } if (!file) { addMsg('error', '请选择文件'); return; } const fd = new FormData(); fd.append('file_type', ft); @@ -820,14 +864,17 @@ document.getElementById('upload-btn').addEventListener('click', async () => { const d = await r.json(); if (!r.ok) throw new Error(d.detail?.message || r.status); statusEl.textContent = ''; - addMsg('user', '[上传] ' + ft + ':' + d.file_name); - addMsg('progress', '上传完成:' + esc(d.file_name) + '(' + d.size + 'B)'); - document.getElementById('file-input').value = ''; + // B5:上传成功后同步 activeProject 并刷新侧边栏状态 const rec = await api('GET', '/api/sessions/' + sid); + activeProject = rec.project || null; badge.textContent = '会话: ' + (rec.name || '新会话'); await refreshSessions(); + document.getElementById('file-input').value = ''; // 仅成功时清空 + addMsg('user', '[上传] ' + ft + ':' + d.file_name); + addMsg('progress', '上传完成:' + esc(d.file_name) + '(' + d.size + 'B)'); } catch (e) { statusEl.textContent = ''; + // F1:失败保留 file-input 值,展示后端明细 addMsg('error', '上传失败:' + esc(String(e))); } });