From 6c4b3bf13100ac28ddc8a1cf2ac398f69cbea177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:42:14 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 93 +++++++++++++++- web/static/i18n/en-US.json | 6 ++ web/static/i18n/zh-CN.json | 6 ++ web/static/js/projects.js | 216 ++++++++++++++++++++++++++++++++----- web/templates/index.html | 22 +++- 5 files changed, 311 insertions(+), 32 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index acc5f288..eb3bc9fe 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -22514,7 +22514,10 @@ button.chat-files-dropdown-item:hover:not(:disabled) { } .projects-list-item { position: relative; - padding: 10px 12px 10px 14px; + display: flex; + align-items: center; + gap: 4px; + padding: 10px 8px 10px 14px; border-radius: 8px; cursor: pointer; font-size: 0.875rem; @@ -22547,8 +22550,43 @@ button.chat-files-dropdown-item:hover:not(:disabled) { color: #94a3b8; } .projects-list-item-body { + flex: 1; min-width: 0; } +.projects-list-item-menu { + width: 24px; + height: 24px; + padding: 0; + border: none; + background: transparent; + color: var(--text-muted, #94a3b8); + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + opacity: 0; + transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease; +} +.projects-list-item:hover .projects-list-item-menu, +.projects-list-item.is-active .projects-list-item-menu { + opacity: 0.75; +} +.projects-list-item-menu:hover, +.projects-list-item-menu:focus-visible { + opacity: 1; + background: #e2e8f0; + color: var(--text-primary, #0f172a); + outline: none; +} +.projects-list-item.is-active .projects-list-item-menu:hover, +.projects-list-item.is-active .projects-list-item-menu:focus-visible { + background: #dbeafe; +} .projects-list-item-name { font-weight: 600; color: var(--text-primary, #0f172a); @@ -22679,12 +22717,61 @@ button.chat-files-dropdown-item:hover:not(:disabled) { font-size: 0.8125rem; color: #94a3b8; } -.projects-detail-desc { +.projects-detail-desc-block { margin: 10px 0 0; + max-width: min(640px, 100%); +} +.projects-detail-desc { + margin: 0; font-size: 0.875rem; color: #475569; line-height: 1.55; - max-width: 640px; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; +} +.projects-detail-desc.is-collapsed { + max-height: 4.65em; + overflow: hidden; + position: relative; +} +.projects-detail-desc.is-collapsed::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1.4em; + background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 85%); + pointer-events: none; +} +.projects-detail-desc.is-expanded { + max-height: min(240px, 32vh); + overflow-y: auto; + overscroll-behavior: contain; + padding-right: 4px; +} +.projects-detail-desc-toggle { + display: inline-block; + margin-top: 6px; + padding: 0; + border: none; + background: none; + color: #0066ff; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + line-height: 1.4; +} +.projects-detail-desc-toggle:hover, +.projects-detail-desc-toggle:focus-visible { + text-decoration: underline; + outline: none; +} +.projects-description-textarea { + max-height: 200px; + resize: vertical; + overflow-y: auto; } .projects-detail-stats { display: flex; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 6d0c4833..7a72abd8 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -324,6 +324,9 @@ "statsSparse": "{{count}} incomplete", "projectNotFound": "Project not found", "updatedPrefix": "Updated {{time}}", + "descExpand": "Show all", + "descCollapse": "Show less", + "descriptionLengthHint": "Keep it brief (max 4000 chars). Put long logs/POCs in fact board body instead.", "noMatchingFacts": "No matching facts, try adjusting filters", "noFacts": "No facts yet. Click Add fact or let Agent write facts automatically", "relatedVulnIdTitle": "Related vulnerability ID", @@ -407,6 +410,9 @@ "dangerZoneTitle": "Danger zone", "dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.", "archiveRestore": "Archive / Restore", + "archiveProject": "Archive", + "restoreProjectActive": "Restore to active", + "projectActions": "Project actions", "deleteProject": "Delete project", "saveChangesHint": "Click save to sync changes to server", "saveSettings": "Save changes", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 81ee5273..5396c9e1 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -312,6 +312,9 @@ "statsSparse": "{{count}} 待补全", "projectNotFound": "项目不存在", "updatedPrefix": "更新于 {{time}}", + "descExpand": "展开全部", + "descCollapse": "收起", + "descriptionLengthHint": "简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body", "noMatchingFacts": "无匹配事实,请调整筛选条件", "noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入", "relatedVulnIdTitle": "关联漏洞 ID", @@ -395,6 +398,9 @@ "dangerZoneTitle": "危险操作", "dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。", "archiveRestore": "归档 / 恢复", + "archiveProject": "归档", + "restoreProjectActive": "恢复为进行中", + "projectActions": "项目操作", "deleteProject": "删除项目", "saveChangesHint": "修改后请点击保存以同步到服务器", "saveSettings": "保存更改", diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 35b03b78..4da8fd14 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -11,6 +11,8 @@ let _projectsListReady = false; let _projectsFetchPromise = null; const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId'; +const PROJECT_DESCRIPTION_MAX_LENGTH = 4000; +const PROJECT_DESC_COLLAPSE_THRESHOLD = 180; function tp(key, opts) { if (typeof window.t === 'function') return window.t(key, opts); @@ -611,11 +613,71 @@ function renderProjectsSidebar() {
${escapeHtml(p.name)}${badges}
${formatProjectTime(p.updated_at)}
+ `; }).join(''); updateProjectsDetailVisibility(); } +function clampProjectDescription(text) { + const s = (text || '').trim(); + if (s.length <= PROJECT_DESCRIPTION_MAX_LENGTH) return s; + return s.slice(0, PROJECT_DESCRIPTION_MAX_LENGTH); +} + +function projectDescriptionNeedsToggle(text) { + const s = (text || '').trim(); + if (!s) return false; + if (s.length > PROJECT_DESC_COLLAPSE_THRESHOLD) return true; + return s.split('\n').length > 3; +} + +function renderProjectDetailDesc(desc) { + const blockEl = document.getElementById('projects-detail-desc-block'); + const descEl = document.getElementById('projects-detail-desc'); + const toggleEl = document.getElementById('projects-detail-desc-toggle'); + if (!descEl || !blockEl) return; + const text = (desc || '').trim(); + if (!text) { + blockEl.hidden = true; + descEl.textContent = ''; + descEl.className = 'projects-detail-desc is-collapsed'; + if (toggleEl) { + toggleEl.hidden = true; + toggleEl.dataset.expanded = 'false'; + } + return; + } + descEl.textContent = text; + blockEl.hidden = false; + descEl.classList.remove('is-expanded'); + descEl.classList.add('is-collapsed'); + if (toggleEl) { + const needsToggle = projectDescriptionNeedsToggle(text); + toggleEl.hidden = !needsToggle; + toggleEl.textContent = tp('projects.descExpand'); + toggleEl.dataset.expanded = 'false'; + } +} + +function toggleProjectDetailDesc() { + const descEl = document.getElementById('projects-detail-desc'); + const toggleEl = document.getElementById('projects-detail-desc-toggle'); + if (!descEl || !toggleEl || toggleEl.hidden) return; + const expanded = toggleEl.dataset.expanded === 'true'; + if (expanded) { + descEl.classList.add('is-collapsed'); + descEl.classList.remove('is-expanded'); + toggleEl.textContent = tp('projects.descExpand'); + toggleEl.dataset.expanded = 'false'; + } else { + descEl.classList.remove('is-collapsed'); + descEl.classList.add('is-expanded'); + toggleEl.textContent = tp('projects.descCollapse'); + toggleEl.dataset.expanded = 'true'; + } +} + function updateProjectStatusPill(status) { const el = document.getElementById('projects-detail-status'); if (!el) return; @@ -682,16 +744,7 @@ async function selectProject(id) { const metaEl = document.getElementById('projects-detail-meta'); if (metaEl) metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${formatProjectTime(p.updated_at)}`, { time: formatProjectTime(p.updated_at) }); const descEl = document.getElementById('projects-detail-desc'); - if (descEl) { - const desc = (p.description || '').trim(); - if (desc) { - descEl.textContent = desc; - descEl.hidden = false; - } else { - descEl.textContent = ''; - descEl.hidden = true; - } - } + if (descEl) renderProjectDetailDesc(p.description); projectNameById[p.id] = p.name || p.id; } catch (e) { console.warn(e); @@ -1205,7 +1258,7 @@ async function saveProjectModal() { if (!name) return alert(tp('projects.enterProjectName')); const body = { name, - description: document.getElementById('project-modal-description').value.trim(), + description: clampProjectDescription(document.getElementById('project-modal-description').value), }; const editId = window._projectModalEditId; const res = editId @@ -1272,7 +1325,7 @@ async function saveProjectSettings() { } const body = { name: document.getElementById('project-edit-name').value.trim(), - description: document.getElementById('project-edit-description').value.trim(), + description: clampProjectDescription(document.getElementById('project-edit-description').value), scope_json: scopeRaw, status: document.getElementById('project-edit-status')?.value || 'active', pinned: !!document.getElementById('project-edit-pinned')?.checked, @@ -1288,30 +1341,110 @@ async function saveProjectSettings() { alert(tp('projects.saved')); } -async function archiveCurrentProject() { - if (!currentProjectId) return; - const statusEl = document.getElementById('project-edit-status'); - const cur = statusEl?.value || 'active'; +function findProjectById(projectId) { + return projectsCache.find((p) => p.id === projectId) || projectsCacheAll.find((p) => p.id === projectId); +} + +let _projectListMenuTargetId = null; +let _projectListMenuDocClickBound = false; + +function closeProjectListActionMenu() { + const menu = document.getElementById('projects-list-action-menu'); + if (!menu) return; + menu.style.display = 'none'; + _projectListMenuTargetId = null; +} + +function positionProjectListActionMenu(event) { + const menu = document.getElementById('projects-list-action-menu'); + if (!menu) return; + menu.style.display = 'block'; + menu.style.visibility = 'visible'; + menu.style.opacity = '1'; + void menu.offsetHeight; + const menuRect = menu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + let left = event.clientX; + let top = event.clientY; + if (left + menuRect.width > viewportWidth) { + left = Math.max(8, event.clientX - menuRect.width); + } + if (top + menuRect.height > viewportHeight) { + top = Math.max(8, event.clientY - menuRect.height); + } + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; +} + +function showProjectListActionMenu(event, projectId) { + event.stopPropagation(); + event.preventDefault(); + const menu = document.getElementById('projects-list-action-menu'); + if (!menu) return; + if (_projectListMenuTargetId === projectId && menu.style.display === 'block') { + closeProjectListActionMenu(); + return; + } + closeProjectListActionMenu(); + const p = findProjectById(projectId); + if (!p) return; + _projectListMenuTargetId = projectId; + const archiveText = document.getElementById('projects-list-menu-archive-text'); + const deleteText = document.getElementById('projects-list-menu-delete-text'); + if (archiveText) { + archiveText.textContent = p.status === 'archived' + ? tp('projects.restoreProjectActive') + : tp('projects.archiveProject'); + } + if (deleteText) deleteText.textContent = tp('projects.deleteProject'); + positionProjectListActionMenu(event); +} + +function initProjectListActionMenu() { + if (_projectListMenuDocClickBound) return; + _projectListMenuDocClickBound = true; + document.addEventListener('click', (event) => { + const menu = document.getElementById('projects-list-action-menu'); + if (!menu || menu.style.display === 'none') return; + if (menu.contains(event.target)) return; + if (event.target.closest('.projects-list-item-menu')) return; + closeProjectListActionMenu(); + }); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') closeProjectListActionMenu(); + }); +} + +async function toggleProjectArchiveById(projectId) { + const p = findProjectById(projectId); + if (!p) return; + const cur = p.status || 'active'; const next = cur === 'archived' ? 'active' : 'archived'; if (!confirm(next === 'archived' ? tp('projects.confirmArchiveProject') : tp('projects.confirmRestoreProjectActive'))) return; - const res = await apiFetch(`/api/projects/${currentProjectId}`, { + const res = await apiFetch(`/api/projects/${projectId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: next }), }); if (!res.ok) return alert(tp('projects.operationFailed')); await loadProjectsList(); - await selectProject(currentProjectId); + if (currentProjectId === projectId && projectsCache.some((item) => item.id === projectId)) { + await selectProject(projectId); + } else if (currentProjectId === projectId) { + currentProjectId = null; + updateProjectsDetailVisibility(); + if (projectsCache.length) await selectProject(projectsCache[0].id); + } } -async function deleteCurrentProject() { - if (!currentProjectId || !confirm(tp('projects.confirmDeleteProject'))) return; - const deletedId = currentProjectId; - const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId); - const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' }); +async function deleteProjectById(projectId) { + if (!projectId || !confirm(tp('projects.confirmDeleteProject'))) return; + const deletedIndex = projectsCache.findIndex((p) => p.id === projectId); + const res = await apiFetch(`/api/projects/${projectId}`, { method: 'DELETE' }); if (!res.ok) return alert(tp('projects.deleteFailed')); - if (getActiveProjectId() === deletedId) setActiveProjectId(''); - currentProjectId = null; + if (getActiveProjectId() === projectId) setActiveProjectId(''); + if (currentProjectId === projectId) currentProjectId = null; await loadProjectsList(); if (projectsCache.length) { const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1); @@ -1321,6 +1454,30 @@ async function deleteCurrentProject() { } } +async function toggleProjectArchiveFromListMenu() { + const projectId = _projectListMenuTargetId; + closeProjectListActionMenu(); + if (!projectId) return; + await toggleProjectArchiveById(projectId); +} + +async function deleteProjectFromListMenu() { + const projectId = _projectListMenuTargetId; + closeProjectListActionMenu(); + if (!projectId) return; + await deleteProjectById(projectId); +} + +async function archiveCurrentProject() { + if (!currentProjectId) return; + await toggleProjectArchiveById(currentProjectId); +} + +async function deleteCurrentProject() { + if (!currentProjectId) return; + await deleteProjectById(currentProjectId); +} + function resetFactModalForm() { window._factModalEditId = null; const keyEl = document.getElementById('fact-modal-key'); @@ -1731,9 +1888,13 @@ function initChatProjectSelector() { } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initChatProjectSelector); + document.addEventListener('DOMContentLoaded', () => { + initChatProjectSelector(); + initProjectListActionMenu(); + }); } else { initChatProjectSelector(); + initProjectListActionMenu(); } window.initProjectsPage = initProjectsPage; @@ -1752,6 +1913,9 @@ window.closeFactDetailModal = closeFactDetailModal; window.saveProjectSettings = saveProjectSettings; window.archiveCurrentProject = archiveCurrentProject; window.deleteCurrentProject = deleteCurrentProject; +window.showProjectListActionMenu = showProjectListActionMenu; +window.toggleProjectArchiveFromListMenu = toggleProjectArchiveFromListMenu; +window.deleteProjectFromListMenu = deleteProjectFromListMenu; window.refreshChatProjectSelector = refreshChatProjectSelector; window.onChatProjectChange = onChatProjectChange; window.toggleChatProjectPanel = toggleChatProjectPanel; diff --git a/web/templates/index.html b/web/templates/index.html index 90a9e093..28fa0ef4 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1476,7 +1476,10 @@ 进行中

-

+
0 条事实 0 个漏洞 @@ -1669,7 +1672,8 @@
- + + 简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body
@@ -3777,6 +3781,17 @@ + + +