class PathVisualizer { constructor() { this.currentData = null; this.currentIndex = 0; this.allData = {}; this.urls = {}; // 改为对象,每个长度存储对应的URLs this.currentLength = 1; // 当前选择的路径长度 this.currentScale = 1; this.translateX = 0; this.translateY = 0; this.isDragging = false; this.currentModalBox = null; this.analysisData = {}; // 存储分析数据 this.initializeEventListeners(); this.initializeModal(); this.loadData(); } async loadData() { try { // 加载当前长度的数据 const response = await fetch(`path/processed_${this.currentLength}.json`); const data = await response.json(); this.allData = data; this.urls = Object.keys(this.allData); // 加载分析数据 try { const analysisResponse = await fetch(`path/processed_${this.currentLength}_with_analysis.json`); this.analysisData = await analysisResponse.json(); } catch (error) { console.error('加载分析数据失败:', error); this.analysisData = {}; } // 更新URL总数显示 this.updateUrlCount(); this.showPath(0); } catch (error) { console.error('加载数据失败:', error); } } updateUrlCount() { // 创建或更新URL计数显示 let urlCountElement = document.getElementById('urlCount'); if (!urlCountElement) { urlCountElement = document.createElement('div'); urlCountElement.id = 'urlCount'; urlCountElement.className = 'url-count'; // 获取长度选择器的父元素,并在其后插入计数元素 const lengthSelector = document.getElementById('pathLength'); lengthSelector.parentNode.appendChild(urlCountElement); } // 更新计数显示 urlCountElement.textContent = `页面数: ${this.urls.length}`; } initializeEventListeners() { document.getElementById('prevBtn').addEventListener('click', () => this.showPrevious()); document.getElementById('nextBtn').addEventListener('click', () => this.showNext()); document.getElementById('urlSearch').addEventListener('input', (e) => this.handleSearch(e)); // 添加随机URL按钮的事件监听 document.getElementById('randomUrl').addEventListener('click', () => this.showRandomUrl()); // 添加路径长度选择事件 document.getElementById('pathLength').addEventListener('change', (e) => { this.currentLength = parseInt(e.target.value); this.currentIndex = 0; // 重置索引 this.loadData(); // 重新加载数据 }); } initializeModal() { const modal = document.getElementById('imageModal'); const modalImg = document.getElementById('modalImage'); const zoomIn = document.getElementById('zoomIn'); const zoomOut = document.getElementById('zoomOut'); const zoomReset = document.getElementById('zoomReset'); const closeModalBtn = document.getElementById('closeModal'); // 关闭模态框 const closeModal = () => { modal.style.display = 'none'; this.currentScale = 1; this.translateX = 0; this.translateY = 0; this.updateModalImageScale(); }; closeModalBtn.onclick = closeModal; // 点击空白处关闭 modal.onclick = (event) => { if (event.target === modal) { closeModal(); } }; // 鼠标拖拽 modalImg.onmousedown = (e) => { this.isDragging = true; this.startX = e.clientX - this.translateX; this.startY = e.clientY - this.translateY; e.preventDefault(); }; document.onmousemove = (e) => { if (!this.isDragging) return; this.translateX = e.clientX - this.startX; this.translateY = e.clientY - this.startY; this.updateModalImageScale(); }; document.onmouseup = () => { this.isDragging = false; }; // 优化滚轮缩放,以鼠标位置为中心,降低灵敏度 modal.addEventListener('wheel', (e) => { e.preventDefault(); // 获取鼠标相对于图片的位置 const rect = modalImg.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // 计算鼠标在图片上的相对位置(考虑当前的变换) const x = (mouseX - this.translateX) / this.currentScale; const y = (mouseY - this.translateY) / this.currentScale; // 计算缩放比例,降低灵敏度 const delta = e.deltaY > 0 ? 0.95 : 1.05; // 从0.9/1.1改为0.95/1.05 const newScale = this.currentScale * delta; // 限制最大和最小缩放比例 const limitedScale = Math.min(Math.max(newScale, 0.1), 10); // 限制在0.1到10倍之间 // 只有当缩放比例在限制范围内才应用变换 if (limitedScale !== this.currentScale) { // 计算新的平移值,保持鼠标位置不变 this.translateX = mouseX - x * limitedScale; this.translateY = mouseY - y * limitedScale; this.currentScale = limitedScale; this.updateModalImageScale(); } }, { passive: false }); // 按钮缩放 zoomIn.onclick = () => { this.currentScale *= 1.2; this.updateModalImageScale(); }; zoomOut.onclick = () => { this.currentScale /= 1.2; this.updateModalImageScale(); }; zoomReset.onclick = () => { this.currentScale = 1; this.translateX = 0; this.translateY = 0; this.updateModalImageScale(); }; } handleSearch(event) { const searchTerm = event.target.value.toLowerCase(); const foundIndex = this.urls.findIndex(url => url.toLowerCase().includes(searchTerm)); if (foundIndex !== -1) { this.showPath(foundIndex); } } showRandomUrl() { if (this.urls && this.urls.length > 0) { // 生成随机索引 const randomIndex = Math.floor(Math.random() * this.urls.length); // 显示随机选中的路径 this.showPath(randomIndex); } } showPath(index) { if (index < 0 || index >= this.urls.length) return; this.currentIndex = index; const url = this.urls[index]; this.currentData = this.allData[url]; document.getElementById('currentUrl').textContent = `[长度: ${this.currentLength}] ${url}`; this.renderPaths(); } showPrevious() { this.showPath(this.currentIndex - 1); } showNext() { this.showPath(this.currentIndex + 1); } openModal(img) { const modal = document.getElementById('imageModal'); const modalImg = document.getElementById('modalImage'); modal.style.display = 'block'; modalImg.src = img.src; // 重置缩放和位置 this.currentScale = 1; this.translateX = 0; this.translateY = 0; this.updateModalImageScale(); } updateModalImageScale() { const modalImg = document.getElementById('modalImage'); const modalBox = document.getElementById('modalBoundingBox'); // 应用缩放和平移 modalImg.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.currentScale})`; modalImg.style.transformOrigin = '0 0'; // 设置变换原点为左上角 if (this.currentModalBox) { modalBox.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.currentScale})`; modalBox.style.transformOrigin = '0 0'; } } async renderPaths() { const container = document.getElementById('pathsContainer'); container.innerHTML = ''; // 获取当前URL的分析数据 const currentUrl = this.urls[this.currentIndex]; const analysisForUrl = this.analysisData[currentUrl] || {}; // 添加任务描述区域 - 即使没有数据也显示空白框架 const taskContainer = document.createElement('div'); taskContainer.className = 'task-extract-container'; // 添加任务标题 const titleLabel = document.createElement('div'); titleLabel.className = 'text-label'; titleLabel.textContent = '任务描述:'; taskContainer.appendChild(titleLabel); const titleText = document.createElement('div'); titleText.className = 'task-description'; // 如果有数据则显示,否则留空 if (analysisForUrl.shortestPathsMeta && analysisForUrl.shortestPathsMeta.length > 0) { titleText.textContent = analysisForUrl.shortestPathsMeta[0].title || ''; } taskContainer.appendChild(titleText); // 添加过程描述 const descLabel = document.createElement('div'); descLabel.className = 'text-label'; descLabel.textContent = '过程描述:'; taskContainer.appendChild(descLabel); const descText = document.createElement('div'); descText.className = 'process-description'; // 如果有数据则显示,否则留空 if (analysisForUrl.shortestPathsMeta && analysisForUrl.shortestPathsMeta.length > 0) { descText.textContent = analysisForUrl.shortestPathsMeta[0].description || ''; } taskContainer.appendChild(descText); container.appendChild(taskContainer); for (const path of this.currentData.shortestPaths) { const pathDiv = document.createElement('div'); pathDiv.className = 'path-container'; const meta = this.currentData.shortestPathsMeta.find(m => JSON.stringify(m.chainIDs) === JSON.stringify(path)); if (!meta) continue; // 添加缩略图预览区域 const thumbnailsDiv = document.createElement('div'); thumbnailsDiv.className = 'path-thumbnails'; // 创建所有步骤的缩略图 for (let i = 0; i < path.length; i++) { const thumbDiv = document.createElement('div'); thumbDiv.className = 'thumbnail-container'; // 添加步骤编号 const stepLabel = document.createElement('div'); stepLabel.className = 'thumbnail-step-label'; stepLabel.textContent = `${i}`; thumbDiv.appendChild(stepLabel); // 添加缩略图,使用 chainChildNum 而不是滚动位置 const imgPath = i === path.length - 1 ? `screenshots/${path[i]}_full.png` : `screenshots/${path[i]}_${meta.chainChildNum[i]}.png`; const thumbImg = document.createElement('img'); thumbImg.src = imgPath; thumbImg.className = 'thumbnail-image'; thumbImg.onclick = () => this.openModal(thumbImg); thumbDiv.appendChild(thumbImg); thumbnailsDiv.appendChild(thumbDiv); } pathDiv.appendChild(thumbnailsDiv); for (let i = 0; i < path.length; i++) { const stepDiv = document.createElement('div'); stepDiv.className = 'step-container'; const stepHeader = document.createElement('div'); stepHeader.className = 'step-header'; stepHeader.textContent = `步骤 ${i}`; stepDiv.appendChild(stepHeader); const urlDiv = document.createElement('div'); urlDiv.className = 'step-url'; urlDiv.textContent = `URL: ${meta.chainUrls[i]}`; stepDiv.appendChild(urlDiv); // 添加request ID显示和按钮区域 const requestId = path[i]; const requestIdDiv = document.createElement('div'); requestIdDiv.className = 'request-id-container'; // 显示request ID const requestIdLabel = document.createElement('span'); requestIdLabel.textContent = `request id: ${requestId}`; requestIdDiv.appendChild(requestIdLabel); // 添加查看child按钮 const viewChildBtn = document.createElement('button'); viewChildBtn.className = 'view-btn'; viewChildBtn.textContent = '查看child'; viewChildBtn.onclick = () => this.viewChildJson(requestId); requestIdDiv.appendChild(viewChildBtn); // 添加查看request queue按钮 const viewQueueBtn = document.createElement('button'); viewQueueBtn.className = 'view-btn'; viewQueueBtn.textContent = '查看request queue'; viewQueueBtn.onclick = () => this.viewRequestQueueJson(requestId); requestIdDiv.appendChild(viewQueueBtn); stepDiv.appendChild(requestIdDiv); if (i < meta.chainTexts.length) { const textDiv = document.createElement('div'); textDiv.className = 'path-info'; textDiv.textContent = `点击文本: ${meta.chainTexts[i]}`; stepDiv.appendChild(textDiv); } // 构建并显示截图路径,使用 chainChildNum 而不是滚动位置 const imgPath = i === path.length - 1 ? `screenshots/${path[i]}_full.png` : `screenshots/${path[i]}_${meta.chainChildNum[i]}.png`; const screenshotPathDiv = document.createElement('div'); screenshotPathDiv.className = 'screenshot-path'; screenshotPathDiv.textContent = `截图路径: ${imgPath}`; // 添加下载按钮 const downloadBtn = document.createElement('button'); downloadBtn.className = 'download-btn'; downloadBtn.textContent = '下载截图'; downloadBtn.onclick = () => this.downloadImage(imgPath); screenshotPathDiv.appendChild(downloadBtn); stepDiv.appendChild(screenshotPathDiv); const screenshotDiv = document.createElement('div'); screenshotDiv.className = 'screenshot-container'; const img = document.createElement('img'); img.src = imgPath; img.onclick = () => this.openModal(img); img.onload = () => { if (i < meta.chainViewportBoundingBoxes.length && i !== path.length - 1) { const box = meta.chainViewportBoundingBoxes[i]; const boundingBox = document.createElement('div'); boundingBox.className = 'bounding-box'; const scale = img.width / img.naturalWidth; boundingBox.style.left = `${box.x * scale}px`; boundingBox.style.top = `${box.y * scale}px`; boundingBox.style.width = `${box.width * scale}px`; boundingBox.style.height = `${box.height * scale}px`; screenshotDiv.appendChild(boundingBox); } }; screenshotDiv.appendChild(img); stepDiv.appendChild(screenshotDiv); pathDiv.appendChild(stepDiv); } container.appendChild(pathDiv); } } // 添加查看child JSON的方法 async viewChildJson(requestId) { try { const response = await fetch(`path/childs/${requestId}.json`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); this.showJsonModal('Child JSON', data); } catch (error) { console.error('加载child JSON失败:', error); alert(`加载child JSON失败: ${error.message}`); } } // 添加查看request queue JSON的方法 async viewRequestQueueJson(requestId) { try { const response = await fetch(`storage/request_queues/default/${requestId}.json`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); this.showJsonModal('Request Queue JSON', data); } catch (error) { console.error('加载request queue JSON失败:', error); alert(`加载request queue JSON失败: ${error.message}`); } } // 显示JSON模态框 showJsonModal(title, jsonData) { // 检查是否已存在JSON模态框,如果不存在则创建 let jsonModal = document.getElementById('jsonModal'); if (!jsonModal) { jsonModal = document.createElement('div'); jsonModal.id = 'jsonModal'; jsonModal.className = 'modal'; const modalContent = document.createElement('div'); modalContent.className = 'modal-content json-modal-content'; const closeBtn = document.createElement('span'); closeBtn.className = 'close-btn'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => { jsonModal.style.display = 'none'; }; const modalTitle = document.createElement('h3'); modalTitle.id = 'jsonModalTitle'; const preElement = document.createElement('pre'); preElement.id = 'jsonContent'; preElement.className = 'json-content'; modalContent.appendChild(closeBtn); modalContent.appendChild(modalTitle); modalContent.appendChild(preElement); jsonModal.appendChild(modalContent); document.body.appendChild(jsonModal); // 点击模态框外部关闭 jsonModal.onclick = (event) => { if (event.target === jsonModal) { jsonModal.style.display = 'none'; } }; } // 更新模态框内容 document.getElementById('jsonModalTitle').textContent = title; // 格式化并高亮JSON const formattedJson = JSON.stringify(jsonData, null, 2); const preElement = document.getElementById('jsonContent'); preElement.textContent = formattedJson; // 如果有语法高亮库(如highlight.js),可以在这里应用 if (window.hljs) { preElement.innerHTML = window.hljs.highlight('json', formattedJson).value; } // 显示模态框 jsonModal.style.display = 'block'; } // 添加下载图片的方法 async downloadImage(imgPath) { try { const response = await fetch(imgPath); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = imgPath.split('/').pop(); // 使用原始文件名 document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error('下载图片失败:', error); alert('下载图片失败,请重试'); } } } // 初始化可视化器 document.addEventListener('DOMContentLoaded', () => { new PathVisualizer(); });