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)));
}
});