/** * 历史记录管理器 * 负责管理历史记录的显示和操作 */ import { app } from "../../../../scripts/app.js"; import { logger } from '../utils/logger.js'; import { HistoryCacheService } from "../services/cache.js"; import { PopupManager } from "../utils/popupManager.js"; import { EventManager } from "../utils/eventManager.js"; import { UIToolkit } from "../utils/UIToolkit.js"; import { ResourceManager } from "../utils/resourceManager.js"; /** * 历史记录管理器类 * 管理历史记录弹窗和历史记录选择 */ class HistoryManager { static popupInstance = null; static onCloseCallback = null; static currentNodeId = null; // 当前节点ID static currentInputId = null; // 当前输入框ID static currentWidgetKey = null; // 当前widgetKey static eventCleanups = []; // 事件清理函数数组 static activeTooltip = null; // 当前活动的 tooltip /** * 显示历史记录弹窗 */ static async showHistoryPopup(params) { const { anchorButton, nodeId, inputId, onClose } = params; try { // 保存数据 this.currentNodeId = nodeId; this.currentInputId = inputId; this.currentWidgetKey = params.widgetKey || null; // logger.debug(`历史弹窗 | 触发显示 | 节点:${nodeId} | 输入框:${inputId}`); // 清理现有事件监听 this._cleanupEvents(); // 获取历史数据 const historyList = HistoryCacheService.getHistoryList({ nodeId: nodeId, limit: 100, // 增加限制以确保有足够的历史数据 workflowId: app.graph?._workflow_id }); // 创建新弹窗 const popup = this._createHistoryPopup({ historyList, nodeId }); // 使用PopupManager显示弹窗 await PopupManager.showPopup({ popup: popup, anchorButton: anchorButton, buttonInfo: params.buttonInfo, onClose: () => { // 清理事件监听 this._cleanupEvents(); // 执行传入的关闭回调 if (typeof onClose === 'function') { onClose(); } } }); // logger.debug(`历史弹窗 | 结果:显示成功 | 节点:${nodeId}`); } catch (error) { logger.error(`历史弹窗 | 结果:失败 | 错误:${error.message}`); this._cleanupAll(); } } /** * 隐藏历史记录弹窗 */ static hideHistoryPopup() { // 清理事件监听 this._cleanupEvents(); // 使用PopupManager关闭所有弹窗 PopupManager.closeAllPopups(); } /** * 清理所有事件监听 */ static _cleanupEvents() { // 执行并清空所有事件清理函数 if (this.eventCleanups.length > 0) { this.eventCleanups.forEach(cleanup => { if (typeof cleanup === 'function') { cleanup(); } }); this.eventCleanups = []; } } /** * 强制清理所有相关资源 */ static _cleanupAll() { // 清理事件监听 this._cleanupEvents(); // 使用PopupManager关闭所有弹窗 PopupManager.closeAllPopups(); } /** * 格式化历史内容,进行适当的截断和处理 */ static _formatHistoryContent(content, operationType) { // 不再添加操作类型前缀,直接返回内容 return content; } /** * 创建并显示tooltip */ static _showTooltip(target, text) { // 移除已存在的tooltip this._hideTooltip(); // 创建tooltip元素 const tooltip = document.createElement('div'); tooltip.className = 'tag_tooltip'; tooltip.textContent = text; document.body.appendChild(tooltip); // 获取目标元素的位置和尺寸 const rect = target.getBoundingClientRect(); // 计算tooltip位置 const tooltipRect = tooltip.getBoundingClientRect(); const left = rect.left + (rect.width - tooltipRect.width) / 2; const top = rect.top - tooltipRect.height - 8; // 8px的间距 // 设置tooltip位置 tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; // 保存当前tooltip引用 this.activeTooltip = tooltip; } /** * 隐藏tooltip */ static _hideTooltip() { if (this.activeTooltip) { this.activeTooltip.remove(); this.activeTooltip = null; } } /** * 创建历史记录弹窗 */ static _createHistoryPopup({ historyList, nodeId }) { const popup = document.createElement('div'); popup.className = 'popup_container'; // 创建标题栏 const titleBar = document.createElement('div'); titleBar.className = 'popup_title_bar'; const title = document.createElement('div'); title.className = 'popup_title'; title.style.display = 'flex'; title.style.alignItems = 'center'; // 添加图标 const iconContainer = ResourceManager.getIcon('icon-history.svg'); if (iconContainer) { iconContainer.style.width = '18px'; iconContainer.style.height = '18px'; iconContainer.style.color = 'var(--p-dialog-color)'; iconContainer.style.marginRight = '8px'; title.appendChild(iconContainer); } title.appendChild(document.createTextNode('历史记录')); const actions = document.createElement('div'); actions.className = 'popup_actions'; // 创建清除当前按钮 const clearCurrentBtn = document.createElement('button'); clearCurrentBtn.className = 'popup_action_btn'; clearCurrentBtn.textContent = '清除当前'; // 使用EventManager添加点击事件 const clearCurrentCleanup = EventManager.addDOMListener(clearCurrentBtn, 'click', async (e) => { e.preventDefault(); e.stopPropagation(); this.hideHistoryPopup(); try { // 获取当前输入框内容 const mapping = UIToolkit._findMapping(nodeId, this.currentInputId, this.currentWidgetKey); const inputEl = mapping?.inputEl; const currentContent = inputEl?.value || ''; // 清除当前节点的历史 await HistoryCacheService.clearNodeHistory(nodeId); // 如果输入框有内容,添加到历史记录 if (currentContent.trim()) { HistoryCacheService.addHistory({ workflow_id: '', node_id: nodeId, input_id: this.currentInputId, content: currentContent, operation_type: 'input', timestamp: Date.now() }); logger.debug(`历史记录 | 清除后保存当前内容 | 节点:${nodeId} | 输入框:${this.currentInputId}`); } // 更新按钮状态 if (mapping?.widget) { const widget = mapping.widget; UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); } logger.debug(`历史记录 | 清除当前节点历史 | 节点:${nodeId}`); } catch (error) { logger.error(`历史记录 | 清除失败 | 错误:${error.message}`); } }); this.eventCleanups.push(clearCurrentCleanup); // 创建清除所有按钮 const clearAllBtn = document.createElement('button'); clearAllBtn.className = 'popup_action_btn danger'; clearAllBtn.textContent = '清除所有'; // 使用EventManager添加点击事件 const clearAllCleanup = EventManager.addDOMListener(clearAllBtn, 'click', async (e) => { e.preventDefault(); e.stopPropagation(); this.hideHistoryPopup(); try { // 获取当前输入框内容 const mapping = UIToolkit._findMapping(nodeId, this.currentInputId, this.currentWidgetKey); const inputEl = mapping?.inputEl; const currentContent = inputEl?.value || ''; // 清除所有历史 await HistoryCacheService.clearAllHistory(); // 如果输入框有内容,添加到历史记录 if (currentContent.trim()) { HistoryCacheService.addHistory({ workflow_id: '', node_id: nodeId, input_id: this.currentInputId, content: currentContent, operation_type: 'input', timestamp: Date.now() }); logger.debug(`历史记录 | 清除后保存当前内容 | 节点:${nodeId} | 输入框:${this.currentInputId}`); } // 更新按钮状态 if (mapping?.widget) { const widget = mapping.widget; UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService); } logger.debug('历史记录 | 清除所有历史'); } catch (error) { logger.error(`历史记录 | 清除失败 | 错误:${error.message}`); } }); this.eventCleanups.push(clearAllCleanup); const closeBtn = document.createElement('button'); closeBtn.className = 'popup_btn'; UIToolkit.addIconToButton(closeBtn, 'pi-times', '关闭'); // 使用EventManager添加点击事件 const closeCleanup = EventManager.addDOMListener(closeBtn, 'click', (e) => { e.preventDefault(); e.stopPropagation(); this.hideHistoryPopup(); }); this.eventCleanups.push(closeCleanup); // 添加按钮到操作区域 actions.appendChild(clearCurrentBtn); actions.appendChild(clearAllBtn); actions.appendChild(closeBtn); titleBar.appendChild(title); titleBar.appendChild(actions); // 将历史记录按节点分组并排序 const { orderedNodeIds, nodeGroups } = this._groupAndSortHistoryByNode(historyList, nodeId); // 创建tabs容器 const tabsContainer = document.createElement('div'); tabsContainer.className = 'popup_tabs_container'; // 创建可滚动区域 const tabsScroll = document.createElement('div'); tabsScroll.className = 'popup_tabs_scroll'; // 创建tabs const tabs = document.createElement('div'); tabs.className = 'popup_tabs'; // 创建左右滚动指示器 const leftIndicator = document.createElement('div'); leftIndicator.className = 'tabs_scroll_indicator left'; // 添加图标 const leftIcon = ResourceManager.getIcon('icon-movedown.svg'); if (leftIcon) { leftIcon.classList.add('rotate_left', 'scroll_indicator_icon'); leftIndicator.appendChild(leftIcon); } leftIndicator.style.display = 'none'; // 初始隐藏 const rightIndicator = document.createElement('div'); rightIndicator.className = 'tabs_scroll_indicator right'; // 添加图标 const rightIcon = ResourceManager.getIcon('icon-movedown.svg'); if (rightIcon) { rightIcon.classList.add('rotate_right', 'scroll_indicator_icon'); rightIndicator.appendChild(rightIcon); } rightIndicator.style.display = 'none'; // 初始隐藏 // 添加指示器点击事件 - 改进滚动逻辑 const leftScrollCleanup = EventManager.addDOMListener(leftIndicator, 'click', () => { // 获取可视区域宽度 const visibleWidth = tabsScroll.clientWidth; // 计算滚动距离,PrimeVue风格是滚动一个较大的距离 const scrollDistance = visibleWidth * 0.75; // 平滑滚动 tabsScroll.scrollBy({ left: -scrollDistance, behavior: 'smooth' }); }); this.eventCleanups.push(leftScrollCleanup); const rightScrollCleanup = EventManager.addDOMListener(rightIndicator, 'click', () => { // 获取可视区域宽度 const visibleWidth = tabsScroll.clientWidth; // 计算滚动距离,PrimeVue风格是滚动一个较大的距离 const scrollDistance = visibleWidth * 0.75; // 平滑滚动 tabsScroll.scrollBy({ left: scrollDistance, behavior: 'smooth' }); }); this.eventCleanups.push(rightScrollCleanup); // 监听滚动事件,显示/隐藏滚动指示器 const scrollCleanup = EventManager.addDOMListener(tabsScroll, 'scroll', () => { // 检查是否需要滚动 const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth; if (!canScroll) { // 如果不需要滚动,隐藏两个指示器 leftIndicator.style.display = 'none'; rightIndicator.style.display = 'none'; return; } // 显示/隐藏左右滚动指示器 leftIndicator.style.display = tabsScroll.scrollLeft > 0 ? 'flex' : 'none'; rightIndicator.style.display = tabsScroll.scrollLeft < (tabsScroll.scrollWidth - tabsScroll.clientWidth - 2) ? 'flex' : 'none'; }); this.eventCleanups.push(scrollCleanup); // 初始检测是否需要滚动指示器 setTimeout(() => { // 检查是否需要滚动 const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth; if (canScroll) { // 如果需要滚动,显示右侧指示器 rightIndicator.style.display = 'flex'; } }, 100); // 创建内容区域 const tabContentsContainer = document.createElement('div'); tabContentsContainer.className = 'popup_content'; // 记录tab和内容元素的映射 const tabContentPairs = []; // 遍历已排序的节点ID数组,确保当前节点在最前面,其他节点按序号排序 orderedNodeIds.forEach((groupNodeId) => { const nodeItems = nodeGroups[groupNodeId]; // 如果没有数据,跳过 if (!nodeItems || nodeItems.length === 0) return; // 创建tab const tab = document.createElement('div'); tab.className = 'popup_tab'; // 设置tab标题 if (groupNodeId === nodeId) { tab.textContent = '当前节点'; tab.classList.add('current_node'); // 当前节点tab默认选中并激活 tab.classList.add('active'); } else { // 提取节点编号(假设格式为数字+可能的字母) const nodeNumMatch = String(groupNodeId).match(/\d+/); const nodeNum = nodeNumMatch ? nodeNumMatch[0] : groupNodeId; tab.textContent = `节点 ${nodeNum}`; } // 创建内容容器 const tabContent = document.createElement('div'); tabContent.className = 'popup_tab_content'; // 如果该tab默认选中,显示其内容 if (tab.classList.contains('active')) { tabContent.classList.add('active'); } // 渲染该节点的历史记录 if (nodeItems.length === 0) { const empty = document.createElement('div'); empty.className = 'popup_empty'; empty.textContent = '暂无历史记录'; tabContent.appendChild(empty); } else { nodeItems.forEach((item) => { const itemDiv = document.createElement('div'); itemDiv.className = 'popup_list_item'; // 添加元数据容器 const metaDiv = document.createElement('div'); metaDiv.className = 'history_meta'; // 添加输入框ID(始终显示) const inputIdSpan = document.createElement('span'); inputIdSpan.className = 'input_id'; inputIdSpan.textContent = `${item.input_id}`; metaDiv.appendChild(inputIdSpan); // 根据操作类型添加对应标签 if (item.operation_type) { const operationSpan = document.createElement('span'); switch (item.operation_type) { case 'translate': operationSpan.className = 'history_operation translated'; operationSpan.textContent = '翻译'; break; case 'expand': operationSpan.className = 'history_operation expanded'; operationSpan.textContent = '提示词优化'; break; case 'caption': operationSpan.className = 'history_operation caption'; operationSpan.textContent = '提示词反推'; break; // 其他操作类型可以在这里添加 } if (operationSpan.textContent) { metaDiv.appendChild(operationSpan); } } // 创建内容容器 const contentDiv = document.createElement('div'); contentDiv.className = 'history_content'; // 设置显示内容 - 使用格式化方法 const displayContent = this._formatHistoryContent(item.content, item.operation_type); contentDiv.textContent = displayContent; // 组装历史记录项 itemDiv.appendChild(metaDiv); itemDiv.appendChild(contentDiv); // 添加鼠标悬浮事件,显示完整内容的tooltip const mouseEnterCleanup = EventManager.addDOMListener(itemDiv, 'mouseenter', () => { this._showTooltip(itemDiv, displayContent); }); const mouseLeaveCleanup = EventManager.addDOMListener(itemDiv, 'mouseleave', () => { this._hideTooltip(); }); this.eventCleanups.push(mouseEnterCleanup, mouseLeaveCleanup); // 使用EventManager添加点击事件 const itemCleanup = EventManager.addDOMListener(itemDiv, 'click', (e) => { e.preventDefault(); e.stopPropagation(); // 主动关闭 tooltip this._hideTooltip(); // 使用UIToolkit写入内容到输入框 const success = UIToolkit.writeToInput(item.content, this.currentNodeId, this.currentInputId, { highlight: true, focus: true, widgetKey: this.currentWidgetKey }); if (success) { // 如果内容来自其他输入框,使用新方法添加历史并更新撤销状态 if (item.node_id !== this.currentNodeId || item.input_id !== this.currentInputId) { HistoryCacheService.addHistoryAndUpdateUndoState( this.currentNodeId, this.currentInputId, item.content, 'input' ); } // 隐藏历史弹窗 this.hideHistoryPopup(); } }); this.eventCleanups.push(itemCleanup); tabContent.appendChild(itemDiv); }); } // 添加到容器 tabs.appendChild(tab); tabContentsContainer.appendChild(tabContent); // 保存tab和内容的映射关系,用于点击tab时切换内容 tabContentPairs.push({ tab, content: tabContent }); // 添加tab点击事件 const tabCleanup = EventManager.addDOMListener(tab, 'click', () => { // 获取当前激活的标签 const currentActiveTab = tabs.querySelector('.popup_tab.active'); // 如果点击的是当前激活的标签,不做任何处理 if (currentActiveTab === tab) return; // 为当前激活的标签添加退出动画 if (currentActiveTab) { currentActiveTab.classList.add('exiting'); // 监听动画结束 const animationEndHandler = () => { currentActiveTab.classList.remove('active', 'exiting'); currentActiveTab.removeEventListener('transitionend', animationEndHandler); }; currentActiveTab.addEventListener('transitionend', animationEndHandler); } // 移除所有内容的active类 tabContentPairs.forEach(pair => { pair.content.classList.remove('active'); }); // 激活当前点击的tab tab.classList.add('active'); tabContent.classList.add('active'); // 滚动到可见区域 tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); }); this.eventCleanups.push(tabCleanup); }); // 组装tabs容器 tabsScroll.appendChild(tabs); tabsContainer.appendChild(tabsScroll); tabsContainer.appendChild(leftIndicator); tabsContainer.appendChild(rightIndicator); // 组装弹窗 popup.appendChild(titleBar); popup.appendChild(tabsContainer); popup.appendChild(tabContentsContainer); return popup; } /** * 按节点分组并排序历史记录 */ static _groupAndSortHistoryByNode(historyList, currentNodeId) { // 按节点ID分组 const groups = {}; // 收集所有节点ID,用于后续排序 const nodeIds = new Set(); // 分组历史记录 historyList.forEach(item => { const nodeId = item.node_id; if (!groups[nodeId]) { groups[nodeId] = []; nodeIds.add(nodeId); } groups[nodeId].push(item); }); // 确保当前节点ID存在于节点列表中 if (currentNodeId && !nodeIds.has(currentNodeId)) { nodeIds.add(currentNodeId); groups[currentNodeId] = []; } // 将节点ID转换为数组并排序 // 排序规则:1. 当前节点最前 2. 其他节点按数字序号排序 const sortedNodeIds = Array.from(nodeIds).sort((a, b) => { // 如果a是当前节点,排在最前面 if (a === currentNodeId) return -1; // 如果b是当前节点,排在最前面 if (b === currentNodeId) return 1; // 提取节点ID中的数字部分 const getNodeNumber = (id) => { const match = String(id).match(/\d+/); return match ? parseInt(match[0]) : 0; }; // 按数字大小排序 return getNodeNumber(a) - getNodeNumber(b); }); // 对每个组内的历史记录按时间戳排序(最新的在前) Object.keys(groups).forEach(nodeId => { groups[nodeId].sort((a, b) => b.timestamp - a.timestamp); }); // 返回排序后的节点ID数组和分组后的历史记录 return { orderedNodeIds: sortedNodeIds, nodeGroups: groups }; } } export { HistoryManager };