/**
* 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 = `
输入字段
${inputCount}
输出字段
${outputCount}
⏱️ ${execTime}
`;
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 = `
${renderStepContent(stepKey, stepData)}
`;
stagesAccordion.appendChild(item);
});
}
/**
* 渲染步骤内容
*/
function renderStepContent(stepKey, stepData) {
let html = `
输出数据
${formatJson(stepData.output || {})}
`;
// 如果有子步骤(如实体增强检索的process_details)
if (stepData.output && stepData.output.process_details) {
html += `🔬 子步骤详情
`;
stepData.output.process_details.forEach((detail, idx) => {
html += `
子步骤
${formatJson(detail.steps || {})}
`;
});
html += `
`;
}
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, '>');
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 '' + match + '';
});
}
/**
* 格式化时间戳
*/
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 =
`${meta.icon}` +
`当前环节: ${meta.name} — ${meta.desc}`;
// 更新输入框占位符
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 = `
${meta.icon}
执行环节
${meta.name}
${hasError ? '❌' : '✅'}
状态
${hasError ? '失败' : '成功'}
`;
if (hasError) {
outputArea.innerHTML = `
❌
${escapeHtml(data.error || '未知错误')}
`;
return;
}
// 输入摘要
let summaryHtml = '';
if (data.input_summary) {
summaryHtml += '
📥 输入摘要
';
for (const [k, v] of Object.entries(data.input_summary)) {
summaryHtml += `
${k}${v}
`;
}
summaryHtml += '
';
}
summaryHtml += '
';
// 输出内容
let outputHtml = '📤 输出结果
';
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 += `
${formatJson(output)}`;
}
outputHtml += '
';
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 = `
${hasError ? '❌' : '✅'}
状态
${hasError ? data.error || '失败' : '成功'}
`;
// 链式流程图
const stepNames = ['query_extract', 'entity_enhance', 'parent_doc_enhance', 'extract_results'];
let flowHtml = '';
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 += `
${meta.name}
${timeStr}
${escapeHtml(summary)}
`;
if (i < stepNames.length - 1) {
flowHtml += '
→
';
}
});
flowHtml += '
';
chainFlow.innerHTML = flowHtml;
// 最后一步(extract_results)的详情
const lastStep = steps['extract_results'];
let outputHtml = '';
if (lastStep && lastStep.status === 'success' && lastStep.output) {
outputHtml = '📤 最终输出 (extract_results)
';
outputHtml += renderExtractResultsTable(lastStep.output);
outputHtml += '';
}
// 各步骤折叠详情
outputHtml += '📋 各环节详情
';
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 += `
摘要
${stepData.summary || '无'}
`;
if (stepData.output) {
outputHtml += `
输出数据
${formatJson(stepData.output)}`;
}
if (stepData.error) {
outputHtml += `
错误
${escapeHtml(stepData.error)}`;
}
outputHtml += '
';
});
outputHtml += '
';
outputArea.innerHTML = outputHtml;
}
// ==================== 差异化渲染函数 ====================
function renderQueryExtractTable(queryPairs) {
if (!queryPairs || queryPairs.length === 0) {
return '';
}
let html = '' +
'| # | 实体 (entity) | 搜索关键词 | 背景 (background) | 参数 (parameter) |
';
queryPairs.forEach((qp, i) => {
const keywords = Array.isArray(qp.search_keywords) ? qp.search_keywords.join(', ') : (qp.search_keywords || '');
html += `
| ${i + 1} |
${escapeHtml(qp.entity || '')} |
${escapeHtml(keywords)} |
${escapeHtml(qp.background || '')} |
${escapeHtml(qp.parameter || '')} |
`;
});
html += '
';
return html;
}
function renderSearchResultCards(results, stepName) {
if (!results || results.length === 0) {
return '';
}
let html = `共 ${results.length} 个结果
`;
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 += `
📄 ${escapeHtml(String(fileName))}
${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}
`;
});
html += '
';
return html;
}
function renderParentDocSummary(output) {
let html = '';
for (const [k, v] of Object.entries(output)) {
html += `
${k}${v}
`;
}
html += '
';
if (output.parent_docs && output.parent_docs.length > 0) {
html += `父文档列表 (${output.parent_docs.length})
`;
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 += `
📄 ${escapeHtml(String(fileName))}
${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}
`;
});
html += '
';
}
return html;
}
function renderExtractResultsTable(entityResults) {
if (!entityResults || entityResults.length === 0) {
return '';
}
let html = '' +
'| # | 实体 | combined_query | 分数 | 文件名 | 内容预览 |
';
entityResults.forEach((r, i) => {
const score = r.final_score || r.bfp_rerank_score || 0;
const content = (r.text_content || '').substring(0, 150);
html += `
| ${i + 1} |
${escapeHtml(r.entity || '')} |
${escapeHtml((r.combined_query || '').substring(0, 80))} |
${typeof score === 'number' ? score.toFixed(4) : score} |
${escapeHtml((r.file_name || '').substring(0, 50))} |
${escapeHtml(content)}${content.length >= 150 ? '...' : ''} |
`;
});
html += '
';
return html;
}
// ==================== 工具函数 ====================
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}