crawlee/show_path.js
2025-04-23 12:14:50 +08:00

660 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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].raw_result || '';
}
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);
// 添加查看axtree按钮
const viewAxtreeBtn = document.createElement('button');
viewAxtreeBtn.className = 'view-btn';
viewAxtreeBtn.textContent = '查看axtree';
viewAxtreeBtn.onclick = () => this.viewAxtree(requestId);
requestIdDiv.appendChild(viewAxtreeBtn);
stepDiv.appendChild(requestIdDiv);
if (i < meta.chainTexts.length) {
const textDiv = document.createElement('div');
textDiv.className = 'path-info';
textDiv.textContent = `点击文本: 【${meta.chainTexts[i]}】 AxTreeID: [${meta.chainAxTreeID[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 = '&times;';
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('下载图片失败,请重试');
}
}
// 添加查看axtree的方法
async viewAxtree(requestId) {
try {
// 获取axtree文本文件
const txtResponse = await fetch(`axtrees/${requestId}.txt`);
let txtContent = '';
if (txtResponse.ok) {
txtContent = await txtResponse.text();
} else {
txtContent = '无法加载axtree.txt文件';
}
// 获取idToSelector JSON文件
const jsonResponse = await fetch(`axtrees/${requestId}_idToSelector.json`);
let jsonData = {};
if (jsonResponse.ok) {
jsonData = await jsonResponse.json();
}
// 显示组合的结果
this.showAxtreeModal(requestId, txtContent, jsonData);
} catch (error) {
console.error('加载axtree数据失败:', error);
alert(`加载axtree数据失败: ${error.message}`);
}
}
// 显示Axtree模态框
showAxtreeModal(requestId, txtContent, jsonData) {
// 检查是否已存在Axtree模态框如果不存在则创建
let axtreeModal = document.getElementById('axtreeModal');
if (!axtreeModal) {
axtreeModal = document.createElement('div');
axtreeModal.id = 'axtreeModal';
axtreeModal.className = 'modal';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content json-modal-content'; // 使用与JSON模态框相同的样式
const closeBtn = document.createElement('span');
closeBtn.className = 'close-btn';
closeBtn.innerHTML = '&times;';
closeBtn.onclick = () => { axtreeModal.style.display = 'none'; };
const modalTitle = document.createElement('h3');
modalTitle.id = 'axtreeModalTitle';
// 创建选项卡
const tabContainer = document.createElement('div');
tabContainer.className = 'tab-container';
const txtTab = document.createElement('button');
txtTab.className = 'tab-button active';
txtTab.textContent = 'Axtree文本';
txtTab.onclick = () => {
txtTab.className = 'tab-button active';
jsonTab.className = 'tab-button';
txtContentElement.style.display = 'block';
jsonContentElement.style.display = 'none';
};
const jsonTab = document.createElement('button');
jsonTab.className = 'tab-button';
jsonTab.textContent = 'idToSelector JSON';
jsonTab.onclick = () => {
jsonTab.className = 'tab-button active';
txtTab.className = 'tab-button';
jsonContentElement.style.display = 'block';
txtContentElement.style.display = 'none';
};
tabContainer.appendChild(txtTab);
tabContainer.appendChild(jsonTab);
// 创建内容区域
const txtContentElement = document.createElement('pre');
txtContentElement.id = 'axtreeTxtContent';
txtContentElement.className = 'axtree-content';
txtContentElement.style.textAlign = 'left'; // 确保文本左对齐
txtContentElement.style.whiteSpace = 'pre'; // 保留所有空格和换行
const jsonContentElement = document.createElement('pre');
jsonContentElement.id = 'axtreeJsonContent';
jsonContentElement.className = 'axtree-content';
jsonContentElement.style.display = 'none';
modalContent.appendChild(closeBtn);
modalContent.appendChild(modalTitle);
modalContent.appendChild(tabContainer);
modalContent.appendChild(txtContentElement);
modalContent.appendChild(jsonContentElement);
axtreeModal.appendChild(modalContent);
document.body.appendChild(axtreeModal);
// 点击模态框外部关闭
axtreeModal.onclick = (event) => {
if (event.target === axtreeModal) {
axtreeModal.style.display = 'none';
}
};
}
// 更新模态框内容
document.getElementById('axtreeModalTitle').textContent = `Axtree - ${requestId}`;
// 更新文本内容
const txtContentElement = document.getElementById('axtreeTxtContent');
txtContentElement.textContent = txtContent;
// 格式化并高亮JSON
const formattedJson = JSON.stringify(jsonData, null, 2);
const jsonContentElement = document.getElementById('axtreeJsonContent');
jsonContentElement.textContent = formattedJson;
// 如果有语法高亮库如highlight.js可以在这里应用
if (window.hljs) {
jsonContentElement.innerHTML = window.hljs.highlight('json', formattedJson).value;
}
// 显示模态框
axtreeModal.style.display = 'block';
}
}
// 初始化可视化器
document.addEventListener('DOMContentLoaded', () => {
new PathVisualizer();
});