| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- /**
- * RAG管道测试可视化工具
- * 用于展示RAG链路各环节输入输出数据流转
- */
- // API配置
- const API_BASE = 'http://localhost:8765';
- // 全局数据存储
- let pipelineData = null;
- let currentSelectedStep = null;
- let serverConnected = false;
- // DOM元素引用
- const uploadArea = document.getElementById('uploadArea');
- const fileInput = document.getElementById('fileInput');
- const pipelineOverview = document.getElementById('pipelineOverview');
- const pipelineFlow = document.getElementById('pipelineFlow');
- const detailPanel = document.getElementById('detailPanel');
- const stagesDetail = document.getElementById('stagesDetail');
- const flowContainer = document.getElementById('flowContainer');
- const stagesAccordion = document.getElementById('stagesAccordion');
- // 阶段配置映射
- const stageConfig = {
- '1_query_extract': {
- icon: '🔍',
- title: '查询提取',
- description: '从输入内容中提取查询对'
- },
- '2_entity_enhance_retrieval': {
- icon: '🎯',
- title: '实体增强检索',
- description: '实体召回 + BFP召回'
- },
- '3_parent_doc_enhancement': {
- icon: '📚',
- title: '父文档增强',
- description: '使用父文档增强检索结果'
- },
- '4_extract_first_result': {
- icon: '✂️',
- title: '结果提取',
- description: '提取最终检索结果'
- },
- '1_rag_retrieval': {
- icon: '🔄',
- title: 'RAG检索',
- description: '完整RAG检索流程'
- },
- '2_parameter_compliance_check': {
- icon: '✅',
- title: '参数合规检查',
- description: 'LLM审查参数合规性'
- }
- };
- // 初始化事件监听
- document.addEventListener('DOMContentLoaded', () => {
- initUploadEvents();
- initTabEvents();
- checkServerStatus();
- // 定时检查服务状态
- setInterval(checkServerStatus, 10000);
- });
- /**
- * 检查服务器状态
- */
- function checkServerStatus() {
- fetch(`${API_BASE}/api/health`)
- .then(response => response.json())
- .then(data => {
- serverConnected = true;
- updateServerStatus(true, data.milvus_ready);
- })
- .catch(() => {
- serverConnected = false;
- updateServerStatus(false, false);
- });
- }
- /**
- * 更新服务器状态显示
- */
- function updateServerStatus(connected, milvusReady) {
- const statusEl = document.getElementById('serverStatus');
- if (!statusEl) return;
- const dot = statusEl.querySelector('.status-dot');
- const text = statusEl.querySelector('.status-text');
- dot.className = 'status-dot ' + (connected ? (milvusReady ? 'online' : 'warning') : 'offline');
- if (connected) {
- text.textContent = milvusReady ? '服务已连接 (Milvus就绪)' : '服务已连接 (Milvus未就绪)';
- } else {
- text.textContent = '服务未连接 - 请启动 rag_pipeline_server.py';
- }
- // 保存到全局变量供其他模块使用
- window.serverConnected = serverConnected;
- window.milvusReady = milvusReady;
- const runRagBtn = document.getElementById('runRagBtn');
- if (runRagBtn) {
- runRagBtn.disabled = !connected;
- }
- }
- /**
- * 执行RAG检索
- */
- function runRAG() {
- const content = document.getElementById('testInput').value.trim();
- if (!content) {
- alert('请输入测试文本');
- return;
- }
- if (!serverConnected) {
- alert('服务未连接,请先启动 rag_pipeline_server.py');
- return;
- }
- // 显示加载状态
- const loadingOverlay = document.getElementById('loadingOverlay');
- const loadingText = document.getElementById('loadingText');
- loadingOverlay.style.display = 'flex';
- loadingText.textContent = '正在执行RAG检索...';
- document.getElementById('runRagBtn').disabled = true;
- fetch(`${API_BASE}/api/rag`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content: content })
- })
- .then(response => {
- if (!response.ok) throw new Error(`请求失败: ${response.status}`);
- return response.json();
- })
- .then(data => {
- pipelineData = data;
- window.pipelineData = data; // 同时保存到全局
- renderPipeline(data);
- loadingOverlay.style.display = 'none';
- document.getElementById('runRagBtn').disabled = false;
- })
- .catch(err => {
- alert(`RAG执行失败: ${err.message}`);
- loadingOverlay.style.display = 'none';
- document.getElementById('runRagBtn').disabled = false;
- });
- }
- /**
- * 初始化上传事件
- */
- function initUploadEvents() {
- uploadArea.addEventListener('click', () => fileInput.click());
-
- uploadArea.addEventListener('dragover', (e) => {
- e.preventDefault();
- uploadArea.classList.add('dragover');
- });
-
- uploadArea.addEventListener('dragleave', () => {
- uploadArea.classList.remove('dragover');
- });
-
- uploadArea.addEventListener('drop', (e) => {
- e.preventDefault();
- uploadArea.classList.remove('dragover');
- const file = e.dataTransfer.files[0];
- if (file) handleFile(file);
- });
-
- fileInput.addEventListener('change', (e) => {
- const file = e.target.files[0];
- if (file) handleFile(file);
- });
- }
- /**
- * 初始化标签页事件
- */
- function initTabEvents() {
- document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- const tabId = btn.dataset.tab;
- switchTab(tabId);
- });
- });
- }
- /**
- * 切换标签页
- */
- function switchTab(tabId) {
- document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.classList.toggle('active', btn.dataset.tab === tabId);
- });
- document.querySelectorAll('.tab-pane').forEach(pane => {
- pane.classList.toggle('active', pane.id === tabId + 'Pane');
- });
- }
- /**
- * 处理上传的文件
- */
- function handleFile(file) {
- if (!file.name.endsWith('.json')) {
- alert('请上传JSON文件');
- return;
- }
-
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const data = JSON.parse(e.target.result);
- pipelineData = data;
- renderPipeline(data);
- } catch (err) {
- alert('JSON解析失败: ' + err.message);
- }
- };
- reader.readAsText(file);
- }
- /**
- * 默认数据文件路径
- */
- const DEFAULT_DATA_PATH = '../../../temp/entity_bfp_recall/rag_pipeline_data.json';
- /**
- * 加载默认数据文件
- */
- function loadSampleData() {
- // 优先从服务器API加载
- if (serverConnected) {
- fetch(`${API_BASE}/api/data`)
- .then(response => {
- if (!response.ok) throw new Error('数据不存在');
- return response.json();
- })
- .then(data => {
- pipelineData = data;
- renderPipeline(data);
- })
- .catch(() => {
- // 服务器没有数据,尝试本地文件
- loadFromPath(DEFAULT_DATA_PATH);
- });
- } else {
- loadFromPath(DEFAULT_DATA_PATH);
- }
- }
- /**
- * 从指定路径加载JSON数据
- */
- function loadFromPath(path) {
- fetch(path)
- .then(response => {
- if (!response.ok) {
- throw new Error(`文件加载失败: ${response.status} ${response.statusText}`);
- }
- return response.json();
- })
- .then(data => {
- pipelineData = data;
- renderPipeline(data);
- })
- .catch(err => {
- alert(`加载数据失败: ${err.message}\n\n请确保已运行 test_rag_pipeline.py 生成数据文件,或手动上传JSON文件。`);
- console.error('加载数据失败:', err);
- });
- }
- /**
- * 清空数据
- */
- function clearData() {
- pipelineData = null;
- currentSelectedStep = null;
- document.getElementById('testInput').value = '';
- pipelineOverview.style.display = 'none';
- pipelineFlow.style.display = 'none';
- detailPanel.style.display = 'none';
- stagesDetail.style.display = 'none';
- flowContainer.innerHTML = '';
- stagesAccordion.innerHTML = '';
- }
- /**
- * 渲染管道数据
- */
- function renderPipeline(data) {
- renderOverview(data);
- renderFlowChart(data);
- renderStagesDetail(data);
-
- pipelineOverview.style.display = 'block';
- pipelineFlow.style.display = 'block';
- stagesDetail.style.display = 'block';
- }
- /**
- * 渲染概览信息
- */
- function renderOverview(data) {
- const steps = data.steps || {};
- const stepCount = Object.keys(steps).length;
-
- document.getElementById('stageCount').textContent = stepCount + ' 个';
-
- // 显示总执行时间
- if (data.total_execution_time) {
- document.getElementById('totalTime').textContent = data.total_execution_time + ' 秒';
- } else {
- document.getElementById('totalTime').textContent = formatTimestamp(data.timestamp);
- }
-
- const hasError = data.error || (data.final_result && data.final_result.retrieval_status === 'no_results');
- document.getElementById('execStatus').textContent = hasError ? '异常' : '成功';
- document.getElementById('execStatus').style.color = hasError ? '#ff5555' : '#00ff88';
- }
- /**
- * 渲染流程图
- */
- function renderFlowChart(data) {
- flowContainer.innerHTML = '';
- const steps = data.steps || {};
-
- Object.entries(steps).forEach(([stepKey, stepData], index) => {
- const config = stageConfig[stepKey] || {
- icon: '📦',
- title: stepData.name || stepKey,
- description: ''
- };
-
- const node = document.createElement('div');
- node.className = 'flow-node';
- node.dataset.step = stepKey;
-
- const inputCount = stepData.input ? Object.keys(stepData.input).length : 0;
- const outputCount = stepData.output ? Object.keys(stepData.output).length : 0;
- const execTime = stepData.execution_time ? `${stepData.execution_time}s` : '-';
-
- node.innerHTML = `
- <div class="node-header">
- <span class="node-icon">${config.icon}</span>
- <span class="node-title">${stepData.name || config.title}</span>
- <span class="node-step">Step ${index + 1}</span>
- </div>
- <div class="node-stats">
- <div class="node-stat">
- <span>输入字段</span>
- <span class="node-stat-value">${inputCount}</span>
- </div>
- <div class="node-stat">
- <span>输出字段</span>
- <span class="node-stat-value">${outputCount}</span>
- </div>
- </div>
- <div class="node-time">⏱️ ${execTime}</div>
- `;
-
- node.addEventListener('click', () => selectStep(stepKey, stepData));
- flowContainer.appendChild(node);
- });
- }
- /**
- * 选择步骤显示详情
- */
- function selectStep(stepKey, stepData) {
- currentSelectedStep = stepKey;
-
- // 更新节点选中状态
- document.querySelectorAll('.flow-node').forEach(node => {
- node.classList.toggle('active', node.dataset.step === stepKey);
- });
-
- // 显示详情面板
- detailPanel.style.display = 'block';
-
- // 渲染输入输出数据
- document.getElementById('inputViewer').innerHTML = formatJson(stepData.input || {});
- document.getElementById('outputViewer').innerHTML = formatJson(stepData.output || {});
- document.getElementById('rawViewer').innerHTML = formatJson(stepData);
-
- // 滚动到详情面板
- detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- /**
- * 渲染各阶段详情
- */
- function renderStagesDetail(data) {
- stagesAccordion.innerHTML = '';
- const steps = data.steps || {};
-
- Object.entries(steps).forEach(([stepKey, stepData], index) => {
- const config = stageConfig[stepKey] || {
- icon: '📦',
- title: stepKey,
- description: ''
- };
-
- const item = document.createElement('div');
- item.className = 'accordion-item';
-
- item.innerHTML = `
- <div class="accordion-header" onclick="toggleAccordion(this)">
- <div class="accordion-title">
- <span>${config.icon}</span>
- <span>Step ${index + 1}: ${config.title}</span>
- </div>
- <span class="accordion-icon">▼</span>
- </div>
- <div class="accordion-content">
- ${renderStepContent(stepKey, stepData)}
- </div>
- `;
-
- stagesAccordion.appendChild(item);
- });
- }
- /**
- * 渲染步骤内容
- */
- function renderStepContent(stepKey, stepData) {
- let html = `
- <div class="data-flow">
- <div class="data-section input">
- <h4>输入数据</h4>
- <pre class="json-viewer">${formatJson(stepData.input || {})}</pre>
- </div>
- <div class="data-section output">
- <h4>输出数据</h4>
- <pre class="json-viewer">${formatJson(stepData.output || {})}</pre>
- </div>
- </div>
- `;
-
- // 如果有子步骤(如实体增强检索的process_details)
- if (stepData.output && stepData.output.process_details) {
- html += `<div class="sub-steps"><h4>🔬 子步骤详情</h4>`;
- stepData.output.process_details.forEach((detail, idx) => {
- html += `
- <div class="sub-step">
- <div class="sub-step-header">查询对 ${detail.index || idx + 1}</div>
- <div class="data-flow">
- <div class="data-section input">
- <h4>输入</h4>
- <pre class="json-viewer">${formatJson(detail.input || {})}</pre>
- </div>
- <div class="data-section output">
- <h4>子步骤</h4>
- <pre class="json-viewer">${formatJson(detail.steps || {})}</pre>
- </div>
- </div>
- </div>
- `;
- });
- html += `</div>`;
- }
-
- return html;
- }
- /**
- * 切换手风琴
- */
- function toggleAccordion(header) {
- const content = header.nextElementSibling;
- const isActive = header.classList.contains('active');
-
- // 关闭所有
- document.querySelectorAll('.accordion-header').forEach(h => h.classList.remove('active'));
- document.querySelectorAll('.accordion-content').forEach(c => c.classList.remove('active'));
-
- // 如果之前不是激活状态,则打开当前
- if (!isActive) {
- header.classList.add('active');
- content.classList.add('active');
- }
- }
- /**
- * 格式化JSON为带语法高亮的HTML
- */
- function formatJson(obj) {
- const json = JSON.stringify(obj, null, 2);
- return syntaxHighlight(json);
- }
- /**
- * JSON语法高亮
- */
- function syntaxHighlight(json) {
- json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
- return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
- let cls = 'json-number';
- if (/^"/.test(match)) {
- if (/:$/.test(match)) {
- cls = 'json-key';
- } else {
- cls = 'json-string';
- }
- } else if (/true|false/.test(match)) {
- cls = 'json-boolean';
- } else if (/null/.test(match)) {
- cls = 'json-null';
- }
- return '<span class="' + cls + '">' + match + '</span>';
- });
- }
- /**
- * 格式化时间戳
- */
- function formatTimestamp(timestamp) {
- if (!timestamp) return '-';
- const date = new Date(timestamp * 1000);
- return date.toLocaleString('zh-CN');
- }
|