| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025 |
- /**
- * 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');
- }
- // ==================== 环节调试功能 ====================
- let currentDebugStep = 'query_extract';
- const stepMeta = {
- query_extract: { name: '查询提取', icon: '🔍', desc: '从输入内容中提取查询实体和关键词', inputType: 'text' },
- entity_enhance: { name: '实体增强检索', icon: '🎯', desc: '实体召回 + BFP召回,需要 query_pairs 输入(JSON)', inputType: 'json' },
- multi_stage_recall: { name: '多阶段召回', icon: '🔄', desc: '混合检索 + 重排序', inputType: 'text' },
- hybrid_search: { name: '混合检索', icon: '⚡', desc: 'Dense + Sparse 加权融合检索', inputType: 'text' },
- parent_doc_enhance: { name: '父文档增强', icon: '📚', desc: '使用父文档增强检索结果,需要 bfp_result_lists 输入(JSON)', inputType: 'json' },
- extract_results: { name: '结果提取', icon: '✂️', desc: '按查询对提取高分结果,需要 bfp_result_lists 输入(JSON)', inputType: 'json' }
- };
- function selectDebugStep(stepName) {
- currentDebugStep = stepName;
- const meta = stepMeta[stepName];
- // 更新按钮状态
- document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active'));
- document.querySelector(`[data-step="${stepName}"]`).classList.add('active');
- // 更新提示
- document.getElementById('currentStepInfo').innerHTML =
- `<span class="step-info-icon">${meta.icon}</span>` +
- `<span class="step-info-text">当前环节: <strong>${meta.name}</strong> — ${meta.desc}</span>`;
- // 更新输入框占位符
- const input = document.getElementById('debugInput');
- if (meta.inputType === 'json') {
- input.placeholder = '粘贴 JSON 数据...\n\n示例: [{"entity": "...", "search_keywords": [...], "background": "..."}]';
- } else {
- input.placeholder = '输入测试文本...\n\n示例:主要部件说明\n1、主梁总成\n主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t...';
- }
- // 显示/隐藏相关参数
- updateParamVisibility(stepName);
- }
- function updateParamVisibility(stepName) {
- const visibleParams = {
- query_extract: [],
- entity_enhance: [],
- multi_stage_recall: ['collectionName', 'topK', 'hybridTopK'],
- hybrid_search: ['collectionName', 'topK', 'denseWeight'],
- parent_doc_enhance: ['scoreThreshold', 'maxParents'],
- extract_results: ['scoreThreshold']
- };
- const allParams = ['paramCollectionName', 'paramTopK', 'paramHybridTopK', 'paramScoreThreshold', 'paramDenseWeight', 'paramMaxParents'];
- const visible = visibleParams[stepName] || [];
- allParams.forEach(id => {
- const el = document.getElementById(id);
- if (!el) return;
- const row = el.closest('.param-row');
- if (!row) return;
- const paramKey = {
- 'paramCollectionName': 'collectionName',
- 'paramTopK': 'topK',
- 'paramHybridTopK': 'hybridTopK',
- 'paramScoreThreshold': 'scoreThreshold',
- 'paramDenseWeight': 'denseWeight',
- 'paramMaxParents': 'maxParents'
- }[id];
- row.style.display = visible.includes(paramKey) ? '' : 'none';
- });
- }
- function toggleParamsPanel() {
- const body = document.getElementById('paramsBody');
- const icon = document.getElementById('paramsToggleIcon');
- if (body.style.display === 'none') {
- body.style.display = 'block';
- icon.textContent = '▲';
- } else {
- body.style.display = 'none';
- icon.textContent = '▼';
- }
- }
- function updateDebugServerStatus() {
- const statusEl = document.getElementById('debugServerStatus');
- if (!statusEl) return;
- const dot = statusEl.querySelector('.status-dot');
- const text = statusEl.querySelector('.status-text');
- if (window.serverConnected) {
- dot.className = 'status-dot ' + (window.milvusReady ? 'online' : 'warning');
- text.textContent = window.milvusReady ? '服务已连接 (Milvus就绪)' : '服务已连接 (Milvus未就绪)';
- } else {
- dot.className = 'status-dot offline';
- text.textContent = '服务未连接';
- }
- }
- function getDebugParams() {
- return {
- collection_name: document.getElementById('paramCollectionName').value || 'rag_children_hybrid',
- top_k: parseInt(document.getElementById('paramTopK').value) || 10,
- hybrid_top_k: parseInt(document.getElementById('paramHybridTopK').value) || 50,
- score_threshold: parseFloat(document.getElementById('paramScoreThreshold').value) || 0.5,
- dense_weight: parseFloat(document.getElementById('paramDenseWeight').value) || 0.7,
- sparse_weight: 1.0 - (parseFloat(document.getElementById('paramDenseWeight').value) || 0.7),
- max_parents_per_pair: parseInt(document.getElementById('paramMaxParents').value) || 3
- };
- }
- function runDebugStep() {
- const content = document.getElementById('debugInput').value.trim();
- if (!content) {
- alert('请输入测试内容');
- return;
- }
- if (!window.serverConnected) {
- alert('服务未连接,请先启动 rag_pipeline_server.py');
- return;
- }
- const overlay = document.getElementById('debugLoadingOverlay');
- const loadingText = document.getElementById('debugLoadingText');
- overlay.style.display = 'flex';
- loadingText.textContent = `正在执行: ${stepMeta[currentDebugStep].name}...`;
- document.getElementById('runDebugStepBtn').disabled = true;
- document.getElementById('runDebugChainBtn').disabled = true;
- fetch(`${API_BASE}/api/debug/step`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- step: currentDebugStep,
- content: content,
- params: getDebugParams()
- })
- })
- .then(r => r.json().then(data => ({ status: r.status, data })))
- .then(({ status, data }) => {
- overlay.style.display = 'none';
- document.getElementById('runDebugStepBtn').disabled = false;
- document.getElementById('runDebugChainBtn').disabled = false;
- renderDebugResult(data, false);
- })
- .catch(err => {
- overlay.style.display = 'none';
- document.getElementById('runDebugStepBtn').disabled = false;
- document.getElementById('runDebugChainBtn').disabled = false;
- alert(`执行失败: ${err.message}`);
- });
- }
- function runDebugChain() {
- const content = document.getElementById('debugInput').value.trim();
- if (!content) {
- alert('请输入测试内容');
- return;
- }
- if (!window.serverConnected) {
- alert('服务未连接,请先启动 rag_pipeline_server.py');
- return;
- }
- const overlay = document.getElementById('debugLoadingOverlay');
- const loadingText = document.getElementById('debugLoadingText');
- overlay.style.display = 'flex';
- loadingText.textContent = '链式执行中: query_extract → entity_enhance → parent_doc_enhance → extract_results...';
- document.getElementById('runDebugStepBtn').disabled = true;
- document.getElementById('runDebugChainBtn').disabled = true;
- fetch(`${API_BASE}/api/debug/chain`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- content: content,
- params: getDebugParams()
- })
- })
- .then(r => r.json())
- .then(data => {
- overlay.style.display = 'none';
- document.getElementById('runDebugStepBtn').disabled = false;
- document.getElementById('runDebugChainBtn').disabled = false;
- renderChainResult(data);
- })
- .catch(err => {
- overlay.style.display = 'none';
- document.getElementById('runDebugStepBtn').disabled = false;
- document.getElementById('runDebugChainBtn').disabled = false;
- alert(`链式执行失败: ${err.message}`);
- });
- }
- function clearDebugResult() {
- document.getElementById('debugResultSection').style.display = 'none';
- document.getElementById('debugInput').value = '';
- document.getElementById('chainFlow').style.display = 'none';
- }
- // ==================== 单环节结果渲染 ====================
- function renderDebugResult(data, isChainStep) {
- const section = document.getElementById('debugResultSection');
- const overviewCards = document.getElementById('debugOverviewCards');
- const chainFlow = document.getElementById('chainFlow');
- const outputArea = document.getElementById('debugOutputArea');
- section.style.display = 'block';
- if (!isChainStep) {
- chainFlow.style.display = 'none';
- }
- const stepName = data.step || currentDebugStep;
- const meta = stepMeta[stepName] || { name: stepName, icon: '📦' };
- // 统计卡片
- const execTime = data.execution_time || 0;
- const hasError = data.status === 'error';
- overviewCards.innerHTML = `
- <div class="overview-card">
- <div class="card-icon">${meta.icon}</div>
- <div class="card-content">
- <span class="card-label">执行环节</span>
- <span class="card-value">${meta.name}</span>
- </div>
- </div>
- <div class="overview-card">
- <div class="card-icon">⏱️</div>
- <div class="card-content">
- <span class="card-label">执行时间</span>
- <span class="card-value">${execTime} 秒</span>
- </div>
- </div>
- <div class="overview-card">
- <div class="card-icon">${hasError ? '❌' : '✅'}</div>
- <div class="card-content">
- <span class="card-label">状态</span>
- <span class="card-value" style="color: ${hasError ? '#ff5555' : '#00ff88'}">${hasError ? '失败' : '成功'}</span>
- </div>
- </div>
- `;
- if (hasError) {
- outputArea.innerHTML = `<div class="debug-error-box">
- <div class="debug-error-icon">❌</div>
- <div class="debug-error-msg">${escapeHtml(data.error || '未知错误')}</div>
- </div>`;
- return;
- }
- // 输入摘要
- let summaryHtml = '<div class="debug-summary-box">';
- if (data.input_summary) {
- summaryHtml += '<h4>📥 输入摘要</h4><div class="debug-summary-grid">';
- for (const [k, v] of Object.entries(data.input_summary)) {
- summaryHtml += `<div class="debug-summary-item"><span class="debug-summary-key">${k}</span><span class="debug-summary-val">${v}</span></div>`;
- }
- summaryHtml += '</div>';
- }
- summaryHtml += '</div>';
- // 输出内容
- let outputHtml = '<div class="debug-output-box"><h4>📤 输出结果</h4>';
- const output = data.output;
- if (stepName === 'query_extract' && Array.isArray(output)) {
- outputHtml += renderQueryExtractTable(output);
- } else if ((stepName === 'entity_enhance' || stepName === 'multi_stage_recall' || stepName === 'hybrid_search') && Array.isArray(output)) {
- outputHtml += renderSearchResultCards(output, stepName);
- } else if (stepName === 'parent_doc_enhance' && typeof output === 'object') {
- outputHtml += renderParentDocSummary(output);
- } else if (stepName === 'extract_results' && Array.isArray(output)) {
- outputHtml += renderExtractResultsTable(output);
- } else {
- outputHtml += `<pre class="json-viewer">${formatJson(output)}</pre>`;
- }
- outputHtml += '</div>';
- outputArea.innerHTML = summaryHtml + outputHtml;
- }
- // ==================== 链式执行结果渲染 ====================
- function renderChainResult(data) {
- const section = document.getElementById('debugResultSection');
- const overviewCards = document.getElementById('debugOverviewCards');
- const chainFlow = document.getElementById('chainFlow');
- const outputArea = document.getElementById('debugOutputArea');
- section.style.display = 'block';
- chainFlow.style.display = 'block';
- // 总体统计
- const totalTime = data.execution_time || 0;
- const hasError = data.status === 'error';
- const steps = data.steps || {};
- const stepCount = Object.keys(steps).length;
- overviewCards.innerHTML = `
- <div class="overview-card">
- <div class="card-icon">🔗</div>
- <div class="card-content">
- <span class="card-label">链式执行</span>
- <span class="card-value">${stepCount} 个环节</span>
- </div>
- </div>
- <div class="overview-card">
- <div class="card-icon">⏱️</div>
- <div class="card-content">
- <span class="card-label">总耗时</span>
- <span class="card-value">${totalTime} 秒</span>
- </div>
- </div>
- <div class="overview-card">
- <div class="card-icon">${hasError ? '❌' : '✅'}</div>
- <div class="card-content">
- <span class="card-label">状态</span>
- <span class="card-value" style="color: ${hasError ? '#ff5555' : '#00ff88'}">${hasError ? data.error || '失败' : '成功'}</span>
- </div>
- </div>
- `;
- // 链式流程图
- const stepNames = ['query_extract', 'entity_enhance', 'parent_doc_enhance', 'extract_results'];
- let flowHtml = '<div class="chain-flow-container">';
- stepNames.forEach((sn, i) => {
- const stepData = steps[sn];
- const meta = stepMeta[sn] || { name: sn, icon: '📦' };
- const statusClass = stepData ? (stepData.status === 'success' ? 'chain-step-success' : 'chain-step-error') : 'chain-step-pending';
- const statusIcon = stepData ? (stepData.status === 'success' ? '✅' : '❌') : '⏳';
- const timeStr = stepData ? `${stepData.execution_time}s` : '-';
- const summary = stepData ? (stepData.summary || '') : '未执行';
- flowHtml += `
- <div class="chain-step ${statusClass}">
- <div class="chain-step-header">
- <span class="chain-step-icon">${meta.icon}</span>
- <span class="chain-step-status">${statusIcon}</span>
- </div>
- <div class="chain-step-name">${meta.name}</div>
- <div class="chain-step-time">${timeStr}</div>
- <div class="chain-step-summary">${escapeHtml(summary)}</div>
- </div>`;
- if (i < stepNames.length - 1) {
- flowHtml += '<div class="chain-arrow">→</div>';
- }
- });
- flowHtml += '</div>';
- chainFlow.innerHTML = flowHtml;
- // 最后一步(extract_results)的详情
- const lastStep = steps['extract_results'];
- let outputHtml = '';
- if (lastStep && lastStep.status === 'success' && lastStep.output) {
- outputHtml = '<div class="debug-output-box"><h4>📤 最终输出 (extract_results)</h4>';
- outputHtml += renderExtractResultsTable(lastStep.output);
- outputHtml += '</div>';
- }
- // 各步骤折叠详情
- outputHtml += '<div class="debug-output-box"><h4>📋 各环节详情</h4><div class="accordion">';
- stepNames.forEach(sn => {
- const stepData = steps[sn];
- if (!stepData) return;
- const meta = stepMeta[sn] || { name: sn, icon: '📦' };
- const statusClass = stepData.status === 'success' ? 'status-success' : 'status-error';
- outputHtml += `
- <div class="accordion-item">
- <div class="accordion-header" onclick="toggleAccordion(this)">
- <div class="accordion-title">
- <span>${meta.icon}</span>
- <span>${meta.name}</span>
- <span class="status-badge ${statusClass}">${stepData.status}</span>
- </div>
- <span class="accordion-icon">▼</span>
- </div>
- <div class="accordion-content">
- <div class="debug-step-detail">
- <div class="data-section"><h4>摘要</h4>
- <pre class="json-viewer">${stepData.summary || '无'}</pre>
- </div>`;
- if (stepData.output) {
- outputHtml += `<div class="data-section"><h4>输出数据</h4>
- <pre class="json-viewer">${formatJson(stepData.output)}</pre></div>`;
- }
- if (stepData.error) {
- outputHtml += `<div class="data-section"><h4>错误</h4>
- <pre class="json-viewer" style="color:#ff5555">${escapeHtml(stepData.error)}</pre></div>`;
- }
- outputHtml += '</div></div></div>';
- });
- outputHtml += '</div></div>';
- outputArea.innerHTML = outputHtml;
- }
- // ==================== 差异化渲染函数 ====================
- function renderQueryExtractTable(queryPairs) {
- if (!queryPairs || queryPairs.length === 0) {
- return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>未提取到查询对</p></div>';
- }
- let html = '<div class="debug-table-wrap"><table class="debug-table"><thead><tr>' +
- '<th>#</th><th>实体 (entity)</th><th>搜索关键词</th><th>背景 (background)</th><th>参数 (parameter)</th></tr></thead><tbody>';
- queryPairs.forEach((qp, i) => {
- const keywords = Array.isArray(qp.search_keywords) ? qp.search_keywords.join(', ') : (qp.search_keywords || '');
- html += `<tr>
- <td>${i + 1}</td>
- <td><strong>${escapeHtml(qp.entity || '')}</strong></td>
- <td>${escapeHtml(keywords)}</td>
- <td>${escapeHtml(qp.background || '')}</td>
- <td>${escapeHtml(qp.parameter || '')}</td>
- </tr>`;
- });
- html += '</tbody></table></div>';
- return html;
- }
- function renderSearchResultCards(results, stepName) {
- if (!results || results.length === 0) {
- return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>未检索到结果</p></div>';
- }
- let html = `<div class="debug-result-count">共 ${results.length} 个结果</div><div class="debug-result-cards">`;
- results.forEach((r, i) => {
- const textContent = (r.text_content || r.text || '').substring(0, 300);
- const fileName = r.file_name || r.metadata?.file_name || r.metadata?.document_id || 'N/A';
- const score = r.rerank_score || r.hybrid_similarity || r.bfp_rerank_score || r.distance || 0;
- const scoreType = r.rerank_score ? 'rerank' : (r.hybrid_similarity ? 'hybrid' : (r.bfp_rerank_score ? 'bfp' : 'score'));
- html += `
- <div class="debug-result-card">
- <div class="drc-header">
- <span class="drc-index">#${i + 1}</span>
- <span class="drc-score">${scoreType}: ${typeof score === 'number' ? score.toFixed(4) : score}</span>
- </div>
- <div class="drc-file">📄 ${escapeHtml(String(fileName))}</div>
- <div class="drc-content">${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}</div>
- </div>`;
- });
- html += '</div>';
- return html;
- }
- function renderParentDocSummary(output) {
- let html = '<div class="debug-summary-grid">';
- for (const [k, v] of Object.entries(output)) {
- html += `<div class="debug-summary-item"><span class="debug-summary-key">${k}</span><span class="debug-summary-val">${v}</span></div>`;
- }
- html += '</div>';
- if (output.parent_docs && output.parent_docs.length > 0) {
- html += `<div class="debug-result-count">父文档列表 (${output.parent_docs.length})</div><div class="debug-result-cards">`;
- output.parent_docs.forEach((p, i) => {
- const textContent = (p.text_content || '').substring(0, 300);
- const fileName = p.metadata?.file_name || p.metadata?.document_id || 'N/A';
- html += `
- <div class="debug-result-card parent-doc">
- <div class="drc-header">
- <span class="drc-index">📚 父文档 #${i + 1}</span>
- </div>
- <div class="drc-file">📄 ${escapeHtml(String(fileName))}</div>
- <div class="drc-content">${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}</div>
- </div>`;
- });
- html += '</div>';
- }
- return html;
- }
- function renderExtractResultsTable(entityResults) {
- if (!entityResults || entityResults.length === 0) {
- return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>没有通过阈值过滤的结果</p></div>';
- }
- let html = '<div class="debug-table-wrap"><table class="debug-table"><thead><tr>' +
- '<th>#</th><th>实体</th><th>combined_query</th><th>分数</th><th>文件名</th><th>内容预览</th></tr></thead><tbody>';
- entityResults.forEach((r, i) => {
- const score = r.final_score || r.bfp_rerank_score || 0;
- const content = (r.text_content || '').substring(0, 150);
- html += `<tr>
- <td>${i + 1}</td>
- <td><strong>${escapeHtml(r.entity || '')}</strong></td>
- <td>${escapeHtml((r.combined_query || '').substring(0, 80))}</td>
- <td><span class="score-badge">${typeof score === 'number' ? score.toFixed(4) : score}</span></td>
- <td>${escapeHtml((r.file_name || '').substring(0, 50))}</td>
- <td class="preview-cell">${escapeHtml(content)}${content.length >= 150 ? '...' : ''}</td>
- </tr>`;
- });
- html += '</tbody></table></div>';
- return html;
- }
- // ==================== 工具函数 ====================
- function escapeHtml(str) {
- if (!str) return '';
- return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
- }
|