feat(chat): 首条消息建会话(D-v4)+上传门禁(B5/F1)+Esc焦点(C1)+抽屉未保存守卫(B6)

This commit is contained in:
lhl
2026-08-28 02:12:04 +08:00
parent 851a3dae6a
commit 57df45eae5
+55 -8
View File
@@ -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 = '<div class="drawer-item' + (drawerSelected === null ? ' active' : '') + '" data-name="">'
+ '<span class="di-new">+</span><span class="di-name">新建项目</span></div>';
@@ -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', '<span class="progress-item ok">上传完成:' + esc(d.file_name) + '' + d.size + 'B</span>');
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', '<span class="progress-item ok">上传完成:' + esc(d.file_name) + '' + d.size + 'B</span>');
} catch (e) {
statusEl.textContent = '';
// F1:失败保留 file-input 值,展示后端明细
addMsg('error', '上传失败:' + esc(String(e)));
}
});