feat(ui): 要件定义书直传(点按钮开系统文件框/仅.xlsx/选定即上传)+移除 rag-bar(影响调查改由聊天驱动)+空输入禁用发送键

This commit is contained in:
lhl
2026-09-13 00:24:33 +08:00
parent 1e6d0838be
commit 4e25f76fa1
6 changed files with 178 additions and 575 deletions
@@ -0,0 +1,78 @@
# 要件定义书直传入口 + 移除 rag-bar(含输入区上移根治)— 实施计划
目标文件:`src/genesis/server/static/chat.html``frontend/src/main.ts``frontend/src/chat.css`
后端零改动(沿用 `POST /api/sessions/{sid}/files``file_type=requirements`)。
## 背景 / 问题
- 点「上传资料」展开 `#upload-panel``#composer`(底部锚定)变高 → 顶部输入框/按钮被顶上去。
- 需求:按钮改名「要件定义书」;**只允许上传要件定义书**;点击后**直接打开系统文件选择框**;选定即上传;其他资料改由项目配置提供。
- 另有同类“上移”来源:`#rag-bar` 初始 `hidden`,**建会话后被显示** → 工具栏高度变化 → 输入区再跳一次。
## 已确认决策
- D1 选定文件后**自动上传**(选定即 POST,无二次确认)。
- D2 未创建会话时点按钮 → 提示「请先发送一条消息以创建会话」。
- D3 会话内**只传要件定义书**`.xlsx`);模板/规则/做成说明书由项目配置提供;`.xls` 不支持(后端 `EXPECTED_SUFFIX``excel_reader``.xlsx`)。
- D4 **移除 `#rag-bar`**(DOM + 相关前端 JS)。理由:影响调查已由聊天驱动(`chat/agent.py` 意图 `impact``tests/test_chat_intent.py` 覆盖),按钮冗余;RAG 现不可用(0 片段/禁用)。“启用 RAG/影响调查”待下一轮 RAG 落地再以合适方式加回。
## 变更清单
### chat.html
- `#tb-upload``📎 上传资料``📄 要件定义书``title="上传要件定义书"`;补 `aria-label`
- 删除 `#upload-panel` 整块(`#file-type``.file-pick``#upload-btn``#proj-hint`)。
- `#composer-toolbar` 内保留 `<input type="file" id="file-input" accept=".xlsx" style="display:none">``<span id="upload-status"></span>`
- **删除 `#rag-bar`**`#rag-enabled` / `#rag-stats` / `#impact-btn`)。
### frontend/src/main.ts
- 删除常量 `fileTypeSel` / `projHintEl` / `uploadPanelEl`;保留 `fileInputEl` / `uploadStatusEl`
- 删除 `updateUploadBar()` 及**全部调用点**。
- 删除 `UPLOAD_EXT_RULES` / `projectHasPrefixedType`;保留 `pickExt`
- 新增 `uploadRequirements()`:无 `sid` 提示 → 取文件 → 校验 `.xlsx``FormData(file_type=requirements)` POST → 刷新标题/侧边栏 → 聊天区提示;失败提示;**各路径均清空 `file-input.value`**。
- 绑定:`#tb-upload` click → `fileInputEl.click()``fileInputEl` change → `uploadRequirements()`;删除 `#upload-btn` 绑定与面板切换。
- **删除 RAG 相关前端代码**:常量 `ragEnabledEl` / `ragStatsEl` / `ragBarEl` / `impactBtnEl``RAG_LS_KEY` / `readRagFlag` / `writeRagFlag` / `refreshRagStats()` / `updateImpactButton()``ragEnabledEl` change 监听、`impactBtnEl` click 监听;以及 `refreshRagStats()` 的全部调用点。
- `state.ts``resolveUploadType` / `shouldHideUploadSelect` 保留(node 测试覆盖),仅不再调用。
### frontend/src/chat.css
- 删除死样式:`#upload-bar``#upload-bar-inner``#upload-bar select(:focus)``.file-pick(:hover)``#upload-btn(:hover)``#proj-hint``#upload-panel``label.vh`
- **删除 RAG 样式**`#rag-bar``#rag-bar label.rag-switch``#rag-bar input[type=checkbox]`(及 `:disabled`)、`#rag-bar .rag-stats(.ready/.zero)``#rag-bar button`(`:disabled`/`:hover:not(:disabled)`)。
- 保留 `#upload-status`
## 验证
- `cd frontend && npm run verify`typecheck + node --test + build)。
- `python -m pytest`(覆盖率 ≥99%)。
- 手动:
- 点「要件定义书」→ 弹系统文件框(过滤 .xlsx)→ 选文件**自动上传** → 聊天区显示「上传完成」;
- **输入区/按钮不再上移**(上传面板已删、rag-bar 已移除、工具栏高度恒定)且仍钉底;
- 无会话时提示先发消息;聊天框输入「做影响调查」仍可触发后端影响调查意图。
## 风险
- 会话内不再能补传模板/规则/做成说明书 → 完全依赖项目配置;未配置则生成阶段报错(符合 D3)。
- 移除 rag-bar 后,UI 不再有“启用 RAG / 影响调查”按钮;RAG 开关语义暂不可用(当前本就无效),下一轮 RAG 落地时重新引入。
- 自动上传无二次确认(符合 D1)。
---
## 评审结论与计划调整(plan-eng-review
### 事实核对(Step 0
- 后端 `/files``file_type: str = Form(...)` + `file: UploadFile = File(...)``requirements` 允许,白名单 `(".xlsx",)`。✅
- `updateUploadBar()` 定义 1 处 + 调用 5 处 + 上传分支内 1 处,删函数须清干净(漏调用 tsc 会报错)。✅
- `.vh` 仅用于将被删的「上传类型」label。✅
- 无测试引用将删的 id。✅
### 采纳
- **F1(同类“上移”来源)**`#rag-bar` 建会话后出现会使工具栏变高 → 一并处理:**移除 rag-bar**(DOM+JS)。影响调查改由聊天驱动(已有意图解析)。
- **F2**:仅 `.xlsx`(与后端/解析一致);`accept` 仅过滤,`change` 再校验后缀。
- **F3**:所有路径清空 `fileInputEl.value`(同文件再次可选)。
- **F4**:清理上传相关死样式与 `.vh`
- **F7**:按钮补 `aria-label`
### 接受
- **F5**:会话内不再补传模板/规则 → 依赖项目配置(D3 结果)。
- **F6**:新上传流为 DOM 行为,无自动化测试;靠 tsc + 手动。
## GSTACK REVIEW REPORT
- STATUS: DONE
- REVIEWED: docs/superpowers/plans/2026-09-12-requirements-upload.md
- FINDINGS: F1 同类上移(已采纳:移除 rag-bar) / F2 扩展名(仅 .xlsx) / F3 重复选择(已纳入) / F4 死样式清理 / F7 ariaF5/F6 接受
- KEY DECISION: 移除 rag-bar(影响调查由聊天驱动);会话内仅传 .xlsx 要件定义书,选定即上传
- NEXT: 待执行
-56
View File
@@ -229,34 +229,6 @@ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -.02em; margin: 0
font-family: var(--font-mono); font-family: var(--font-mono);
} }
/* === RAG 控制条 (spec §2.2) === */
#rag-bar {
display: flex; align-items: center; gap: 8px;
background: transparent; border: 1px solid var(--border-subtle);
border-radius: 999px; padding: 3px 10px;
font-size: 12px; color: var(--text-secondary);
font-family: var(--font-mono);
}
#rag-bar label.rag-switch {
display: inline-flex; align-items: center; gap: 6px;
cursor: pointer; user-select: none;
}
#rag-bar input[type=checkbox] {
width: 14px; height: 14px; accent-color: var(--accent);
margin: 0;
}
#rag-bar input[type=checkbox]:disabled + span { color: var(--text-muted); cursor: not-allowed; }
#rag-bar .rag-stats { color: var(--text-muted); }
#rag-bar .rag-stats.ready { color: var(--accent); font-weight: 600; }
#rag-bar .rag-stats.zero { color: var(--text-muted); }
#rag-bar button {
background: var(--accent); color: var(--bg-base); border: none;
border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 700;
cursor: pointer; transition: all var(--dur) var(--ease);
}
#rag-bar button:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; }
#rag-bar button:hover:not(:disabled) { box-shadow: 0 0 16px var(--accent-soft); }
/* === 聊天区 (spec §2.3) === */ /* === 聊天区 (spec §2.3) === */
#chat { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-6) var(--sp-4); max-width: none; width: 100%; margin: 0; } #chat { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-6) var(--sp-4); max-width: none; width: 100%; margin: 0; }
.msg { display: flex; margin: var(--sp-3) 0; padding-right: var(--composer-gutter); animation: zoomIn .25s var(--ease); position: relative; } .msg { display: flex; margin: var(--sp-3) 0; padding-right: var(--composer-gutter); animation: zoomIn .25s var(--ease); position: relative; }
@@ -300,34 +272,7 @@ header h1 { font-size: 16px; font-weight: 700; letter-spacing: -.02em; margin: 0
.actions a:hover { box-shadow: 0 0 16px var(--accent-soft); transform: translateY(-1px); } .actions a:hover { box-shadow: 0 0 16px var(--accent-soft); transform: translateY(-1px); }
.typing { color: var(--text-muted); font-size: 13px; animation: auraPulse 1.4s ease-in-out infinite; font-family: var(--font-mono); } .typing { color: var(--text-muted); font-size: 13px; animation: auraPulse 1.4s ease-in-out infinite; font-family: var(--font-mono); }
/* === 上传条 (spec §2.4) === */
#upload-bar { background: var(--bg-surface); border-top: 1px solid var(--border-subtle); padding: var(--sp-3) var(--sp-6); font-size: 13px; }
#upload-bar-inner { max-width: 880px; margin: 0 auto; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
#upload-bar select {
font-size: 13px; padding: 8px 12px; border: 1px solid var(--border-strong); border-radius: var(--r-md);
background: var(--bg-input); color: var(--text-primary); cursor: pointer; outline: none;
transition: all var(--dur) var(--ease);
font-family: var(--font-ui);
}
#upload-bar select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.file-pick {
display: inline-block; padding: 8px 14px; border: 1px solid var(--border-strong); border-radius: var(--r-md);
background: var(--bg-input); color: var(--text-secondary); font-size: 13px; font-weight: 500;
cursor: pointer; transition: all var(--dur) var(--ease);
}
.file-pick:hover { border-color: var(--accent); color: var(--accent); }
#upload-btn {
background: transparent; border: 1px solid var(--border-strong); color: var(--text-primary);
border-radius: var(--r-md); padding: 8px 16px; cursor: pointer; font-size: 13px; font-weight: 600;
transition: all var(--dur) var(--ease);
}
#upload-btn:hover { background: var(--accent); border-color: var(--accent); color: var(--bg-base); }
#upload-status { color: var(--text-muted); font-size: 12px; font-family: var(--font-mono); } #upload-status { color: var(--text-muted); font-size: 12px; font-family: var(--font-mono); }
#proj-hint {
color: var(--warning); font-size: 12px; background: rgba(251,191,36,.1); border: 1px solid var(--warning);
border-left-width: 4px; border-radius: var(--r-sm); padding: 4px 10px;
}
label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
/* === 输入区 (spec §2.5) === */ /* === 输入区 (spec §2.5) === */
#composer { flex-shrink: 0; border-top: none; background: transparent; padding: var(--sp-3) var(--sp-4) var(--sp-4); } #composer { flex-shrink: 0; border-top: none; background: transparent; padding: var(--sp-3) var(--sp-4) var(--sp-4); }
@@ -456,7 +401,6 @@ label.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px
.tb-btn .tb-ic { font-size: 14px; line-height: 1; } .tb-btn .tb-ic { font-size: 14px; line-height: 1; }
.tb-btn .tb-caret { font-size: 10px; color: var(--text-muted); } .tb-btn .tb-caret { font-size: 10px; color: var(--text-muted); }
.proj-switcher { position: relative; } .proj-switcher { position: relative; }
#upload-panel { max-width: none; margin: 10px 0 0; }
/* === 会话项「⋯」与重命名菜单 === */ /* === 会话项「⋯」与重命名菜单 === */
.sess .kebab { flex-shrink: 0; opacity: 0; transition: opacity .15s ease; background: transparent; .sess .kebab { flex-shrink: 0; opacity: 0; transition: opacity .15s ease; background: transparent;
+44 -170
View File
@@ -6,7 +6,7 @@ import * as GenesisState from './state.ts';
import * as GenesisWS from './ws.ts'; import * as GenesisWS from './ws.ts';
import type { import type {
SessionListItem, SessionRecord, SessionCreated, ProjectConfig, ProjectBody, SessionListItem, SessionRecord, SessionCreated, ProjectConfig, ProjectBody,
ChatMessage, ChatReply, RagStats, UploadResp, RenameResp, ProgressItem, ChatMessage, ChatReply, UploadResp, RenameResp, ProgressItem,
} from './types.ts'; } from './types.ts';
let sid: string | null = null; let sid: string | null = null;
@@ -33,16 +33,8 @@ const sidebarToggleBtn = must<HTMLButtonElement>('sidebar-toggle');
const projSwitcherEl = maybe<HTMLDivElement>('proj-switcher'); const projSwitcherEl = maybe<HTMLDivElement>('proj-switcher');
const sessMenuEl = must<HTMLDivElement>('sess-menu'); const sessMenuEl = must<HTMLDivElement>('sess-menu');
const fileTypeSel = must<HTMLSelectElement>('file-type');
const fileInputEl = must<HTMLInputElement>('file-input'); const fileInputEl = must<HTMLInputElement>('file-input');
const uploadStatusEl = must<HTMLElement>('upload-status'); const uploadStatusEl = must<HTMLElement>('upload-status');
const projHintEl = must<HTMLElement>('proj-hint');
const uploadPanelEl = maybe<HTMLDivElement>('upload-panel');
const ragEnabledEl = must<HTMLInputElement>('rag-enabled');
const ragStatsEl = must<HTMLElement>('rag-stats');
const ragBarEl = must<HTMLElement>('rag-bar');
const impactBtnEl = must<HTMLButtonElement>('impact-btn');
const pfName = must<HTMLInputElement>('pf-name'); const pfName = must<HTMLInputElement>('pf-name');
const pfDisplay = must<HTMLInputElement>('pf-display'); const pfDisplay = must<HTMLInputElement>('pf-display');
@@ -180,7 +172,6 @@ function onProjectRowClick(name: string): void {
setExplicitNone(false); setExplicitNone(false);
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
} }
const collapsed = loadCollapsedProjects(); const collapsed = loadCollapsedProjects();
if (collapsed.has(name)) collapsed.delete(name); else collapsed.add(name); if (collapsed.has(name)) collapsed.delete(name); else collapsed.add(name);
@@ -242,7 +233,6 @@ function renderProjectSwitcher(): void {
setExplicitNone(draftProject === null); setExplicitNone(draftProject === null);
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
toggleProjectSwitcher(false); toggleProjectSwitcher(false);
}; };
}); });
@@ -389,23 +379,13 @@ function toggleSidebar(force?: boolean): void {
try { localStorage.setItem('genesis_sidebar_collapsed', willCollapse ? '1' : '0'); } catch (e) { /* 忽略 */ } try { localStorage.setItem('genesis_sidebar_collapsed', willCollapse ? '1' : '0'); } catch (e) { /* 忽略 */ }
} }
// ---------- 上传区 ----------
function updateUploadBar(): void {
fileTypeSel.style.display = '';
if (fileTypeSel.value !== 'requirements') fileTypeSel.value = 'requirements';
if (draftProject) {
projHintEl.textContent = '默认上传要件定义 xlsx;如需补传模板/规则/既有系统 zip,请在下拉中选择对应类型。';
} else {
projHintEl.textContent = '请选择 file_type 后上传(未选项目时,模板/规则仍可手动上传)。';
}
}
// ---------- 空态/欢迎渲染 ---------- // ---------- 空态/欢迎渲染 ----------
function insertPrompt(text: string): void { function insertPrompt(text: string): void {
inputEl.value = text; inputEl.value = text;
inputEl.focus(); inputEl.focus();
try { inputEl.setSelectionRange(text.length, text.length); } catch (e) { /* 忽略 */ } try { inputEl.setSelectionRange(text.length, text.length); } catch (e) { /* 忽略 */ }
autoGrowInput(); autoGrowInput();
updateSendState();
} }
function renderEmptyOrWelcome(): void { function renderEmptyOrWelcome(): void {
@@ -478,8 +458,6 @@ async function newSession(): Promise<void> {
try { localStorage.removeItem('genesis_session'); } catch (e) { /* 忽略 */ } try { localStorage.removeItem('genesis_session'); } catch (e) { /* 忽略 */ }
chatEl.innerHTML = ''; chatEl.innerHTML = '';
setText(sessionTitle, '未创建会话'); setText(sessionTitle, '未创建会话');
updateUploadBar();
await refreshRagStats();
await refreshSessions(); await refreshSessions();
renderEmptyOrWelcome(); renderEmptyOrWelcome();
} }
@@ -503,8 +481,6 @@ async function loadSession(id: string): Promise<void> {
const role = roleMap[m.role] || 'assistant'; const role = roleMap[m.role] || 'assistant';
addMsg(role, esc(m.content)); addMsg(role, esc(m.content));
} }
updateUploadBar();
await refreshRagStats();
await refreshSessions(); await refreshSessions();
try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ } try { localStorage.setItem('genesis_session', sid); } catch (e) { /* 忽略 */ }
renderProjectMismatchHint(); renderProjectMismatchHint();
@@ -540,7 +516,6 @@ function renderProjectMismatchHint(): void {
draftProject = activeProject; draftProject = activeProject;
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
void refreshSessions(); void refreshSessions();
wrap.remove(); wrap.remove();
}; };
@@ -557,11 +532,13 @@ function renderProjectMismatchHint(): void {
// ---------- 发送 ---------- // ---------- 发送 ----------
let sendInFlight = false; let sendInFlight = false;
function updateSendState(): void { sendBtn.disabled = sendInFlight || inputEl.value.trim() === ''; }
async function send(): Promise<void> { async function send(): Promise<void> {
if (sendInFlight) return; if (sendInFlight) return;
const text = inputEl.value.trim(); const text = inputEl.value.trim();
if (!text) return; if (!text) return;
sendInFlight = true; sendInFlight = true;
updateSendState();
if (!sid) { if (!sid) {
try { try {
const d = await api<SessionCreated>('POST', '/api/sessions', { user_id: 'default', project: draftProject || null }); const d = await api<SessionCreated>('POST', '/api/sessions', { user_id: 'default', project: draftProject || null });
@@ -573,6 +550,7 @@ async function send(): Promise<void> {
} catch (e) { } catch (e) {
addMsg('error', '创建会话失败:' + esc(String(e))); addMsg('error', '创建会话失败:' + esc(String(e)));
sendInFlight = false; sendInFlight = false;
updateSendState();
return; return;
} }
} }
@@ -588,7 +566,6 @@ async function send(): Promise<void> {
if (es) es.remove(); if (es) es.remove();
addMsg('user', esc(text)); addMsg('user', esc(text));
const typing = showTyping(); const typing = showTyping();
sendBtn.disabled = true;
try { try {
const res = await api<ChatReply>('POST', '/api/chat/' + encodeURIComponent(sessionId) + '/messages', { content: text }); const res = await api<ChatReply>('POST', '/api/chat/' + encodeURIComponent(sessionId) + '/messages', { content: text });
typing.remove(); typing.remove();
@@ -611,7 +588,7 @@ async function send(): Promise<void> {
addMsg('error', '请求失败:' + esc(String(e))); addMsg('error', '请求失败:' + esc(String(e)));
} finally { } finally {
sendInFlight = false; sendInFlight = false;
sendBtn.disabled = false; updateSendState();
inputEl.focus(); inputEl.focus();
} }
} }
@@ -620,7 +597,8 @@ document.getElementById('send')!.addEventListener('click', () => { void send();
inputEl.addEventListener('keydown', (e: KeyboardEvent) => { inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void send(); } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void send(); }
}); });
inputEl.addEventListener('input', autoGrowInput); inputEl.addEventListener('input', () => { autoGrowInput(); updateSendState(); });
updateSendState();
document.getElementById('new-chat')!.addEventListener('click', () => { void newSession().catch((e) => addMsg('error', esc(String(e)))); }); document.getElementById('new-chat')!.addEventListener('click', () => { void newSession().catch((e) => addMsg('error', esc(String(e)))); });
sidebarToggleBtn.addEventListener('click', () => toggleSidebar()); sidebarToggleBtn.addEventListener('click', () => toggleSidebar());
document.getElementById('ps-current')!.addEventListener('click', (e) => { document.getElementById('ps-current')!.addEventListener('click', (e) => {
@@ -636,9 +614,8 @@ document.getElementById('side-add-project')!.addEventListener('click', () => {
if (!guardDrawerDirty()) return; if (!guardDrawerDirty()) return;
openProjectDrawer(null); openProjectDrawer(null);
}); });
document.getElementById('tb-upload')!.addEventListener('click', () => { document.getElementById('tb-upload')!.addEventListener('click', () => { fileInputEl.click(); });
if (uploadPanelEl) uploadPanelEl.hidden = !uploadPanelEl.hidden; fileInputEl.addEventListener('change', () => { void uploadRequirements(); });
});
document.getElementById('sm-rename')!.addEventListener('click', (ev) => { document.getElementById('sm-rename')!.addEventListener('click', (ev) => {
ev.stopPropagation(); ev.stopPropagation();
const t = sessMenuTarget; const t = sessMenuTarget;
@@ -646,82 +623,6 @@ document.getElementById('sm-rename')!.addEventListener('click', (ev) => {
if (t) startRenameSession(t.el, { session_id: t.sid, name: t.name }); if (t) startRenameSession(t.el, { session_id: t.sid, name: t.name });
}); });
// ---------- RAG 开关 + 状态徽标 + 开始影响调查 ----------
const RAG_LS_KEY = 'genesis_rag_enabled';
function readRagFlag(): boolean {
try { return localStorage.getItem(RAG_LS_KEY) === '1'; } catch (e) { return false; }
}
function writeRagFlag(v: boolean): void {
try { localStorage.setItem(RAG_LS_KEY, v ? '1' : '0'); } catch (e) { /* 忽略 */ }
}
ragEnabledEl.addEventListener('change', () => { writeRagFlag(ragEnabledEl.checked); updateImpactButton(); });
async function refreshRagStats(): Promise<void> {
if (!sid) {
ragBarEl.hidden = true;
return;
}
ragBarEl.hidden = false;
let stats: RagStats = { chunks: 0, rag_enabled: false };
try {
stats = await api<RagStats>('GET', '/api/sessions/' + encodeURIComponent(sid) + '/rag-stats');
} catch (e) {
stats = { chunks: 0, rag_enabled: false };
}
const n = Number(stats.chunks) || 0;
setText(ragStatsEl, n > 0
? 'RAG 已索引 ' + n + ' 片段'
: 'RAG 0 片段(请在项目配置中设置代码库目录)');
ragStatsEl.classList.toggle('ready', n > 0);
ragStatsEl.classList.toggle('zero', n === 0);
ragEnabledEl.disabled = n === 0;
if (n === 0) {
ragEnabledEl.checked = false;
writeRagFlag(false);
if (ragEnabledEl.parentElement) ragEnabledEl.parentElement.title = '请在项目配置中设置既有系统代码库目录';
} else {
ragEnabledEl.checked = readRagFlag();
if (ragEnabledEl.parentElement) {
ragEnabledEl.parentElement.title = ragEnabledEl.checked
? 'RAG 检索结果会注入到 LLM prompt'
: '勾选后启用 RAG 检索';
}
}
updateImpactButton();
}
function updateImpactButton(): void {
if (!sid) { impactBtnEl.disabled = true; return; }
const hasRag = (Number(ragStatsEl.dataset.chunks || 0) > 0) || !ragEnabledEl.disabled;
impactBtnEl.disabled = !hasRag;
setText(impactBtnEl, ragEnabledEl.checked
? '开始影响调查(启用 RAG'
: '开始影响调查');
}
impactBtnEl.addEventListener('click', () => {
void (async () => {
if (!sid) { addMsg('error', '请先发送一条消息以创建会话'); inputEl.focus(); return; }
const useRag = !!ragEnabledEl.checked;
impactBtnEl.disabled = true;
const typing = showTyping();
try {
const url = '/api/sessions/' + encodeURIComponent(sid) + '/start-impact?use_rag=' + (useRag ? 'true' : 'false');
const r = await fetch(url, { method: 'POST' });
const d = (await r.json().catch(() => ({}))) as { detail?: { message?: string } };
if (!r.ok) throw new Error((d && d.detail && d.detail.message) || String(r.status));
typing.remove();
addMsg('progress', '<span class="progress-item ok">影响调查已提交(use_rag=' + (useRag ? 'true' : 'false') + '</span>');
addMsg('assistant', '影响调查已启动。完成后可在此会话点击"预览"或访问 /result/impact-report 下载报告。');
} catch (e) {
typing.remove();
addMsg('error', '影响调查失败:' + esc(String(e)));
} finally {
updateImpactButton();
}
})();
});
document.addEventListener('click', (e: MouseEvent) => { document.addEventListener('click', (e: MouseEvent) => {
const target = e.target as Node; const target = e.target as Node;
if (projSwitcherEl && !projSwitcherEl.contains(target)) toggleProjectSwitcher(false); if (projSwitcherEl && !projSwitcherEl.contains(target)) toggleProjectSwitcher(false);
@@ -736,72 +637,45 @@ document.addEventListener('keydown', (e: KeyboardEvent) => {
} }
}); });
// 客户端类型 → 扩展名白名单校验 // 取扩展名(小写,含点)
const UPLOAD_EXT_RULES: Record<string, string[]> = {
existing_system: ['.zip'],
requirements: ['.xlsx'],
template: ['.docx'],
write_instruction: ['.docx'],
rules: ['.docx', '.xlsx'],
};
function pickExt(name: string): string { function pickExt(name: string): string {
const i = String(name).lastIndexOf('.'); const i = String(name).lastIndexOf('.');
return i >= 0 ? String(name).slice(i).toLowerCase() : ''; return i >= 0 ? String(name).slice(i).toLowerCase() : '';
} }
function projectHasPrefixedType(ft: string): boolean {
const name = activeProject || draftProject;
if (!name) return false;
const p = (projects || []).find((x) => x.name === name);
if (!p) return false;
if (ft === 'template') return !!(p.template && p.template.trim());
if (ft === 'write_instruction') return !!(p.write_instruction && p.write_instruction.trim());
if (ft === 'existing_system') return !!(p.existing_system_code_dir && p.existing_system_code_dir.trim());
if (ft === 'rules') return Array.isArray(p.rules) && p.rules.length > 0;
return false;
}
document.getElementById('upload-btn')!.addEventListener('click', () => { // 要件定义书上传:仅 .xlsx;选定文件即自动上传
void (async () => { async function uploadRequirements(): Promise<void> {
if (!sid) { addMsg('error', '请先发送一条消息以创建会话'); inputEl.focus(); return; } const file = fileInputEl.files && fileInputEl.files[0];
const ft = GenesisState.resolveUploadType(activeProject, draftProject, fileTypeSel.value); if (!sid) { addMsg('error', '请先发送一条消息以创建会话'); inputEl.focus(); fileInputEl.value = ''; return; }
const file = fileInputEl.files && fileInputEl.files[0]; if (!file) return;
if (!file) { addMsg('error', '请选择文件'); return; } if (pickExt(file.name) !== '.xlsx') {
const allowed = UPLOAD_EXT_RULES[ft]; addMsg('error', '要件定义书须为 .xlsx 文件(当前:' + esc(file.name) + '');
if (allowed) { fileInputEl.value = '';
const ext = pickExt(file.name); return;
if (!ext || !allowed.includes(ext)) { }
addMsg('error', '类型不匹配:' + esc(ft) + ' 应为 ' + allowed.join('/') + ' 文件(当前:' + esc(file.name) + ''); const sessionId = sid;
return; const fd = new FormData();
} fd.append('file_type', 'requirements');
} fd.append('file', file);
if (projectHasPrefixedType(ft)) { setText(uploadStatusEl, '上传中…');
const ok = confirm('项目已预置 ' + ft + '。继续上传将覆盖该会话中的此类型文件,是否继续?'); try {
if (!ok) return; const r = await fetch('/api/sessions/' + encodeURIComponent(sessionId) + '/files', { method: 'POST', body: fd });
} const d = (await r.json().catch(() => ({}))) as UploadResp & { detail?: { message?: string } };
const fd = new FormData(); if (!r.ok) throw new Error((d && d.detail && d.detail.message) || String(r.status));
fd.append('file_type', ft); setText(uploadStatusEl, '');
fd.append('file', file); const rec = await api<SessionRecord>('GET', '/api/sessions/' + encodeURIComponent(sessionId));
setText(uploadStatusEl, '上传中…'); activeProject = rec.project || null;
try { setText(sessionTitle, (rec.name || '新会话'));
const r = await fetch('/api/sessions/' + encodeURIComponent(sid as string) + '/files', { method: 'POST', body: fd }); await refreshSessions();
const d = (await r.json().catch(() => ({}))) as UploadResp & { detail?: { message?: string } }; addMsg('user', '[上传] 要件定义书:' + d.file_name);
if (!r.ok) throw new Error((d && d.detail && d.detail.message) || String(r.status)); addMsg('progress', '<span class="progress-item ok">上传完成:' + esc(d.file_name) + '' + d.size + 'B</span>');
setText(uploadStatusEl, ''); } catch (e) {
const rec = await api<SessionRecord>('GET', '/api/sessions/' + encodeURIComponent(sid as string)); setText(uploadStatusEl, '');
activeProject = rec.project || null; addMsg('error', '上传失败:' + esc(String(e)));
setText(sessionTitle, (rec.name || '新会话')); } finally {
await refreshSessions(); fileInputEl.value = '';
updateUploadBar(); }
await refreshRagStats(); }
fileInputEl.value = '';
addMsg('user', '[上传] ' + ft + '' + d.file_name);
addMsg('progress', '<span class="progress-item ok">上传完成:' + esc(d.file_name) + '' + d.size + 'B</span>');
} catch (e) {
setText(uploadStatusEl, '');
addMsg('error', '上传失败:' + esc(String(e)));
}
})();
});
// ---------- 启动:恢复侧边栏折叠态、上次会话 ---------- // ---------- 启动:恢复侧边栏折叠态、上次会话 ----------
void (async () => { void (async () => {
-143
View File
@@ -557,64 +557,6 @@ header h1 {
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: var(--font-mono); font-family: var(--font-mono);
} }
#rag-bar {
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 1px solid var(--border-subtle);
border-radius: 999px;
padding: 3px 10px;
font-size: 12px;
color: var(--text-secondary);
font-family: var(--font-mono);
}
#rag-bar label.rag-switch {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
}
#rag-bar input[type=checkbox] {
width: 14px;
height: 14px;
accent-color: var(--accent);
margin: 0;
}
#rag-bar input[type=checkbox]:disabled + span {
color: var(--text-muted);
cursor: not-allowed;
}
#rag-bar .rag-stats {
color: var(--text-muted);
}
#rag-bar .rag-stats.ready {
color: var(--accent);
font-weight: 600;
}
#rag-bar .rag-stats.zero {
color: var(--text-muted);
}
#rag-bar button {
background: var(--accent);
color: var(--bg-base);
border: none;
border-radius: 999px;
padding: 4px 12px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all var(--dur) var(--ease);
}
#rag-bar button:disabled {
background: var(--bg-elevated);
color: var(--text-muted);
cursor: not-allowed;
}
#rag-bar button:hover:not(:disabled) {
box-shadow: 0 0 16px var(--accent-soft);
}
#chat { #chat {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -709,92 +651,11 @@ header h1 {
animation: auraPulse 1.4s ease-in-out infinite; animation: auraPulse 1.4s ease-in-out infinite;
font-family: var(--font-mono); font-family: var(--font-mono);
} }
#upload-bar {
background: var(--bg-surface);
border-top: 1px solid var(--border-subtle);
padding: var(--sp-3) var(--sp-6);
font-size: 13px;
}
#upload-bar-inner {
max-width: 880px;
margin: 0 auto;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
#upload-bar select {
font-size: 13px;
padding: 8px 12px;
border: 1px solid var(--border-strong);
border-radius: var(--r-md);
background: var(--bg-input);
color: var(--text-primary);
cursor: pointer;
outline: none;
transition: all var(--dur) var(--ease);
font-family: var(--font-ui);
}
#upload-bar select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.file-pick {
display: inline-block;
padding: 8px 14px;
border: 1px solid var(--border-strong);
border-radius: var(--r-md);
background: var(--bg-input);
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all var(--dur) var(--ease);
}
.file-pick:hover {
border-color: var(--accent);
color: var(--accent);
}
#upload-btn {
background: transparent;
border: 1px solid var(--border-strong);
color: var(--text-primary);
border-radius: var(--r-md);
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all var(--dur) var(--ease);
}
#upload-btn:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--bg-base);
}
#upload-status { #upload-status {
color: var(--text-muted); color: var(--text-muted);
font-size: 12px; font-size: 12px;
font-family: var(--font-mono); font-family: var(--font-mono);
} }
#proj-hint {
color: var(--warning);
font-size: 12px;
background: rgba(251, 191, 36, .1);
border: 1px solid var(--warning);
border-left-width: 4px;
border-radius: var(--r-sm);
padding: 4px 10px;
}
label.vh {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
#composer { #composer {
flex-shrink: 0; flex-shrink: 0;
border-top: none; border-top: none;
@@ -1194,10 +1055,6 @@ label.vh {
.proj-switcher { .proj-switcher {
position: relative; position: relative;
} }
#upload-panel {
max-width: none;
margin: 10px 0 0;
}
.sess .kebab { .sess .kebab {
flex-shrink: 0; flex-shrink: 0;
opacity: 0; opacity: 0;
+4 -28
View File
@@ -40,7 +40,7 @@
<div id="composer"> <div id="composer">
<div id="composer-inner"> <div id="composer-inner">
<textarea id="input" rows="1" placeholder="输入指令,如:生成概要设计书 / 现在什么状态?(Enter 发送,Shift+Enter 换行)" autocomplete="off" maxlength="2000"></textarea> <textarea id="input" rows="1" placeholder="输入指令,如:生成概要设计书 / 现在什么状态?(Enter 发送,Shift+Enter 换行)" autocomplete="off" maxlength="2000"></textarea>
<button id="send" aria-label="发送" title="发送"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg></button> <button id="send" aria-label="发送" title="发送" disabled><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg></button>
</div> </div>
<div id="composer-toolbar"> <div id="composer-toolbar">
@@ -52,33 +52,9 @@
</button> </button>
<div class="ps-menu" id="ps-menu" role="menu"></div> <div class="ps-menu" id="ps-menu" role="menu"></div>
</div> </div>
<button class="tb-btn" id="tb-upload" type="button" title="上传资料"><span class="tb-ic">📎</span><span class="tb-text">上传资料</span></button> <button class="tb-btn" id="tb-upload" type="button" title="上传要件定义书" aria-label="上传要件定义书"><span class="tb-ic">📄</span><span class="tb-text">要件定义书</span></button>
<div id="rag-bar" hidden> <input type="file" id="file-input" accept=".xlsx" style="display:none">
<label class="rag-switch"> <span id="upload-status"></span>
<input type="checkbox" id="rag-enabled">
<span>启用 RAG</span>
</label>
<span class="rag-stats" id="rag-stats">RAG 0 片段</span>
<button id="impact-btn" type="button" disabled>影响调查</button>
</div>
</div>
<div id="upload-panel" hidden>
<div id="upload-bar-inner">
<span id="proj-hint"></span>
<label for="file-type" class="vh">上传类型</label>
<span>📎 上传资料:</span>
<select id="file-type" aria-label="上传类型">
<option value="requirements">要件定义 xlsx(必需)</option>
<option value="template">概要设计模板 docx(必需)</option>
<option value="write_instruction">做成说明书 docx</option>
<option value="rules">记入/图表规则 docx/xlsx</option>
</select>
<label class="file-pick" for="file-input">选择文件</label>
<input type="file" id="file-input" style="display:none">
<button id="upload-btn">上传</button>
<span id="upload-status"></span>
</div>
</div> </div>
</div> </div>
</div> </div>
+52 -178
View File
@@ -42,9 +42,6 @@ function autoBindProject(draftProject2, projects2, explicitNone) {
} }
return draftProject2; return draftProject2;
} }
function resolveUploadType(_activeProject, _draftProject, selectValue) {
return selectValue;
}
function computeDrawerSnapshot(fields) { function computeDrawerSnapshot(fields) {
return fields.map((f) => (f == null ? "" : String(f)).trim()).join(String.fromCharCode(1)); return fields.map((f) => (f == null ? "" : String(f)).trim()).join(String.fromCharCode(1));
} }
@@ -131,15 +128,8 @@ var sidebarEl = must("sidebar");
var sidebarToggleBtn = must("sidebar-toggle"); var sidebarToggleBtn = must("sidebar-toggle");
var projSwitcherEl = maybe("proj-switcher"); var projSwitcherEl = maybe("proj-switcher");
var sessMenuEl = must("sess-menu"); var sessMenuEl = must("sess-menu");
var fileTypeSel = must("file-type");
var fileInputEl = must("file-input"); var fileInputEl = must("file-input");
var uploadStatusEl = must("upload-status"); var uploadStatusEl = must("upload-status");
var projHintEl = must("proj-hint");
var uploadPanelEl = maybe("upload-panel");
var ragEnabledEl = must("rag-enabled");
var ragStatsEl = must("rag-stats");
var ragBarEl = must("rag-bar");
var impactBtnEl = must("impact-btn");
var pfName = must("pf-name"); var pfName = must("pf-name");
var pfDisplay = must("pf-display"); var pfDisplay = must("pf-display");
var pfTemplate = must("pf-template"); var pfTemplate = must("pf-template");
@@ -281,7 +271,6 @@ function onProjectRowClick(name) {
setExplicitNone(false); setExplicitNone(false);
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
} }
const collapsed = loadCollapsedProjects(); const collapsed = loadCollapsedProjects();
if (collapsed.has(name)) collapsed.delete(name); if (collapsed.has(name)) collapsed.delete(name);
@@ -336,7 +325,6 @@ function renderProjectSwitcher() {
setExplicitNone(draftProject === null); setExplicitNone(draftProject === null);
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
toggleProjectSwitcher(false); toggleProjectSwitcher(false);
}; };
}); });
@@ -487,15 +475,6 @@ function toggleSidebar(force) {
} catch (e) { } catch (e) {
} }
} }
function updateUploadBar() {
fileTypeSel.style.display = "";
if (fileTypeSel.value !== "requirements") fileTypeSel.value = "requirements";
if (draftProject) {
projHintEl.textContent = "\u9ED8\u8BA4\u4E0A\u4F20\u8981\u4EF6\u5B9A\u4E49 xlsx\uFF1B\u5982\u9700\u8865\u4F20\u6A21\u677F/\u89C4\u5219/\u65E2\u6709\u7CFB\u7EDF zip\uFF0C\u8BF7\u5728\u4E0B\u62C9\u4E2D\u9009\u62E9\u5BF9\u5E94\u7C7B\u578B\u3002";
} else {
projHintEl.textContent = "\u8BF7\u9009\u62E9 file_type \u540E\u4E0A\u4F20\uFF08\u672A\u9009\u9879\u76EE\u65F6\uFF0C\u6A21\u677F/\u89C4\u5219\u4ECD\u53EF\u624B\u52A8\u4E0A\u4F20\uFF09\u3002";
}
}
function insertPrompt(text) { function insertPrompt(text) {
inputEl.value = text; inputEl.value = text;
inputEl.focus(); inputEl.focus();
@@ -504,6 +483,7 @@ function insertPrompt(text) {
} catch (e) { } catch (e) {
} }
autoGrowInput(); autoGrowInput();
updateSendState();
} }
function renderEmptyOrWelcome() { function renderEmptyOrWelcome() {
chatEl.innerHTML = ""; chatEl.innerHTML = "";
@@ -584,8 +564,6 @@ async function newSession() {
} }
chatEl.innerHTML = ""; chatEl.innerHTML = "";
setText(sessionTitle, "\u672A\u521B\u5EFA\u4F1A\u8BDD"); setText(sessionTitle, "\u672A\u521B\u5EFA\u4F1A\u8BDD");
updateUploadBar();
await refreshRagStats();
await refreshSessions(); await refreshSessions();
renderEmptyOrWelcome(); renderEmptyOrWelcome();
} }
@@ -614,8 +592,6 @@ async function loadSession(id) {
const role = roleMap[m.role] || "assistant"; const role = roleMap[m.role] || "assistant";
addMsg(role, esc(m.content)); addMsg(role, esc(m.content));
} }
updateUploadBar();
await refreshRagStats();
await refreshSessions(); await refreshSessions();
try { try {
localStorage.setItem("genesis_session", sid); localStorage.setItem("genesis_session", sid);
@@ -650,7 +626,6 @@ function renderProjectMismatchHint() {
draftProject = activeProject; draftProject = activeProject;
applyProjectContext(); applyProjectContext();
renderProjectSwitcher(); renderProjectSwitcher();
updateUploadBar();
void refreshSessions(); void refreshSessions();
wrap.remove(); wrap.remove();
}; };
@@ -666,11 +641,15 @@ function renderProjectMismatchHint() {
}; };
} }
var sendInFlight = false; var sendInFlight = false;
function updateSendState() {
sendBtn.disabled = sendInFlight || inputEl.value.trim() === "";
}
async function send() { async function send() {
if (sendInFlight) return; if (sendInFlight) return;
const text = inputEl.value.trim(); const text = inputEl.value.trim();
if (!text) return; if (!text) return;
sendInFlight = true; sendInFlight = true;
updateSendState();
if (!sid) { if (!sid) {
try { try {
const d = await api("POST", "/api/sessions", { user_id: "default", project: draftProject || null }); const d = await api("POST", "/api/sessions", { user_id: "default", project: draftProject || null });
@@ -685,6 +664,7 @@ async function send() {
} catch (e) { } catch (e) {
addMsg("error", "\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A" + esc(String(e))); addMsg("error", "\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A" + esc(String(e)));
sendInFlight = false; sendInFlight = false;
updateSendState();
return; return;
} }
} }
@@ -706,7 +686,6 @@ async function send() {
if (es) es.remove(); if (es) es.remove();
addMsg("user", esc(text)); addMsg("user", esc(text));
const typing = showTyping(); const typing = showTyping();
sendBtn.disabled = true;
try { try {
const res = await api("POST", "/api/chat/" + encodeURIComponent(sessionId) + "/messages", { content: text }); const res = await api("POST", "/api/chat/" + encodeURIComponent(sessionId) + "/messages", { content: text });
typing.remove(); typing.remove();
@@ -725,7 +704,7 @@ async function send() {
addMsg("error", "\u8BF7\u6C42\u5931\u8D25\uFF1A" + esc(String(e))); addMsg("error", "\u8BF7\u6C42\u5931\u8D25\uFF1A" + esc(String(e)));
} finally { } finally {
sendInFlight = false; sendInFlight = false;
sendBtn.disabled = false; updateSendState();
inputEl.focus(); inputEl.focus();
} }
} }
@@ -738,7 +717,11 @@ inputEl.addEventListener("keydown", (e) => {
void send(); void send();
} }
}); });
inputEl.addEventListener("input", autoGrowInput); inputEl.addEventListener("input", () => {
autoGrowInput();
updateSendState();
});
updateSendState();
document.getElementById("new-chat").addEventListener("click", () => { document.getElementById("new-chat").addEventListener("click", () => {
void newSession().catch((e) => addMsg("error", esc(String(e)))); void newSession().catch((e) => addMsg("error", esc(String(e))));
}); });
@@ -761,7 +744,10 @@ document.getElementById("side-add-project").addEventListener("click", () => {
openProjectDrawer(null); openProjectDrawer(null);
}); });
document.getElementById("tb-upload").addEventListener("click", () => { document.getElementById("tb-upload").addEventListener("click", () => {
if (uploadPanelEl) uploadPanelEl.hidden = !uploadPanelEl.hidden; fileInputEl.click();
});
fileInputEl.addEventListener("change", () => {
void uploadRequirements();
}); });
document.getElementById("sm-rename").addEventListener("click", (ev) => { document.getElementById("sm-rename").addEventListener("click", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
@@ -769,88 +755,6 @@ document.getElementById("sm-rename").addEventListener("click", (ev) => {
closeSessMenu(); closeSessMenu();
if (t) startRenameSession(t.el, { session_id: t.sid, name: t.name }); if (t) startRenameSession(t.el, { session_id: t.sid, name: t.name });
}); });
var RAG_LS_KEY = "genesis_rag_enabled";
function readRagFlag() {
try {
return localStorage.getItem(RAG_LS_KEY) === "1";
} catch (e) {
return false;
}
}
function writeRagFlag(v) {
try {
localStorage.setItem(RAG_LS_KEY, v ? "1" : "0");
} catch (e) {
}
}
ragEnabledEl.addEventListener("change", () => {
writeRagFlag(ragEnabledEl.checked);
updateImpactButton();
});
async function refreshRagStats() {
if (!sid) {
ragBarEl.hidden = true;
return;
}
ragBarEl.hidden = false;
let stats = { chunks: 0, rag_enabled: false };
try {
stats = await api("GET", "/api/sessions/" + encodeURIComponent(sid) + "/rag-stats");
} catch (e) {
stats = { chunks: 0, rag_enabled: false };
}
const n = Number(stats.chunks) || 0;
setText(ragStatsEl, n > 0 ? "RAG \u5DF2\u7D22\u5F15 " + n + " \u7247\u6BB5" : "RAG 0 \u7247\u6BB5\uFF08\u8BF7\u5728\u9879\u76EE\u914D\u7F6E\u4E2D\u8BBE\u7F6E\u4EE3\u7801\u5E93\u76EE\u5F55\uFF09");
ragStatsEl.classList.toggle("ready", n > 0);
ragStatsEl.classList.toggle("zero", n === 0);
ragEnabledEl.disabled = n === 0;
if (n === 0) {
ragEnabledEl.checked = false;
writeRagFlag(false);
if (ragEnabledEl.parentElement) ragEnabledEl.parentElement.title = "\u8BF7\u5728\u9879\u76EE\u914D\u7F6E\u4E2D\u8BBE\u7F6E\u65E2\u6709\u7CFB\u7EDF\u4EE3\u7801\u5E93\u76EE\u5F55";
} else {
ragEnabledEl.checked = readRagFlag();
if (ragEnabledEl.parentElement) {
ragEnabledEl.parentElement.title = ragEnabledEl.checked ? "RAG \u68C0\u7D22\u7ED3\u679C\u4F1A\u6CE8\u5165\u5230 LLM prompt" : "\u52FE\u9009\u540E\u542F\u7528 RAG \u68C0\u7D22";
}
}
updateImpactButton();
}
function updateImpactButton() {
if (!sid) {
impactBtnEl.disabled = true;
return;
}
const hasRag = Number(ragStatsEl.dataset.chunks || 0) > 0 || !ragEnabledEl.disabled;
impactBtnEl.disabled = !hasRag;
setText(impactBtnEl, ragEnabledEl.checked ? "\u5F00\u59CB\u5F71\u54CD\u8C03\u67E5\uFF08\u542F\u7528 RAG\uFF09" : "\u5F00\u59CB\u5F71\u54CD\u8C03\u67E5");
}
impactBtnEl.addEventListener("click", () => {
void (async () => {
if (!sid) {
addMsg("error", "\u8BF7\u5148\u53D1\u9001\u4E00\u6761\u6D88\u606F\u4EE5\u521B\u5EFA\u4F1A\u8BDD");
inputEl.focus();
return;
}
const useRag = !!ragEnabledEl.checked;
impactBtnEl.disabled = true;
const typing = showTyping();
try {
const url = "/api/sessions/" + encodeURIComponent(sid) + "/start-impact?use_rag=" + (useRag ? "true" : "false");
const r = await fetch(url, { method: "POST" });
const d = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(d && d.detail && d.detail.message || String(r.status));
typing.remove();
addMsg("progress", '<span class="progress-item ok">\u5F71\u54CD\u8C03\u67E5\u5DF2\u63D0\u4EA4\uFF08use_rag=' + (useRag ? "true" : "false") + "\uFF09</span>");
addMsg("assistant", '\u5F71\u54CD\u8C03\u67E5\u5DF2\u542F\u52A8\u3002\u5B8C\u6210\u540E\u53EF\u5728\u6B64\u4F1A\u8BDD\u70B9\u51FB"\u9884\u89C8"\u6216\u8BBF\u95EE /result/impact-report \u4E0B\u8F7D\u62A5\u544A\u3002');
} catch (e) {
typing.remove();
addMsg("error", "\u5F71\u54CD\u8C03\u67E5\u5931\u8D25\uFF1A" + esc(String(e)));
} finally {
updateImpactButton();
}
})();
});
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const target = e.target; const target = e.target;
if (projSwitcherEl && !projSwitcherEl.contains(target)) toggleProjectSwitcher(false); if (projSwitcherEl && !projSwitcherEl.contains(target)) toggleProjectSwitcher(false);
@@ -867,77 +771,47 @@ document.addEventListener("keydown", (e) => {
} }
} }
}); });
var UPLOAD_EXT_RULES = {
existing_system: [".zip"],
requirements: [".xlsx"],
template: [".docx"],
write_instruction: [".docx"],
rules: [".docx", ".xlsx"]
};
function pickExt(name) { function pickExt(name) {
const i = String(name).lastIndexOf("."); const i = String(name).lastIndexOf(".");
return i >= 0 ? String(name).slice(i).toLowerCase() : ""; return i >= 0 ? String(name).slice(i).toLowerCase() : "";
} }
function projectHasPrefixedType(ft) { async function uploadRequirements() {
const name = activeProject || draftProject; const file = fileInputEl.files && fileInputEl.files[0];
if (!name) return false; if (!sid) {
const p = (projects || []).find((x) => x.name === name); addMsg("error", "\u8BF7\u5148\u53D1\u9001\u4E00\u6761\u6D88\u606F\u4EE5\u521B\u5EFA\u4F1A\u8BDD");
if (!p) return false; inputEl.focus();
if (ft === "template") return !!(p.template && p.template.trim()); fileInputEl.value = "";
if (ft === "write_instruction") return !!(p.write_instruction && p.write_instruction.trim()); return;
if (ft === "existing_system") return !!(p.existing_system_code_dir && p.existing_system_code_dir.trim()); }
if (ft === "rules") return Array.isArray(p.rules) && p.rules.length > 0; if (!file) return;
return false; if (pickExt(file.name) !== ".xlsx") {
addMsg("error", "\u8981\u4EF6\u5B9A\u4E49\u4E66\u987B\u4E3A .xlsx \u6587\u4EF6\uFF08\u5F53\u524D\uFF1A" + esc(file.name) + "\uFF09");
fileInputEl.value = "";
return;
}
const sessionId = sid;
const fd = new FormData();
fd.append("file_type", "requirements");
fd.append("file", file);
setText(uploadStatusEl, "\u4E0A\u4F20\u4E2D\u2026");
try {
const r = await fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/files", { method: "POST", body: fd });
const d = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(d && d.detail && d.detail.message || String(r.status));
setText(uploadStatusEl, "");
const rec = await api("GET", "/api/sessions/" + encodeURIComponent(sessionId));
activeProject = rec.project || null;
setText(sessionTitle, rec.name || "\u65B0\u4F1A\u8BDD");
await refreshSessions();
addMsg("user", "[\u4E0A\u4F20] \u8981\u4EF6\u5B9A\u4E49\u4E66\uFF1A" + d.file_name);
addMsg("progress", '<span class="progress-item ok">\u4E0A\u4F20\u5B8C\u6210\uFF1A' + esc(d.file_name) + "\uFF08" + d.size + "B\uFF09</span>");
} catch (e) {
setText(uploadStatusEl, "");
addMsg("error", "\u4E0A\u4F20\u5931\u8D25\uFF1A" + esc(String(e)));
} finally {
fileInputEl.value = "";
}
} }
document.getElementById("upload-btn").addEventListener("click", () => {
void (async () => {
if (!sid) {
addMsg("error", "\u8BF7\u5148\u53D1\u9001\u4E00\u6761\u6D88\u606F\u4EE5\u521B\u5EFA\u4F1A\u8BDD");
inputEl.focus();
return;
}
const ft = resolveUploadType(activeProject, draftProject, fileTypeSel.value);
const file = fileInputEl.files && fileInputEl.files[0];
if (!file) {
addMsg("error", "\u8BF7\u9009\u62E9\u6587\u4EF6");
return;
}
const allowed = UPLOAD_EXT_RULES[ft];
if (allowed) {
const ext = pickExt(file.name);
if (!ext || !allowed.includes(ext)) {
addMsg("error", "\u7C7B\u578B\u4E0D\u5339\u914D\uFF1A" + esc(ft) + " \u5E94\u4E3A " + allowed.join("/") + " \u6587\u4EF6\uFF08\u5F53\u524D\uFF1A" + esc(file.name) + "\uFF09");
return;
}
}
if (projectHasPrefixedType(ft)) {
const ok = confirm("\u9879\u76EE\u5DF2\u9884\u7F6E " + ft + "\u3002\u7EE7\u7EED\u4E0A\u4F20\u5C06\u8986\u76D6\u8BE5\u4F1A\u8BDD\u4E2D\u7684\u6B64\u7C7B\u578B\u6587\u4EF6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F");
if (!ok) return;
}
const fd = new FormData();
fd.append("file_type", ft);
fd.append("file", file);
setText(uploadStatusEl, "\u4E0A\u4F20\u4E2D\u2026");
try {
const r = await fetch("/api/sessions/" + encodeURIComponent(sid) + "/files", { method: "POST", body: fd });
const d = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(d && d.detail && d.detail.message || String(r.status));
setText(uploadStatusEl, "");
const rec = await api("GET", "/api/sessions/" + encodeURIComponent(sid));
activeProject = rec.project || null;
setText(sessionTitle, rec.name || "\u65B0\u4F1A\u8BDD");
await refreshSessions();
updateUploadBar();
await refreshRagStats();
fileInputEl.value = "";
addMsg("user", "[\u4E0A\u4F20] " + ft + "\uFF1A" + d.file_name);
addMsg("progress", '<span class="progress-item ok">\u4E0A\u4F20\u5B8C\u6210\uFF1A' + esc(d.file_name) + "\uFF08" + d.size + "B\uFF09</span>");
} catch (e) {
setText(uploadStatusEl, "");
addMsg("error", "\u4E0A\u4F20\u5931\u8D25\uFF1A" + esc(String(e)));
}
})();
});
void (async () => { void (async () => {
try { try {
const collapsed = localStorage.getItem("genesis_sidebar_collapsed") === "1"; const collapsed = localStorage.getItem("genesis_sidebar_collapsed") === "1";