From 4a57574cf944779151ae0bc0fe8480f1aa09c0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:21:51 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 249 +++++++++++++++++++- web/static/i18n/en-US.json | 10 + web/static/i18n/zh-CN.json | 10 + web/static/js/chat.js | 459 ++++++++++++++++++++++++++++++++++--- web/static/js/monitor.js | 53 ++++- web/static/js/projects.js | 10 + web/templates/index.html | 16 +- 7 files changed, 755 insertions(+), 52 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index a76b8d1f..4bb3c660 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1615,9 +1615,34 @@ header { .conversation-search-box { position: relative; + margin-bottom: 10px; +} + +.conversation-sidebar .sidebar-content { + padding: 10px 16px 16px; +} + +.conversation-sidebar .conversation-search-box { + margin-top: 8px; + margin-bottom: 10px; +} + +.conversation-sidebar .conversation-project-filter { + margin-bottom: 10px; +} + +.conversation-sidebar .conversation-groups-section { margin-bottom: 12px; } +.conversation-sidebar .recent-conversations-section { + margin-bottom: 12px; +} + +.conversation-sidebar .section-header { + margin-bottom: 8px; +} + .conversation-search-box input { width: 100%; padding: 8px 32px 8px 12px; @@ -1668,6 +1693,170 @@ header { height: 14px; } +.conversation-project-filter { + margin-bottom: 12px; + min-width: 0; +} + +.conversation-project-filter-label { + display: block; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); + margin-bottom: 4px; + padding: 0 2px; +} + +.conversation-project-filter-native { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.conversation-project-filter-ui { + position: relative; + width: 100%; + min-width: 0; +} + +.conversation-project-filter-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + min-width: 0; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.875rem; + line-height: 1.25; + cursor: pointer; + font-family: inherit; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.conversation-project-filter-trigger:hover:not(:disabled) { + border-color: var(--accent-color); +} + +.conversation-project-filter-ui.open .conversation-project-filter-trigger { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.conversation-project-filter-ui.open { + z-index: 120; +} + +.conversation-project-filter-value { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; +} + +.conversation-project-filter-caret { + flex-shrink: 0; + color: var(--text-secondary); + transition: transform 0.15s ease; +} + +.conversation-project-filter-ui.open .conversation-project-filter-caret { + transform: rotate(180deg); +} + +.conversation-project-filter-dropdown { + display: none; + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: 200; + max-height: 280px; + overflow-x: hidden; + overflow-y: auto; + padding: 4px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: var(--shadow-lg); +} + +.conversation-project-filter-ui.open .conversation-project-filter-dropdown { + display: block; +} + +.conversation-project-filter-option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-width: 0; + padding: 8px 10px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-primary); + font-size: 0.8125rem; + font-family: inherit; + cursor: pointer; + text-align: left; + transition: background 0.12s ease, color 0.12s ease; +} + +.conversation-project-filter-option:hover { + background: var(--bg-secondary); +} + +.conversation-project-filter-option.is-selected { + background: rgba(0, 102, 255, 0.08); + color: var(--accent-color); + font-weight: 500; +} + +.conversation-project-filter-check { + width: 14px; + flex-shrink: 0; + opacity: 0; + font-size: 0.75rem; + line-height: 1; + color: var(--accent-color); +} + +.conversation-project-filter-option.is-selected .conversation-project-filter-check { + opacity: 1; +} + +.conversation-project-filter-option-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conversation-item-project-badge { + font-size: 0.6875rem; + color: var(--text-muted); + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.3; +} + .conversations-list { display: flex; flex-direction: column; @@ -11196,6 +11385,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .conversation-groups-section, .recent-conversations-section { margin-bottom: 24px; + min-width: 0; } .conversation-groups-section:last-child, @@ -11209,6 +11399,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { justify-content: space-between; margin-bottom: 12px; padding: 0 8px; + min-width: 0; + gap: 8px; } .section-header-actions { @@ -11337,6 +11529,21 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { letter-spacing: 0.5px; } +.recent-conversations-section .section-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.recent-conversations-section .section-title.section-title--filtered { + text-transform: none; + letter-spacing: normal; + font-size: 0.875rem; + color: var(--text-primary); +} + .add-group-btn, .batch-manage-btn { width: 24px; @@ -11729,7 +11936,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { /* 批量管理模态框 */ .batch-manage-modal-content { - max-width: 800px; + max-width: 920px; width: 90vw; display: flex; flex-direction: column; @@ -11739,7 +11946,23 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-manage-header-actions { display: flex; align-items: center; - gap: 12px; + gap: 10px; + min-width: 0; +} + +.batch-manage-header-actions .conversation-project-filter-ui { + width: 148px; + min-width: 108px; + flex-shrink: 0; +} + +.batch-manage-header-actions .conversation-project-filter-trigger { + font-size: 0.8125rem; + padding: 8px 10px; +} + +.batch-manage-modal-content .conversation-project-filter-ui.open { + z-index: 400; } .batch-search-box { @@ -11783,8 +12006,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-table-header { display: grid; - grid-template-columns: 40px 1fr 180px 80px; - gap: 16px; + grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px; + gap: 12px; padding: 12px 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); @@ -11802,8 +12025,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-conversation-row { display: grid; - grid-template-columns: 40px 1fr 180px 80px; - gap: 16px; + grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px; + gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border-color); align-items: center; @@ -11830,6 +12053,20 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { /* 完全依赖JavaScript截断,禁用CSS的ellipsis以避免在UTF-8多字节字符中间截断 */ } +.batch-table-col-project { + font-size: 0.8125rem; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.batch-table-col-project.is-unbound { + color: var(--text-muted); + font-style: italic; + opacity: 0.85; +} + .batch-table-col-time { font-size: 0.875rem; color: var(--text-muted); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index c5797641..59af7468 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -499,6 +499,13 @@ "conversationGroups": "Conversation groups", "addGroup": "New group", "recentConversations": "Recent conversations", + "filterByProject": "Filter by project", + "filterAllProjects": "All projects", + "filterUnboundProjects": "Unbound", + "projectConversationsTitle": "{{name}} · Conversations", + "unboundConversationsTitle": "Unbound conversations", + "noProjectConversations": "No conversations in this project", + "noUnboundConversations": "No unbound conversations", "sortConversations": "Sort", "sortByCreatedAt": "Created time", "sortByUpdatedAt": "Updated time", @@ -2527,6 +2534,9 @@ "title": "Manage conversations · {{count}} total", "searchPlaceholder": "Search history", "conversationName": "Conversation name", + "project": "Project", + "noProject": "No project", + "filterByProject": "Filter by project", "lastTime": "Last activity", "action": "Action", "selectAll": "Select all", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index a4a8bb15..79114c79 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -487,6 +487,13 @@ "conversationGroups": "对话分组", "addGroup": "新建分组", "recentConversations": "最近对话", + "filterByProject": "按项目筛选", + "filterAllProjects": "全部项目", + "filterUnboundProjects": "未绑定项目", + "projectConversationsTitle": "{{name}} · 对话", + "unboundConversationsTitle": "未绑定项目", + "noProjectConversations": "该项目暂无对话", + "noUnboundConversations": "暂无未绑定项目的对话", "sortConversations": "排序", "sortByCreatedAt": "创建时间", "sortByUpdatedAt": "更新时间", @@ -2515,6 +2522,9 @@ "title": "管理对话记录·共{{count}}条", "searchPlaceholder": "搜索历史记录", "conversationName": "对话名称", + "project": "项目", + "noProject": "无项目", + "filterByProject": "按项目筛选", "lastTime": "最近一次对话时间", "action": "操作", "selectAll": "全选", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 45c1a084..ec4ac190 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -3322,6 +3322,18 @@ function createConversationListItem(conversation) { title.title = titleText; // 设置完整标题以便悬停查看 contentWrapper.appendChild(title); + if (!getConversationProjectFilter()) { + const pid = conversation.projectId || conversation.project_id || ''; + const projectName = pid && window.projectNameById ? window.projectNameById[pid] : ''; + if (projectName) { + const badge = document.createElement('div'); + badge.className = 'conversation-item-project-badge'; + badge.textContent = projectName; + badge.title = projectName; + contentWrapper.appendChild(badge); + } + } + const time = document.createElement('div'); time.className = 'conversation-time'; time.textContent = conversation._timeText || formatConversationTimestamp(conversation._time || new Date()); @@ -3867,14 +3879,7 @@ async function deleteConversation(conversationId, skipConfirm = false) { const batchModal = document.getElementById('batch-manage-modal'); if (batchModal && isAppModalOpen('batch-manage-modal')) { allConversationsForBatch = allConversationsForBatch.filter(c => c.id !== conversationId); - updateBatchManageTitle(allConversationsForBatch.length); - const searchInput = document.getElementById('batch-search-input'); - const query = searchInput ? searchInput.value : ''; - if (query && query.trim()) { - filterBatchConversations(query); - } else { - renderBatchConversations(); - } + applyBatchConversationFilters(); } // 通知其他模块(如 WebShell AI 助手)同步删除,保持列表一致 @@ -6075,6 +6080,266 @@ let pendingGroupMappings = {}; // 待保留的分组映射(用于处理后端A let conversationsListLoadSeq = 0; // 对话列表加载序号,避免并发请求导致重复渲染 const CONVERSATIONS_PAGE_SIZE_KEY = 'cyberstrike.conversations_page_size'; const CONVERSATIONS_SORT_KEY = 'cyberstrike.conversations_sort_by'; +const CONVERSATIONS_PROJECT_FILTER_KEY = 'cyberstrike.conversations_project_filter'; +const CONVERSATION_PROJECT_FILTER_NONE = '__none__'; +const CONVERSATION_PROJECT_FILTER_SELECT_ID = 'conversation-project-filter'; +const CONVERSATION_PROJECT_FILTER_CARET = ''; +const BATCH_PROJECT_FILTER_SELECT_ID = 'batch-project-filter'; +const projectFilterCustomSelectRegistry = {}; +let projectFilterCustomSelectDocBound = false; + +function closeProjectFilterCustomSelect(selectId) { + const reg = projectFilterCustomSelectRegistry[selectId]; + if (!reg || !reg.wrapper) return; + reg.wrapper.classList.remove('open'); + if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false'); +} + +function closeAllProjectFilterCustomSelects() { + Object.keys(projectFilterCustomSelectRegistry).forEach(closeProjectFilterCustomSelect); +} + +function syncProjectFilterCustomSelect(selectId) { + const reg = projectFilterCustomSelectRegistry[selectId]; + if (!reg) return; + const { select, dropdown, trigger } = reg; + const valueSpan = trigger.querySelector('.conversation-project-filter-value'); + dropdown.innerHTML = ''; + Array.prototype.forEach.call(select.options, (opt) => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'conversation-project-filter-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-value', opt.value); + const labelText = opt.textContent || ''; + item.title = labelText; + if (opt.value === select.value) { + item.classList.add('is-selected'); + item.setAttribute('aria-selected', 'true'); + } else { + item.setAttribute('aria-selected', 'false'); + } + const check = document.createElement('span'); + check.className = 'conversation-project-filter-check'; + check.setAttribute('aria-hidden', 'true'); + check.textContent = '✓'; + const label = document.createElement('span'); + label.className = 'conversation-project-filter-option-label'; + label.textContent = labelText; + label.title = labelText; + item.appendChild(check); + item.appendChild(label); + dropdown.appendChild(item); + }); + const selectedOpt = select.options[select.selectedIndex]; + const selectedText = selectedOpt ? (selectedOpt.textContent || '') : ''; + if (valueSpan) { + valueSpan.textContent = selectedText; + valueSpan.title = selectedText; + } +} + +function initProjectFilterCustomSelect(selectId) { + const select = document.getElementById(selectId); + if (!select) return; + if (select.dataset.projectCustomSelect === '1') { + syncProjectFilterCustomSelect(selectId); + return; + } + select.dataset.projectCustomSelect = '1'; + select.classList.add('conversation-project-filter-native'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + const wrapper = document.createElement('div'); + wrapper.className = 'conversation-project-filter-ui'; + + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'conversation-project-filter-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + const valueSpan = document.createElement('span'); + valueSpan.className = 'conversation-project-filter-value'; + trigger.appendChild(valueSpan); + trigger.insertAdjacentHTML('beforeend', CONVERSATION_PROJECT_FILTER_CARET); + + const dropdown = document.createElement('div'); + dropdown.className = 'conversation-project-filter-dropdown'; + dropdown.setAttribute('role', 'listbox'); + + const parent = select.parentNode; + parent.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(dropdown); + wrapper.appendChild(select); + + projectFilterCustomSelectRegistry[selectId] = { wrapper, trigger, dropdown, select }; + + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + const open = wrapper.classList.contains('open'); + closeAllProjectFilterCustomSelects(); + if (!open) { + wrapper.classList.add('open'); + trigger.setAttribute('aria-expanded', 'true'); + } + }); + + dropdown.addEventListener('click', (e) => { + const opt = e.target.closest('.conversation-project-filter-option'); + if (!opt) return; + e.stopPropagation(); + const val = opt.getAttribute('data-value'); + if (val === null) return; + if (select.value !== val) { + select.value = val; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + closeProjectFilterCustomSelect(selectId); + syncProjectFilterCustomSelect(selectId); + }); + + if (!projectFilterCustomSelectDocBound) { + projectFilterCustomSelectDocBound = true; + document.addEventListener('click', closeAllProjectFilterCustomSelects); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAllProjectFilterCustomSelects(); + }); + } + syncProjectFilterCustomSelect(selectId); +} + +function syncConversationProjectCustomSelect() { + syncProjectFilterCustomSelect(CONVERSATION_PROJECT_FILTER_SELECT_ID); +} + +function initConversationProjectCustomSelect() { + initProjectFilterCustomSelect(CONVERSATION_PROJECT_FILTER_SELECT_ID); +} + +function getConversationProjectFilter() { + try { + return localStorage.getItem(CONVERSATIONS_PROJECT_FILTER_KEY) || ''; + } catch (e) { + return ''; + } +} + +function setConversationProjectFilter(projectId) { + const value = (projectId || '').trim(); + try { + if (value) localStorage.setItem(CONVERSATIONS_PROJECT_FILTER_KEY, value); + else localStorage.removeItem(CONVERSATIONS_PROJECT_FILTER_KEY); + } catch (e) { /* ignore */ } + const sel = document.getElementById('conversation-project-filter'); + if (sel && sel.value !== value) sel.value = value; + syncConversationProjectCustomSelect(); + updateConversationSidebarFilterUI(); +} + +function isValidConversationProjectFilter(projectId) { + if (!projectId) return true; + if (projectId === CONVERSATION_PROJECT_FILTER_NONE) return true; + const map = window.projectNameById; + if (!map || typeof map !== 'object') return true; + return Object.prototype.hasOwnProperty.call(map, projectId); +} + +async function refreshConversationProjectFilter() { + const sel = document.getElementById('conversation-project-filter'); + if (!sel) return; + const saved = getConversationProjectFilter(); + let projects = []; + if (typeof window.ensureProjectsLoaded === 'function') { + try { + const list = await window.ensureProjectsLoaded(); + projects = (list || []).filter((p) => p && p.id && p.status !== 'archived'); + } catch (e) { /* ignore */ } + } + if (!projects.length) { + try { + const res = await apiFetch('/api/projects?status=active&limit=200'); + if (res.ok) { + const data = await res.json(); + const items = data.projects || data.items || (Array.isArray(data) ? data : []); + projects = items.filter((p) => p && p.id); + if (typeof window.rebuildProjectNameMap === 'function') { + window.rebuildProjectNameMap(items); + } + } + } catch (e) { /* ignore */ } + } + const tFn = typeof window.t === 'function' ? window.t.bind(window) : null; + const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目'; + const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目'; + sel.innerHTML = ''; + const allOpt = document.createElement('option'); + allOpt.value = ''; + allOpt.textContent = allLabel; + allOpt.setAttribute('data-i18n', 'chat.filterAllProjects'); + sel.appendChild(allOpt); + const unboundOpt = document.createElement('option'); + unboundOpt.value = CONVERSATION_PROJECT_FILTER_NONE; + unboundOpt.textContent = unboundLabel; + unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects'); + sel.appendChild(unboundOpt); + projects + .slice() + .sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || '', undefined, { sensitivity: 'base' })) + .forEach((p) => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = p.name || p.id; + sel.appendChild(opt); + }); + const normalized = isValidConversationProjectFilter(saved) ? saved : ''; + if (normalized !== saved) setConversationProjectFilter(normalized); + sel.value = normalized; + syncConversationProjectCustomSelect(); + updateConversationSidebarFilterUI(); +} + +function onConversationProjectFilterChange(projectId) { + setConversationProjectFilter(projectId || ''); + conversationsPagination.page = 1; + loadConversationsWithGroups(conversationsSearchQuery); +} + +function updateConversationSidebarFilterUI() { + const groupsSection = document.querySelector('.conversation-groups-section'); + const titleEl = document.querySelector('.recent-conversations-section .section-title'); + const filter = getConversationProjectFilter(); + const hasSearch = !!(conversationsSearchQuery && conversationsSearchQuery.trim()); + if (groupsSection) { + groupsSection.hidden = !!filter || hasSearch; + } + if (!titleEl) return; + const tFn = typeof window.t === 'function' ? window.t.bind(window) : null; + if (filter && filter !== CONVERSATION_PROJECT_FILTER_NONE) { + const name = (window.projectNameById && window.projectNameById[filter]) || filter; + const fullTitle = tFn ? tFn('chat.projectConversationsTitle', { name }) : `${name} · 对话`; + titleEl.textContent = fullTitle; + titleEl.title = fullTitle; + titleEl.classList.add('section-title--filtered'); + titleEl.removeAttribute('data-i18n'); + } else if (filter === CONVERSATION_PROJECT_FILTER_NONE) { + const fullTitle = tFn ? tFn('chat.unboundConversationsTitle') : '未绑定项目'; + titleEl.textContent = fullTitle; + titleEl.title = fullTitle; + titleEl.classList.add('section-title--filtered'); + titleEl.setAttribute('data-i18n', 'chat.unboundConversationsTitle'); + } else { + titleEl.classList.remove('section-title--filtered'); + titleEl.removeAttribute('title'); + titleEl.setAttribute('data-i18n', 'chat.recentConversations'); + if (tFn) titleEl.textContent = tFn('chat.recentConversations'); + } +} + +window.onConversationProjectBindingChanged = function onConversationProjectBindingChanged() { + loadConversationsWithGroups(conversationsSearchQuery); +}; function getConversationSortBy() { try { @@ -6252,6 +6517,13 @@ async function fetchAllConversations(searchQuery) { } function getConversationListEmptyHtml() { + const filter = getConversationProjectFilter(); + if (filter && filter !== CONVERSATION_PROJECT_FILTER_NONE) { + return '
'; + } + if (filter === CONVERSATION_PROJECT_FILTER_NONE) { + return '
'; + } return '
'; } @@ -6428,11 +6700,16 @@ async function loadConversationsWithGroups(searchQuery = '') { if (conversationSortBy === 'created_at') { convParams.set('sort_by', 'created_at'); } + const projectFilter = getConversationProjectFilter(); + if (projectFilter) { + convParams.set('project_id', projectFilter); + } if (searchQuery && searchQuery.trim()) { convParams.set('search', searchQuery.trim()); - } else { + } else if (!projectFilter) { convParams.set('exclude_grouped', 'true'); } + updateConversationSidebarFilterUI(); const url = `/api/conversations?${convParams}`; const [,, response] = await Promise.all([ loadGroups(), @@ -6488,6 +6765,7 @@ async function loadConversationsWithGroups(searchQuery = '') { const pinnedConvs = []; const normalConvs = []; const hasSearchQuery = searchQuery && searchQuery.trim(); + const hasProjectFilter = !!getConversationProjectFilter(); uniqueConversations.forEach(conv => { // 如果有搜索关键词,显示所有匹配的对话(全局搜索,包括分组中的) @@ -6501,6 +6779,16 @@ async function loadConversationsWithGroups(searchQuery = '') { return; } + // 按项目筛选时展示该项目下全部对话(含分组内) + if (hasProjectFilter) { + if (conv.pinned) { + pinnedConvs.push(conv); + } else { + normalConvs.push(conv); + } + return; + } + // 如果没有搜索关键词,使用原有逻辑 // "最近对话"列表应该只显示不在任何分组中的对话 // 无论是否在分组详情页,都不应该在"最近对话"中显示分组中的对话 @@ -7731,6 +8019,84 @@ function closeContextMenu() { // 显示批量管理模态框 let allConversationsForBatch = []; +function getConversationProjectId(conv) { + return (conv?.projectId || conv?.project_id || '').trim(); +} + +function getConversationProjectLabel(conv) { + const pid = getConversationProjectId(conv); + if (!pid) { + return typeof window.t === 'function' ? window.t('batchManageModal.noProject') : '无项目'; + } + return (window.projectNameById && window.projectNameById[pid]) || pid; +} + +async function refreshBatchProjectFilter() { + const sel = document.getElementById('batch-project-filter'); + if (!sel) return; + const saved = sel.value || ''; + if (typeof window.ensureProjectsLoaded === 'function') { + try { + await window.ensureProjectsLoaded(); + } catch (e) { /* ignore */ } + } + const tFn = typeof window.t === 'function' ? window.t.bind(window) : null; + const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目'; + const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目'; + sel.innerHTML = ''; + const allOpt = document.createElement('option'); + allOpt.value = ''; + allOpt.textContent = allLabel; + allOpt.setAttribute('data-i18n', 'chat.filterAllProjects'); + sel.appendChild(allOpt); + const unboundOpt = document.createElement('option'); + unboundOpt.value = CONVERSATION_PROJECT_FILTER_NONE; + unboundOpt.textContent = unboundLabel; + unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects'); + sel.appendChild(unboundOpt); + const source = window.projectNameById ? Object.keys(window.projectNameById) : []; + source + .sort((a, b) => { + const na = (window.projectNameById[a] || a).toLowerCase(); + const nb = (window.projectNameById[b] || b).toLowerCase(); + return na.localeCompare(nb); + }) + .forEach((id) => { + const opt = document.createElement('option'); + opt.value = id; + opt.textContent = window.projectNameById[id] || id; + sel.appendChild(opt); + }); + const valid = !saved || saved === CONVERSATION_PROJECT_FILTER_NONE || (window.projectNameById && window.projectNameById[saved]); + sel.value = valid ? saved : ''; + syncProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); +} + +function getBatchFilteredConversations() { + const query = (document.getElementById('batch-search-input')?.value || '').trim().toLowerCase(); + const projectFilter = (document.getElementById('batch-project-filter')?.value || '').trim(); + return allConversationsForBatch.filter((conv) => { + const pid = getConversationProjectId(conv); + if (projectFilter) { + if (projectFilter === CONVERSATION_PROJECT_FILTER_NONE) { + if (pid) return false; + } else if (pid !== projectFilter) { + return false; + } + } + if (!query) return true; + const title = (conv.title || '').toLowerCase(); + const projectName = getConversationProjectLabel(conv).toLowerCase(); + return title.includes(query) || projectName.includes(query); + }); +} + +function applyBatchConversationFilters() { + const filtered = getBatchFilteredConversations(); + updateBatchManageTitle(filtered.length); + renderBatchConversations(filtered); +} + // 更新批量管理模态框标题(含条数),支持 i18n;count 为当前条数 function updateBatchManageTitle(count) { const titleEl = document.getElementById('batch-manage-title'); @@ -7742,19 +8108,27 @@ function updateBatchManageTitle(count) { async function showBatchManageModal() { try { + initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); allConversationsForBatch = await fetchAllConversations(''); - - const modal = document.getElementById('batch-manage-modal'); - updateBatchManageTitle(allConversationsForBatch.length); - - renderBatchConversations(); + await refreshBatchProjectFilter(); + const sidebarFilter = getConversationProjectFilter(); + const batchSel = document.getElementById('batch-project-filter'); + if (batchSel && sidebarFilter && ( + sidebarFilter === CONVERSATION_PROJECT_FILTER_NONE || + (window.projectNameById && window.projectNameById[sidebarFilter]) + )) { + batchSel.value = sidebarFilter; + } + const searchInput = document.getElementById('batch-search-input'); + if (searchInput) searchInput.value = ''; + applyBatchConversationFilters(); openAppModal('batch-manage-modal', { focus: false }); } catch (error) { console.error('加载对话列表失败:', error); - // 错误时使用空数组,不显示错误提示(更友好的用户体验) + initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); allConversationsForBatch = []; - updateBatchManageTitle(0); - renderBatchConversations(); + await refreshBatchProjectFilter(); + applyBatchConversationFilters(); openAppModal('batch-manage-modal', { focus: false }); } } @@ -7817,15 +8191,27 @@ function renderBatchConversations(filtered = null) { checkbox.dataset.conversationId = conv.id; checkbox.addEventListener('change', syncSelectAllBatchCheckbox); + const checkboxCol = document.createElement('div'); + checkboxCol.className = 'batch-table-col-checkbox'; + checkboxCol.appendChild(checkbox); + const name = document.createElement('div'); name.className = 'batch-table-col-name'; const originalTitle = conv.title || (typeof window.t === 'function' ? window.t('batchManageModal.unnamedConversation') : '未命名对话'); - // 使用安全截断函数,限制最大长度为45个字符(留出空间显示省略号) - const truncatedTitle = safeTruncateText(originalTitle, 45); + const truncatedTitle = safeTruncateText(originalTitle, 36); name.textContent = truncatedTitle; - // 设置title属性以显示完整文本(鼠标悬停时) name.title = originalTitle; + const project = document.createElement('div'); + project.className = 'batch-table-col-project'; + const projectLabel = getConversationProjectLabel(conv); + const truncatedProject = safeTruncateText(projectLabel, 28); + project.textContent = truncatedProject; + project.title = projectLabel; + if (!getConversationProjectId(conv)) { + project.classList.add('is-unbound'); + } + const time = document.createElement('div'); time.className = 'batch-table-col-time'; const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date(); @@ -7858,8 +8244,9 @@ function renderBatchConversations(filtered = null) { }; action.appendChild(deleteBtn); - row.appendChild(checkbox); + row.appendChild(checkboxCol); row.appendChild(name); + row.appendChild(project); row.appendChild(time); row.appendChild(action); @@ -7870,18 +8257,8 @@ function renderBatchConversations(filtered = null) { } // 筛选批量管理对话 -function filterBatchConversations(query) { - if (!query || !query.trim()) { - renderBatchConversations(); - return; - } - - const filtered = allConversationsForBatch.filter(conv => { - const title = (conv.title || '').toLowerCase(); - return title.includes(query.toLowerCase()); - }); - - renderBatchConversations(filtered); +function filterBatchConversations() { + applyBatchConversationFilters(); } // 全选/取消全选 @@ -7958,6 +8335,10 @@ function closeBatchManageModal() { selectAll.checked = false; selectAll.indeterminate = false; } + const searchInput = document.getElementById('batch-search-input'); + if (searchInput) searchInput.value = ''; + const batchProj = document.getElementById('batch-project-filter'); + if (batchProj) batchProj.value = ''; allConversationsForBatch = []; } @@ -8030,7 +8411,7 @@ document.addEventListener('languagechange', function () { refreshChatPanelI18n(); const modal = document.getElementById('batch-manage-modal'); if (isAppModalOpen('batch-manage-modal')) { - updateBatchManageTitle(allConversationsForBatch.length); + refreshBatchProjectFilter().then(() => applyBatchConversationFilters()); } // 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式 if (typeof loadConversationsWithGroups === 'function') { @@ -8962,6 +9343,8 @@ function clearGroupSearch() { // 初始化时加载分组 document.addEventListener('DOMContentLoaded', async () => { updateConversationSortMenuUI(); + initConversationProjectCustomSelect(); + await refreshConversationProjectFilter(); await loadGroups(); await loadConversationsWithGroups(); @@ -9018,8 +9401,16 @@ document.addEventListener('DOMContentLoaded', async () => { }); }); +async function refreshAllProjectFilterSelects() { + await refreshConversationProjectFilter(); + await refreshBatchProjectFilter(); +} + // 顶层 async function 不会自动挂到 window,hitl 等脚本依赖 window.loadConversation if (typeof window !== 'undefined') { window.loadConversation = loadConversation; window.startNewConversation = startNewConversation; + window.refreshConversationProjectFilter = refreshConversationProjectFilter; + window.refreshAllProjectFilterSelects = refreshAllProjectFilterSelects; + window.onConversationProjectFilterChange = onConversationProjectFilterChange; } diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index d52dd4fd..8c71c6ca 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3547,6 +3547,7 @@ const monitorState = { timelineLoading: false, lastFetchedAt: null, retentionDays: 0, + selectedExecutions: new Set(), pagination: { page: 1, pageSize: (() => { @@ -5063,10 +5064,12 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { const terminateBtn = status === 'running' ? `` : ''; + const jsExecId = rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const isSelected = monitorState.selectedExecutions.has(rawExecId); return ` - + ${toolName} ${escapeHtml(statusLabel)} @@ -5212,6 +5215,8 @@ async function deleteExecution(executionId) { throw new Error(error.error || deleteFailedMsg); } + monitorState.selectedExecutions.delete(executionId); + // 删除成功后刷新当前页面 const currentPage = monitorState.pagination.page; await refreshMonitorPanel(currentPage); @@ -5225,10 +5230,22 @@ async function deleteExecution(executionId) { } } +// 切换单条执行记录选中状态(持久化到 monitorState,避免轮询刷新后丢失) +function toggleExecutionSelection(executionId, selected) { + if (!executionId) { + return; + } + if (selected) { + monitorState.selectedExecutions.add(executionId); + } else { + monitorState.selectedExecutions.delete(executionId); + } + updateBatchActionsState(); +} + // 更新批量操作状态 function updateBatchActionsState() { - const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked'); - const selectedCount = checkboxes.length; + const selectedCount = monitorState.selectedExecutions.size; const batchActions = document.getElementById('monitor-batch-actions'); const selectedCountSpan = document.getElementById('monitor-selected-count'); @@ -5245,13 +5262,18 @@ function updateBatchActionsState() { selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : '已选择 ' + selectedCount + ' 项'; } - // 更新全选复选框状态 + // 更新全选复选框状态(仅反映当前页) const selectAllCheckbox = document.getElementById('monitor-select-all'); if (selectAllCheckbox) { const allCheckboxes = document.querySelectorAll('.monitor-execution-checkbox'); - const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked); - selectAllCheckbox.checked = allChecked; - selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < allCheckboxes.length; + if (allCheckboxes.length === 0) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } else { + const checkedOnPage = Array.from(allCheckboxes).filter(cb => monitorState.selectedExecutions.has(cb.value)).length; + selectAllCheckbox.checked = checkedOnPage === allCheckboxes.length; + selectAllCheckbox.indeterminate = checkedOnPage > 0 && checkedOnPage < allCheckboxes.length; + } } } @@ -5260,6 +5282,11 @@ function toggleSelectAll(checkbox) { const checkboxes = document.querySelectorAll('.monitor-execution-checkbox'); checkboxes.forEach(cb => { cb.checked = checkbox.checked; + if (checkbox.checked) { + monitorState.selectedExecutions.add(cb.value); + } else { + monitorState.selectedExecutions.delete(cb.value); + } }); updateBatchActionsState(); } @@ -5269,6 +5296,7 @@ function selectAllExecutions() { const checkboxes = document.querySelectorAll('.monitor-execution-checkbox'); checkboxes.forEach(cb => { cb.checked = true; + monitorState.selectedExecutions.add(cb.value); }); const selectAllCheckbox = document.getElementById('monitor-select-all'); if (selectAllCheckbox) { @@ -5284,6 +5312,7 @@ function deselectAllExecutions() { checkboxes.forEach(cb => { cb.checked = false; }); + monitorState.selectedExecutions.clear(); const selectAllCheckbox = document.getElementById('monitor-select-all'); if (selectAllCheckbox) { selectAllCheckbox.checked = false; @@ -5294,14 +5323,12 @@ function deselectAllExecutions() { // 批量删除执行记录 async function batchDeleteExecutions() { - const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked'); - if (checkboxes.length === 0) { + const ids = Array.from(monitorState.selectedExecutions); + if (ids.length === 0) { const selectFirstMsg = typeof window.t === 'function' ? window.t('mcpMonitor.selectExecFirst') : '请先选择要删除的执行记录'; alert(selectFirstMsg); return; } - - const ids = Array.from(checkboxes).map(cb => cb.value); const count = ids.length; const batchConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteConfirm', { count: count }) : `确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`; if (!confirm(batchConfirmMsg)) { @@ -5325,6 +5352,10 @@ async function batchDeleteExecutions() { const result = await response.json().catch(() => ({})); const deletedCount = result.deleted || count; + + ids.forEach(function (id) { + monitorState.selectedExecutions.delete(id); + }); // 删除成功后刷新当前页面 const currentPage = monitorState.pagination.page; diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 80b2bf05..6c9f7916 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -293,6 +293,9 @@ async function ensureProjectsLoaded(force) { projectsCacheAll = list; rebuildProjectNameMap(projectsCacheAll); _projectsListReady = true; + if (typeof window.refreshConversationProjectFilter === 'function') { + window.refreshConversationProjectFilter(); + } return projectsCacheAll; }) .catch((e) => { @@ -371,6 +374,9 @@ async function loadProjectsList() { if (typeof refreshVulnerabilityProjectFilter === 'function') { refreshVulnerabilityProjectFilter(); } + if (typeof window.refreshAllProjectFilterSelects === 'function') { + await window.refreshAllProjectFilterSelects(); + } } function projectInitial(name) { @@ -2198,6 +2204,9 @@ async function applyChatProjectSelection(projectId) { setActiveProjectId(projectId); } updateChatProjectButtonLabel(); + if (typeof window.onConversationProjectBindingChanged === 'function') { + window.onConversationProjectBindingChanged(projectId); + } } /** 对话页项目选择器:同步按钮文案;若浮层已打开则刷新列表 */ @@ -2326,3 +2335,4 @@ window.focusProjectFactGraphEdge = focusProjectFactGraphEdge; window.toggleProjectFactGraphConnectMode = toggleProjectFactGraphConnectMode; window.rebuildProjectNameMap = rebuildProjectNameMap; window.projectNameById = projectNameById; +window.ensureProjectsLoaded = ensureProjectsLoaded; diff --git a/web/templates/index.html b/web/templates/index.html index 90fdb4ae..0a8ba5c3 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -778,7 +778,7 @@