diff --git a/web/static/css/style.css b/web/static/css/style.css index 13df9f86..0cb32f9e 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -13841,8 +13841,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { /* 批量管理模态框 */ .batch-manage-modal-content { - max-width: 920px; - width: 90vw; + max-width: 1040px; + width: 92vw; display: flex; flex-direction: column; max-height: 85vh; @@ -13856,8 +13856,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { } .batch-manage-header-actions .conversation-project-filter-ui { - width: 148px; - min-width: 108px; + width: 132px; + min-width: 100px; flex-shrink: 0; } @@ -13911,7 +13911,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-table-header { display: grid; - grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px; + grid-template-columns: 40px minmax(0, 1.1fr) minmax(0, 0.75fr) minmax(0, 0.75fr) 160px 72px; gap: 12px; padding: 12px 16px; background: var(--bg-secondary); @@ -13930,7 +13930,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-conversation-row { display: grid; - grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px; + grid-template-columns: 40px minmax(0, 1.1fr) minmax(0, 0.75fr) minmax(0, 0.75fr) 160px 72px; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border-color); @@ -13972,6 +13972,20 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { opacity: 0.85; } +.batch-table-col-group { + font-size: 0.8125rem; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.batch-table-col-group.is-unbound { + color: var(--text-muted); + font-style: italic; + opacity: 0.85; +} + .batch-table-col-time { font-size: 0.875rem; color: var(--text-muted); @@ -14021,12 +14035,52 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .batch-manage-footer { display: flex; align-items: center; - justify-content: flex-end; + justify-content: space-between; + gap: 16px; padding: 16px 24px; border-top: 1px solid var(--border-color); flex-shrink: 0; } +.batch-footer-move { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.batch-footer-move .conversation-project-filter-ui { + width: 180px; + min-width: 140px; + flex-shrink: 0; +} + +.batch-footer-move .conversation-project-filter-trigger { + font-size: 0.8125rem; + padding: 8px 10px; +} + +.batch-manage-modal-content .batch-footer-move .conversation-project-filter-ui.open { + z-index: 400; +} + +.batch-footer-move .conversation-project-filter-dropdown { + top: auto; + bottom: calc(100% + 4px); +} + +.batch-move-group-select { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .batch-table-col-checkbox input[type="checkbox"] { cursor: pointer; } @@ -32038,6 +32092,22 @@ html[data-theme="dark"] .batch-search-box input::placeholder { color: var(--text-muted) !important; } +html[data-theme="dark"] .batch-footer-move .conversation-project-filter-trigger, +html[data-theme="dark"] .batch-footer-move .conversation-project-filter-dropdown { + background: #0f172a !important; + color: var(--text-primary) !important; + border-color: #263244 !important; +} + +html[data-theme="dark"] .batch-footer-move .conversation-project-filter-option:hover { + background: #1e293b !important; +} + +html[data-theme="dark"] .batch-footer-move .conversation-project-filter-option.is-selected { + background: rgba(59, 130, 246, 0.15) !important; + color: #93c5fd !important; +} + html[data-theme="dark"] .batch-manage-modal-content, html[data-theme="dark"] .batch-manage-modal-content .modal-header, html[data-theme="dark"] .batch-manage-body, diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 1eb71442..b7d34ac9 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2758,15 +2758,29 @@ "searchPlaceholder": "Search history", "conversationName": "Conversation name", "project": "Project", + "group": "Group", "noProject": "No project", "unknownProject": "Unknown project", + "noGroup": "Ungrouped", + "unknownGroup": "Unknown group", "filterByProject": "Filter by project", + "filterByGroup": "Filter by group", + "filterAllGroups": "All groups", + "filterUngrouped": "Ungrouped", "lastTime": "Last activity", "action": "Action", "selectAll": "Select all", + "setGroup": "Set group", + "noGroupOption": "Ungrouped", "deleteSelected": "Delete selected", "confirmDeleteNone": "Please select at least one conversation to delete", "confirmDeleteN": "Delete {{count}} selected conversation(s)? Chat messages cannot be recovered, but recorded vulnerabilities will remain in the vulnerability library.", + "confirmGroupChangeNone": "Please select at least one conversation", + "confirmMoveN": "Move {{count}} selected conversation(s) to \"{{group}}\"?", + "confirmRemoveNoGroup": "None of the selected conversations belong to a group", + "confirmRemoveN": "Remove {{count}} selected conversation(s) from their group(s)?", + "removeFailed": "Remove failed", + "moveFailed": "Move failed", "deleteFailed": "Delete failed", "unnamedConversation": "Unnamed conversation" }, diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index f2aaf649..346a32c8 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2746,15 +2746,29 @@ "searchPlaceholder": "搜索历史记录", "conversationName": "对话名称", "project": "项目", + "group": "对话分组", "noProject": "无项目", "unknownProject": "未知项目", + "noGroup": "无分组", + "unknownGroup": "未知分组", "filterByProject": "按项目筛选", + "filterByGroup": "按分组筛选", + "filterAllGroups": "全部分组", + "filterUngrouped": "无分组", "lastTime": "最近一次对话时间", "action": "操作", "selectAll": "全选", + "setGroup": "设置分组", + "noGroupOption": "无分组", "deleteSelected": "删除所选", "confirmDeleteNone": "请先选择要删除的对话", "confirmDeleteN": "确定要删除选中的 {{count}} 条对话吗?对话消息将不可恢复,但已记录的漏洞会保留在漏洞库中。", + "confirmGroupChangeNone": "请先选择要操作的对话", + "confirmMoveN": "确定将选中的 {{count}} 条对话移动到「{{group}}」吗?", + "confirmRemoveNoGroup": "所选对话均未归属分组", + "confirmRemoveN": "确定将选中的 {{count}} 条对话移出分组吗?", + "removeFailed": "移出失败", + "moveFailed": "移动失败", "deleteFailed": "删除失败", "unnamedConversation": "未命名对话" }, diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 82fbc951..556036f0 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2441,6 +2441,20 @@ function compactWorkflowProcessDetails(details) { }); } +function isProcessDetailsUserExpanded(messageId) { + const container = document.getElementById('process-details-' + messageId); + return !!(container && container.dataset && container.dataset.userExpanded === '1'); +} + +function syncProcessDetailButtonLabels(messageId, expanded) { + const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; + const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; + const label = expanded ? collapseT : expandT; + document.querySelectorAll('#' + messageId + ' .process-detail-btn').forEach((btn) => { + btn.innerHTML = '' + label + ''; + }); +} + // 渲染过程详情 // options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中) function renderProcessDetails(messageId, processDetails, options) { @@ -2537,7 +2551,9 @@ function renderProcessDetails(messageId, processDetails, options) { if (!processDetails || processDetails.length === 0) { if (!appendMode) { timeline.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
'; - timeline.classList.remove('expanded'); + if (!isProcessDetailsUserExpanded(messageId)) { + timeline.classList.remove('expanded'); + } } return; } @@ -2732,12 +2748,13 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa const hasErrorOrCancelled = processDetails.some(d => d.eventType === 'error' || d.eventType === 'cancelled' ); - if (hasErrorOrCancelled && !hasPendingHitlInDetails && !hasPendingWorkflowHitl) { + const userExpanded = isProcessDetailsUserExpanded(messageElement.id); + if (userExpanded) { + timeline.classList.add('expanded'); + syncProcessDetailButtonLabels(messageElement.id, true); + } else if (hasErrorOrCancelled && !hasPendingHitlInDetails && !hasPendingWorkflowHitl) { timeline.classList.remove('expanded'); - const processDetailBtn = messageElement.querySelector('.process-detail-btn'); - if (processDetailBtn) { - processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; - } + syncProcessDetailButtonLabels(messageElement.id, false); } if (hasPendingWorkflowHitl && messageElement && messageElement.id) { const convId = typeof window.currentConversationId === 'string' ? window.currentConversationId : ''; @@ -2983,6 +3000,8 @@ function toggleMcpToolList(assistantMessageId) { window.toggleMcpToolList = toggleMcpToolList; window.syncMcpToolsToggleButton = syncMcpToolsToggleButton; +window.isProcessDetailsUserExpanded = isProcessDetailsUserExpanded; +window.syncProcessDetailButtonLabels = syncProcessDetailButtonLabels; window.ensureMcpCallSectionChrome = ensureMcpCallSectionChrome; /** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */ @@ -6279,6 +6298,9 @@ 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 BATCH_GROUP_FILTER_SELECT_ID = 'batch-move-group-select'; +const BATCH_GROUP_HEADER_FILTER_SELECT_ID = 'batch-group-filter'; +const BATCH_GROUP_NONE = '__none__'; const projectFilterCustomSelectRegistry = {}; let projectFilterCustomSelectDocBound = false; @@ -6467,6 +6489,117 @@ function syncProjectFilterCustomSelect(selectId) { } } +function ensureSimpleCustomSelectOptionsUi(reg) { + if (reg.optionsList) return; + reg.dropdown.innerHTML = ''; + const optionsList = document.createElement('div'); + optionsList.className = 'conversation-project-filter-options'; + reg.dropdown.appendChild(optionsList); + reg.optionsList = optionsList; +} + +function renderSimpleCustomSelectOptions(reg) { + ensureSimpleCustomSelectOptionsUi(reg); + const { select, optionsList } = reg; + optionsList.innerHTML = ''; + Array.prototype.forEach.call(select.options, (opt) => { + optionsList.appendChild(createProjectFilterOptionButton(opt.value, opt.textContent || '', select.value)); + }); +} + +function syncSimpleCustomSelect(selectId) { + const reg = projectFilterCustomSelectRegistry[selectId]; + if (!reg) return; + const { select, trigger } = reg; + const valueSpan = trigger.querySelector('.conversation-project-filter-value'); + const selectedOpt = select.options[select.selectedIndex]; + const selectedText = selectedOpt ? (selectedOpt.textContent || '') : ''; + if (valueSpan) { + valueSpan.textContent = selectedText; + valueSpan.title = selectedText; + } +} + +function initSimpleCustomSelect(selectId) { + const select = document.getElementById(selectId); + if (!select) return; + if (select.dataset.projectCustomSelect === '1') { + syncSimpleCustomSelect(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'); + renderSimpleCustomSelectOptions(projectFilterCustomSelectRegistry[selectId]); + } + }); + + 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); + syncSimpleCustomSelect(selectId); + }); + + if (!projectFilterCustomSelectDocBound) { + projectFilterCustomSelectDocBound = true; + document.addEventListener('click', closeAllProjectFilterCustomSelects); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAllProjectFilterCustomSelects(); + }); + } + syncSimpleCustomSelect(selectId); +} + +function initBatchGroupCustomSelect() { + initSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID); +} + +function initBatchGroupHeaderFilterCustomSelect() { + initSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID); +} + function initProjectFilterCustomSelect(selectId) { const select = document.getElementById(selectId); if (!select) return; @@ -8507,6 +8640,22 @@ function getConversationProjectLabel(conv) { return typeof window.t === 'function' ? window.t('batchManageModal.unknownProject') : '未知项目'; } +function getConversationGroupId(conv) { + return (conv && conversationGroupMappingCache[conv.id]) || ''; +} + +function getConversationGroupLabel(conv) { + const groupId = getConversationGroupId(conv); + if (!groupId) { + return typeof window.t === 'function' ? window.t('batchManageModal.noGroup') : '无分组'; + } + const group = Array.isArray(groupsCache) ? groupsCache.find(g => g.id === groupId) : null; + if (group) { + return `${group.icon || '📁'} ${group.name}`; + } + return typeof window.t === 'function' ? window.t('batchManageModal.unknownGroup') : '未知分组'; +} + async function prefetchProjectNamesForConversations(conversations) { const missing = new Set(); for (const conv of conversations || []) { @@ -8536,9 +8685,83 @@ async function refreshBatchProjectFilter() { syncProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); } +async function refreshBatchGroupSelect() { + const sel = document.getElementById('batch-move-group-select'); + if (!sel) return; + + if (!Array.isArray(groupsCache) || groupsCache.length === 0) { + await loadGroups(); + } + + const saved = sel.value || BATCH_GROUP_NONE; + sel.innerHTML = ''; + + const noneOpt = document.createElement('option'); + noneOpt.value = BATCH_GROUP_NONE; + noneOpt.textContent = projectFilterT('batchManageModal.noGroupOption', '无分组'); + sel.appendChild(noneOpt); + + (groupsCache || []).forEach((group) => { + const opt = document.createElement('option'); + opt.value = group.id; + opt.textContent = `${group.icon || '📁'} ${group.name}`; + sel.appendChild(opt); + }); + + if (saved === BATCH_GROUP_NONE || (groupsCache || []).some((group) => group.id === saved)) { + sel.value = saved; + } else { + sel.value = BATCH_GROUP_NONE; + } + syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID); +} + +function appendBatchGroupHeaderFilterNativeOptions(sel) { + const allLabel = projectFilterT('batchManageModal.filterAllGroups', '全部分组'); + const ungroupedLabel = projectFilterT('batchManageModal.filterUngrouped', '无分组'); + sel.innerHTML = ''; + const allOpt = document.createElement('option'); + allOpt.value = ''; + allOpt.textContent = allLabel; + allOpt.setAttribute('data-i18n', 'batchManageModal.filterAllGroups'); + sel.appendChild(allOpt); + const ungroupedOpt = document.createElement('option'); + ungroupedOpt.value = BATCH_GROUP_NONE; + ungroupedOpt.textContent = ungroupedLabel; + ungroupedOpt.setAttribute('data-i18n', 'batchManageModal.filterUngrouped'); + sel.appendChild(ungroupedOpt); +} + +async function refreshBatchGroupHeaderFilter() { + const sel = document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID); + if (!sel) return; + + if (!Array.isArray(groupsCache) || groupsCache.length === 0) { + await loadGroups(); + } + + const saved = sel.value || ''; + appendBatchGroupHeaderFilterNativeOptions(sel); + + (groupsCache || []).forEach((group) => { + const opt = document.createElement('option'); + opt.value = group.id; + opt.textContent = `${group.icon || '📁'} ${group.name}`; + sel.appendChild(opt); + }); + + if (saved === '' || saved === BATCH_GROUP_NONE || (groupsCache || []).some((group) => group.id === saved)) { + sel.value = saved; + } else { + sel.value = ''; + } + syncSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID); +} + function getBatchFilteredConversations() { const query = (document.getElementById('batch-search-input')?.value || '').trim().toLowerCase(); const projectFilter = (document.getElementById('batch-project-filter')?.value || '').trim(); + const groupFilter = (document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID)?.value || '').trim(); return allConversationsForBatch.filter((conv) => { const pid = getConversationProjectId(conv); if (projectFilter) { @@ -8548,10 +8771,19 @@ function getBatchFilteredConversations() { return false; } } + const gid = getConversationGroupId(conv); + if (groupFilter) { + if (groupFilter === BATCH_GROUP_NONE) { + if (gid) return false; + } else if (gid !== groupFilter) { + return false; + } + } if (!query) return true; const title = (conv.title || '').toLowerCase(); const projectName = getConversationProjectLabel(conv).toLowerCase(); - return title.includes(query) || projectName.includes(query); + const groupName = getConversationGroupLabel(conv).toLowerCase(); + return title.includes(query) || projectName.includes(query) || groupName.includes(query); }); } @@ -8574,8 +8806,16 @@ async function showBatchManageModal() { try { initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); allConversationsForBatch = await fetchAllConversations(''); - await prefetchProjectNamesForConversations(allConversationsForBatch); + await Promise.all([ + prefetchProjectNamesForConversations(allConversationsForBatch), + loadGroups(), + loadConversationGroupMapping(), + ]); await refreshBatchProjectFilter(); + initBatchGroupHeaderFilterCustomSelect(); + await refreshBatchGroupHeaderFilter(); + initBatchGroupCustomSelect(); + await refreshBatchGroupSelect(); const sidebarFilter = getConversationProjectFilter(); const batchSel = document.getElementById('batch-project-filter'); if (batchSel && sidebarFilter && ( @@ -8591,8 +8831,10 @@ async function showBatchManageModal() { } catch (error) { console.error('加载对话列表失败:', error); initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); + initBatchGroupHeaderFilterCustomSelect(); + initBatchGroupCustomSelect(); allConversationsForBatch = []; - await refreshBatchProjectFilter(); + await Promise.all([refreshBatchProjectFilter(), refreshBatchGroupHeaderFilter(), refreshBatchGroupSelect()]); applyBatchConversationFilters(); openAppModal('batch-manage-modal', { focus: false }); } @@ -8677,6 +8919,16 @@ function renderBatchConversations(filtered = null) { project.classList.add('is-unbound'); } + const group = document.createElement('div'); + group.className = 'batch-table-col-group'; + const groupLabel = getConversationGroupLabel(conv); + const truncatedGroup = safeTruncateText(groupLabel, 24); + group.textContent = truncatedGroup; + group.title = groupLabel; + if (!getConversationGroupId(conv)) { + group.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(); @@ -8712,6 +8964,7 @@ function renderBatchConversations(filtered = null) { row.appendChild(checkboxCol); row.appendChild(name); row.appendChild(project); + row.appendChild(group); row.appendChild(time); row.appendChild(action); @@ -8759,6 +9012,133 @@ function syncSelectAllBatchCheckbox() { } } +// 批量设置对话分组(选「无分组」即移出) +async function applyBatchGroupChange() { + const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked'); + if (checkboxes.length === 0) { + alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmGroupChangeNone') : '请先选择要操作的对话'); + return; + } + + const groupSelect = document.getElementById('batch-move-group-select'); + const groupId = (groupSelect?.value || BATCH_GROUP_NONE).trim(); + + if (groupId === BATCH_GROUP_NONE) { + const items = Array.from(checkboxes).map((cb) => ({ + id: cb.dataset.conversationId, + groupId: conversationGroupMappingCache[cb.dataset.conversationId] || '', + })).filter((item) => item.groupId); + + if (items.length === 0) { + alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmRemoveNoGroup') : '所选对话均未归属分组'); + return; + } + + const confirmMsg = typeof window.t === 'function' + ? window.t('batchManageModal.confirmRemoveN', { count: items.length }) + : `确定将选中的 ${items.length} 条对话移出分组吗?`; + if (!confirm(confirmMsg)) { + return; + } + + try { + for (const item of items) { + await apiFetch(`/api/groups/${item.groupId}/conversations/${item.id}`, { + method: 'DELETE', + }); + + delete conversationGroupMappingCache[item.id]; + delete pendingGroupMappings[item.id]; + + if (currentConversationId === item.id) { + currentConversationGroupId = null; + } + } + + await finishBatchGroupChangeAfterRemove(); + } catch (error) { + console.error('批量移出分组失败:', error); + await loadConversationGroupMapping(); + applyBatchConversationFilters(); + const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.removeFailed') : '移出失败'; + const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误'; + alert(failedMsg + ': ' + (error.message || unknownErr)); + } + return; + } + + const targetGroup = (groupsCache || []).find((group) => group.id === groupId); + const groupName = targetGroup ? targetGroup.name : groupId; + const confirmMsg = typeof window.t === 'function' + ? window.t('batchManageModal.confirmMoveN', { count: checkboxes.length, group: groupName }) + : `确定将选中的 ${checkboxes.length} 条对话移动到「${groupName}」吗?`; + if (!confirm(confirmMsg)) { + return; + } + + const ids = Array.from(checkboxes).map((cb) => cb.dataset.conversationId); + + try { + for (const id of ids) { + await apiFetch('/api/groups/conversations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + conversationId: id, + groupId, + }), + }); + + conversationGroupMappingCache[id] = groupId; + pendingGroupMappings[id] = groupId; + + if (currentConversationId === id) { + currentConversationGroupId = groupId; + } + } + + if (currentGroupId) { + await loadGroupConversations(currentGroupId); + } + await loadConversationsWithGroups(); + await loadGroups(); + resetBatchSelectionAfterGroupChange(); + } catch (error) { + console.error('批量移动对话失败:', error); + await loadConversationGroupMapping(); + applyBatchConversationFilters(); + const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.moveFailed') : '移动失败'; + const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误'; + alert(failedMsg + ': ' + (error.message || unknownErr)); + } +} + +function resetBatchSelectionAfterGroupChange() { + applyBatchConversationFilters(); + const selectAll = document.getElementById('batch-select-all'); + if (selectAll) { + selectAll.checked = false; + selectAll.indeterminate = false; + } +} + +async function finishBatchGroupChangeAfterRemove() { + if (currentGroupId) { + await loadGroupConversations(currentGroupId); + } + await loadConversationGroupMapping(); + await loadGroups(); + + const savedGroupId = currentGroupId; + currentGroupId = null; + await loadConversationsWithGroups(); + currentGroupId = savedGroupId; + + resetBatchSelectionAfterGroupChange(); +} + // 删除选中的对话 async function deleteSelectedConversations() { const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked'); @@ -8794,6 +9174,7 @@ async function deleteSelectedConversations() { // 关闭批量管理模态框 function closeBatchManageModal() { + closeAllProjectFilterCustomSelects(); closeAppModal('batch-manage-modal'); const selectAll = document.getElementById('batch-select-all'); if (selectAll) { @@ -8804,6 +9185,12 @@ function closeBatchManageModal() { if (searchInput) searchInput.value = ''; const batchProj = document.getElementById('batch-project-filter'); if (batchProj) batchProj.value = ''; + const batchGroupFilter = document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID); + if (batchGroupFilter) batchGroupFilter.value = ''; + syncSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID); + const batchGroup = document.getElementById('batch-move-group-select'); + if (batchGroup) batchGroup.value = BATCH_GROUP_NONE; + syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID); allConversationsForBatch = []; } @@ -8885,6 +9272,12 @@ document.addEventListener('languagechange', function () { } }); } + if (typeof refreshBatchGroupSelect === 'function') { + refreshBatchGroupSelect().then(() => syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID)); + } + if (typeof refreshBatchGroupHeaderFilter === 'function') { + refreshBatchGroupHeaderFilter(); + } // 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式 if (typeof loadConversationsWithGroups === 'function') { loadConversationsWithGroups(); diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index b9ebdba6..8643c815 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1229,10 +1229,9 @@ function collapseAllProgressDetails(assistantMessageId, progressId) { if (assistantMessageId) { const detailsId = 'process-details-' + assistantMessageId; const detailsContainer = document.getElementById(detailsId); - if (detailsContainer) { + if (detailsContainer && detailsContainer.dataset.userExpanded !== '1') { const timeline = detailsContainer.querySelector('.progress-timeline'); if (timeline) { - // 确保移除expanded类(无论是否包含) timeline.classList.remove('expanded'); document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`).forEach((btn) => { btn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; @@ -1421,6 +1420,9 @@ function toggleProcessDetails(progressId, assistantMessageId) { const detailsContainer = document.getElementById(detailsId); if (!detailsContainer) return; + const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; + const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; + // 懒加载:首次展开时才从后端拉取该条消息的过程详情 const maybeLazy = detailsContainer.dataset && detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1'; if (maybeLazy) { @@ -1447,6 +1449,19 @@ function toggleProcessDetails(progressId, assistantMessageId) { }) .finally(() => { detailsContainer.dataset.loading = '0'; + if (detailsContainer.dataset.userExpanded === '1') { + const tl = detailsContainer.querySelector('.progress-timeline'); + if (tl) { + tl.classList.add('expanded'); + } + if (typeof syncProcessDetailButtonLabels === 'function') { + syncProcessDetailButtonLabels(assistantMessageId, true); + } else { + document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => { + btn.innerHTML = '' + collapseT + ''; + }); + } + } }); } } @@ -1456,27 +1471,25 @@ function toggleProcessDetails(progressId, assistantMessageId) { const timeline = detailsContainer.querySelector('.progress-timeline'); const detailBtns = document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`); - const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; - const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; const setDetailBtnLabels = (label) => { detailBtns.forEach((btn) => { btn.innerHTML = '' + label + ''; }); }; + const setExpanded = (expanded) => { + if (!timeline) return; + if (expanded) { + timeline.classList.add('expanded'); + detailsContainer.dataset.userExpanded = '1'; + setDetailBtnLabels(collapseT); + } else { + timeline.classList.remove('expanded'); + delete detailsContainer.dataset.userExpanded; + setDetailBtnLabels(expandT); + } + }; if (content && timeline) { - if (timeline.classList.contains('expanded')) { - timeline.classList.remove('expanded'); - setDetailBtnLabels(expandT); - } else { - timeline.classList.add('expanded'); - setDetailBtnLabels(collapseT); - } + setExpanded(!timeline.classList.contains('expanded')); } else if (timeline) { - if (timeline.classList.contains('expanded')) { - timeline.classList.remove('expanded'); - setDetailBtnLabels(expandT); - } else { - timeline.classList.add('expanded'); - setDetailBtnLabels(collapseT); - } + setExpanded(!timeline.classList.contains('expanded')); } // 滚动到展开的详情位置(流式且用户上滑阅读时不抢主列表滚动) @@ -3101,10 +3114,15 @@ function expandProcessDetailsTimeline(assistantMessageId) { const timeline = detailsContainer.querySelector('.progress-timeline'); if (!timeline) return; timeline.classList.add('expanded'); - const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; - document.querySelectorAll('#' + hitlEscapeAttrSelector(assistantMessageId) + ' .process-detail-btn').forEach(function (btn) { - btn.innerHTML = '' + collapseT + ''; - }); + detailsContainer.dataset.userExpanded = '1'; + if (typeof syncProcessDetailButtonLabels === 'function') { + syncProcessDetailButtonLabels(assistantMessageId, true); + } else { + const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; + document.querySelectorAll('#' + hitlEscapeAttrSelector(assistantMessageId) + ' .process-detail-btn').forEach(function (btn) { + btn.innerHTML = '' + collapseT + ''; + }); + } setTimeout(function () { if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.scrollIntoViewIfFollowing === 'function') { window.CyberStrikeChatScroll.scrollIntoViewIfFollowing(detailsContainer, { behavior: 'smooth', block: 'nearest' }); diff --git a/web/templates/index.html b/web/templates/index.html index 372da749..27e74d28 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -4533,6 +4533,10 @@ +
对话名称
项目
+
对话分组
最近一次对话时间
操作
@@ -4558,6 +4563,12 @@