From 00a936e5dca6595fae1fb63166f4d1ad6217f699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:50:05 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 184 +++++++++++++++++++++++++++++++++++++- web/static/js/projects.js | 6 +- web/static/js/roles.js | 82 +++++++++++++++++ web/static/js/webshell.js | 26 +++--- web/templates/index.html | 8 +- 5 files changed, 290 insertions(+), 16 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 655cc904..653a6604 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -22086,7 +22086,7 @@ html[data-theme="dark"] .vulnerability-card:hover { flex-direction: column; gap: 6px; min-height: 0; - overflow-y: auto; + overflow: visible; } .agent-mode-options > .role-selection-item-main { @@ -22105,6 +22105,61 @@ html[data-theme="dark"] .vulnerability-card:hover { text-align: left; } +.agent-mode-option::after { + content: attr(data-agent-mode-detail); + position: absolute; + left: calc(100% + 12px); + top: 50%; + z-index: 20; + width: max-content; + max-width: min(380px, calc(100vw - 40px)); + padding: 9px 11px; + border-radius: 8px; + background: rgba(17, 24, 39, 0.96); + color: #ffffff; + font-size: 0.75rem; + line-height: 1.45; + text-align: left; + white-space: normal; + overflow-wrap: anywhere; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22); + opacity: 0; + visibility: hidden; + transform: translate(4px, -50%); + pointer-events: none; + transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s ease; +} + +.agent-mode-option::before { + content: ""; + position: absolute; + left: calc(100% + 6px); + top: 50%; + z-index: 21; + width: 10px; + height: 10px; + background: rgba(17, 24, 39, 0.96); + opacity: 0; + visibility: hidden; + transform: translate(4px, -50%) rotate(45deg); + pointer-events: none; + transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s ease; +} + +.agent-mode-option:hover::after, +.agent-mode-option:focus-visible::after, +.agent-mode-option:hover::before, +.agent-mode-option:focus-visible::before { + opacity: 1; + visibility: visible; + transform: translate(0, -50%); +} + +.agent-mode-option:hover::before, +.agent-mode-option:focus-visible::before { + transform: translate(0, -50%) rotate(45deg); +} + /* 选项内勾选:未选中时隐藏(与角色列表一致) */ .agent-mode-option .agent-mode-check { @@ -22316,6 +22371,57 @@ html[data-theme="dark"] .vulnerability-card:hover { color: #8a5ab8; } +.selection-detail-tooltip { + position: fixed; + z-index: 10000; + max-width: min(380px, calc(100vw - 40px)); + padding: 9px 11px; + border-radius: 8px; + background: rgba(17, 24, 39, 0.96); + color: #ffffff; + font-size: 0.75rem; + line-height: 1.45; + text-align: left; + white-space: normal; + overflow-wrap: anywhere; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateX(4px); + transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s ease; +} + +.selection-detail-tooltip.visible { + opacity: 1; + visibility: visible; + transform: translateX(0); +} + +.selection-detail-tooltip::before { + content: ""; + position: absolute; + left: -5px; + top: 50%; + width: 10px; + height: 10px; + background: rgba(17, 24, 39, 0.96); + transform: translateY(-50%) rotate(45deg); +} + +.selection-detail-tooltip.placement-left { + transform: translateX(-4px); +} + +.selection-detail-tooltip.placement-left.visible { + transform: translateX(0); +} + +.selection-detail-tooltip.placement-left::before { + left: auto; + right: -5px; +} + .role-selection-checkmark-main { position: absolute; top: 10px; @@ -23225,6 +23331,25 @@ html[data-theme="dark"] .vulnerability-card:hover { .role-selection-item-description-main { font-size: 0.71875rem; } + + .agent-mode-option::before, + .agent-mode-option::after { + display: none; + } + + .agent-mode-option:hover .role-selection-item-description-main, + .agent-mode-option:focus-visible .role-selection-item-description-main { + opacity: 1; + } + + .agent-mode-option .role-selection-item-description-main { + -webkit-line-clamp: 2; + line-clamp: 2; + } + + .selection-detail-tooltip { + display: none; + } } @media (max-width: 480px) { @@ -28777,6 +28902,42 @@ html[data-theme="dark"] .webshell-db-count { border-color: rgba(96, 165, 250, 0.24) !important; } +html[data-theme="dark"] .projects-status-pill--active { + background: rgba(34, 197, 94, 0.14) !important; + color: #86efac !important; + border-color: rgba(34, 197, 94, 0.34) !important; +} + +html[data-theme="dark"] .projects-status-pill--archived { + background: rgba(148, 163, 184, 0.12) !important; + color: #cbd5e1 !important; + border-color: rgba(148, 163, 184, 0.28) !important; +} + +html[data-theme="dark"] .projects-stat-chip--facts { + background: rgba(59, 130, 246, 0.16) !important; + color: #93c5fd !important; + border-color: rgba(59, 130, 246, 0.34) !important; +} + +html[data-theme="dark"] .projects-stat-chip--vulns { + background: rgba(249, 115, 22, 0.14) !important; + color: #fdba74 !important; + border-color: rgba(249, 115, 22, 0.34) !important; +} + +html[data-theme="dark"] .projects-stat-chip--conversations { + background: rgba(168, 85, 247, 0.14) !important; + color: #d8b4fe !important; + border-color: rgba(168, 85, 247, 0.34) !important; +} + +html[data-theme="dark"] .projects-stat-chip--warn { + background: rgba(245, 158, 11, 0.14) !important; + color: #fcd34d !important; + border-color: rgba(245, 158, 11, 0.34) !important; +} + html[data-theme="dark"] .hitl-tool-badge { background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%) !important; color: #ffffff !important; @@ -29102,6 +29263,27 @@ html[data-theme="dark"] #page-projects .projects-list-item.is-active { border-color: rgba(96, 165, 250, 0.34) !important; } +html[data-theme="dark"] #page-projects .projects-list-item-menu { + color: #94a3b8 !important; +} + +html[data-theme="dark"] #page-projects .projects-list-item:hover .projects-list-item-menu, +html[data-theme="dark"] #page-projects .projects-list-item.is-active .projects-list-item-menu { + opacity: 0.9; +} + +html[data-theme="dark"] #page-projects .projects-list-item-menu:hover, +html[data-theme="dark"] #page-projects .projects-list-item-menu:focus-visible { + background: rgba(148, 163, 184, 0.16) !important; + color: #e2e8f0 !important; +} + +html[data-theme="dark"] #page-projects .projects-list-item.is-active .projects-list-item-menu:hover, +html[data-theme="dark"] #page-projects .projects-list-item.is-active .projects-list-item-menu:focus-visible { + background: rgba(96, 165, 250, 0.20) !important; + color: #dbeafe !important; +} + html[data-theme="dark"] #page-projects .projects-detail-meta, html[data-theme="dark"] #page-projects .projects-list-item-badge { background: rgba(96, 165, 250, 0.10) !important; diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 8ed522ce..2b80df76 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -2152,13 +2152,17 @@ function appendChatProjectPanelItem(list, project, selectedId, onSelect, tFn) { const t = tFn || tp; const isNone = !project.id; const isSelected = isNone ? !selectedId : selectedId === project.id; + const fullDesc = isNone + ? (project.description || '') + : (project.description || '').trim() || t('projects.sharedFactBoard'); const desc = isNone ? (project.description || '') - : (project.description || '').trim().slice(0, 80) || t('projects.sharedFactBoard'); + : fullDesc.slice(0, 80); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : ''); btn.setAttribute('role', 'option'); + btn.setAttribute('data-selection-detail', fullDesc); btn.onclick = () => onSelect(project.id || ''); btn.innerHTML = `
${isNone ? '—' : '📁'}
diff --git a/web/static/js/roles.js b/web/static/js/roles.js index cbc0ad54..95d586a3 100644 --- a/web/static/js/roles.js +++ b/web/static/js/roles.js @@ -24,6 +24,79 @@ function rolePlainDescription(role) { if (raw === 'roles.noDescription' || raw === 'roles.noDescriptionShort') return ''; return raw; } + +function initSelectionDetailTooltip() { + if (window.__selectionDetailTooltipReady) return; + window.__selectionDetailTooltipReady = true; + + const tooltip = document.createElement('div'); + tooltip.className = 'selection-detail-tooltip'; + tooltip.setAttribute('role', 'tooltip'); + document.body.appendChild(tooltip); + + let activeEl = null; + + function hideTooltip() { + activeEl = null; + tooltip.classList.remove('visible', 'placement-left'); + } + + function positionTooltip(el) { + const text = el && el.getAttribute('data-selection-detail'); + if (!text) { + hideTooltip(); + return; + } + activeEl = el; + tooltip.textContent = text; + tooltip.classList.add('visible'); + + const rect = el.getBoundingClientRect(); + const tipRect = tooltip.getBoundingClientRect(); + const gap = 12; + const margin = 12; + const rightSpace = window.innerWidth - rect.right - gap - margin; + const placeLeft = rightSpace < tipRect.width && rect.left > tipRect.width + gap + margin; + const left = placeLeft ? rect.left - tipRect.width - gap : rect.right + gap; + const top = Math.max(margin, Math.min(rect.top + rect.height / 2 - tipRect.height / 2, window.innerHeight - tipRect.height - margin)); + + tooltip.classList.toggle('placement-left', placeLeft); + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + } + + document.addEventListener('mouseover', function (event) { + const el = event.target && event.target.closest && event.target.closest('[data-selection-detail]'); + if (!el || (event.relatedTarget && el.contains(event.relatedTarget))) return; + positionTooltip(el); + }); + document.addEventListener('mouseout', function (event) { + const el = event.target && event.target.closest && event.target.closest('[data-selection-detail]'); + if (!el || (event.relatedTarget && el.contains(event.relatedTarget))) return; + hideTooltip(); + }); + document.addEventListener('focusin', function (event) { + const el = event.target && event.target.closest && event.target.closest('[data-selection-detail]'); + if (el) positionTooltip(el); + }); + document.addEventListener('focusout', function (event) { + const el = event.target && event.target.closest && event.target.closest('[data-selection-detail]'); + if (el) hideTooltip(); + }); + window.addEventListener('scroll', hideTooltip, true); + window.addEventListener('resize', function () { + if (activeEl) positionTooltip(activeEl); + }); +} + +if (typeof document !== 'undefined') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSelectionDetailTooltip); + } else { + initSelectionDetailTooltip(); + } +} + let currentRole = localStorage.getItem('currentRole') || ''; let roles = []; let rolesSearchKeyword = ''; // 角色搜索关键词 @@ -209,10 +282,18 @@ function renderRoleSelectionSidebar() { const isSelected = isDefaultRole ? (currentRole === '' || currentRole === '默认') : (currentRole === role.name); const roleItem = document.createElement('div'); roleItem.className = 'role-selection-item-main' + (isSelected ? ' selected' : ''); + roleItem.setAttribute('role', 'option'); + roleItem.tabIndex = 0; roleItem.onclick = () => { selectRole(role.name); closeRoleSelectionPanel(); // 选择后自动关闭面板 }; + roleItem.onkeydown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + roleItem.click(); + } + }; const icon = getRoleIcon(role); // 处理默认角色的描述 @@ -221,6 +302,7 @@ function renderRoleSelectionSidebar() { if (isDefaultRole && !plainDesc) { description = _t('roles.defaultRoleDescription'); } + roleItem.setAttribute('data-selection-detail', description); roleItem.innerHTML = `
${icon}
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index bef3b051..7c6895a2 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -223,12 +223,13 @@ function wsRenderRoleList() { var html = ''; // 默认角色 var defSelected = !cur ? ' selected' : ''; - html += ''; @@ -238,10 +239,11 @@ function wsRenderRoleList() { if (!r.enabled) continue; if (r.name === '默认') continue; // 已在上方硬编码默认角色,跳过 API 返回的默认项 var sel = (r.name === cur) ? ' selected' : ''; - html += ''; } @@ -1136,6 +1138,10 @@ function escapeHtml(s) { return div.innerHTML; } +function escapeHtmlAttr(s) { + return escapeHtml(s).replace(/"/g, '"').replace(/'/g, '''); +} + function escapeSingleQuotedShellArg(value) { var s = value == null ? '' : String(value); return "'" + s.replace(/'/g, "'\\''") + "'"; @@ -2260,10 +2266,10 @@ function selectWebshell(id, stateReady) { '' + '' + '
' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + '
' + '' + '' + diff --git a/web/templates/index.html b/web/templates/index.html index bbb534e3..d3002aa8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1146,7 +1146,7 @@
- - - -