app.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. /**
  2. * RAG管道测试可视化工具
  3. * 用于展示RAG链路各环节输入输出数据流转
  4. */
  5. // API配置
  6. const API_BASE = 'http://localhost:8765';
  7. // 全局数据存储
  8. let pipelineData = null;
  9. let currentSelectedStep = null;
  10. let serverConnected = false;
  11. // DOM元素引用
  12. const uploadArea = document.getElementById('uploadArea');
  13. const fileInput = document.getElementById('fileInput');
  14. const pipelineOverview = document.getElementById('pipelineOverview');
  15. const pipelineFlow = document.getElementById('pipelineFlow');
  16. const detailPanel = document.getElementById('detailPanel');
  17. const stagesDetail = document.getElementById('stagesDetail');
  18. const flowContainer = document.getElementById('flowContainer');
  19. const stagesAccordion = document.getElementById('stagesAccordion');
  20. // 阶段配置映射
  21. const stageConfig = {
  22. '1_query_extract': {
  23. icon: '🔍',
  24. title: '查询提取',
  25. description: '从输入内容中提取查询对'
  26. },
  27. '2_entity_enhance_retrieval': {
  28. icon: '🎯',
  29. title: '实体增强检索',
  30. description: '实体召回 + BFP召回'
  31. },
  32. '3_parent_doc_enhancement': {
  33. icon: '📚',
  34. title: '父文档增强',
  35. description: '使用父文档增强检索结果'
  36. },
  37. '4_extract_first_result': {
  38. icon: '✂️',
  39. title: '结果提取',
  40. description: '提取最终检索结果'
  41. },
  42. '1_rag_retrieval': {
  43. icon: '🔄',
  44. title: 'RAG检索',
  45. description: '完整RAG检索流程'
  46. },
  47. '2_parameter_compliance_check': {
  48. icon: '✅',
  49. title: '参数合规检查',
  50. description: 'LLM审查参数合规性'
  51. }
  52. };
  53. // 初始化事件监听
  54. document.addEventListener('DOMContentLoaded', () => {
  55. initUploadEvents();
  56. initTabEvents();
  57. checkServerStatus();
  58. // 定时检查服务状态
  59. setInterval(checkServerStatus, 10000);
  60. });
  61. /**
  62. * 检查服务器状态
  63. */
  64. function checkServerStatus() {
  65. fetch(`${API_BASE}/api/health`)
  66. .then(response => response.json())
  67. .then(data => {
  68. serverConnected = true;
  69. updateServerStatus(true, data.milvus_ready);
  70. })
  71. .catch(() => {
  72. serverConnected = false;
  73. updateServerStatus(false, false);
  74. });
  75. }
  76. /**
  77. * 更新服务器状态显示
  78. */
  79. function updateServerStatus(connected, milvusReady) {
  80. const statusEl = document.getElementById('serverStatus');
  81. if (!statusEl) return;
  82. const dot = statusEl.querySelector('.status-dot');
  83. const text = statusEl.querySelector('.status-text');
  84. dot.className = 'status-dot ' + (connected ? (milvusReady ? 'online' : 'warning') : 'offline');
  85. if (connected) {
  86. text.textContent = milvusReady ? '服务已连接 (Milvus就绪)' : '服务已连接 (Milvus未就绪)';
  87. } else {
  88. text.textContent = '服务未连接 - 请启动 rag_pipeline_server.py';
  89. }
  90. // 保存到全局变量供其他模块使用
  91. window.serverConnected = serverConnected;
  92. window.milvusReady = milvusReady;
  93. const runRagBtn = document.getElementById('runRagBtn');
  94. if (runRagBtn) {
  95. runRagBtn.disabled = !connected;
  96. }
  97. }
  98. /**
  99. * 执行RAG检索
  100. */
  101. function runRAG() {
  102. const content = document.getElementById('testInput').value.trim();
  103. if (!content) {
  104. alert('请输入测试文本');
  105. return;
  106. }
  107. if (!serverConnected) {
  108. alert('服务未连接,请先启动 rag_pipeline_server.py');
  109. return;
  110. }
  111. // 显示加载状态
  112. const loadingOverlay = document.getElementById('loadingOverlay');
  113. const loadingText = document.getElementById('loadingText');
  114. loadingOverlay.style.display = 'flex';
  115. loadingText.textContent = '正在执行RAG检索...';
  116. document.getElementById('runRagBtn').disabled = true;
  117. fetch(`${API_BASE}/api/rag`, {
  118. method: 'POST',
  119. headers: { 'Content-Type': 'application/json' },
  120. body: JSON.stringify({ content: content })
  121. })
  122. .then(response => {
  123. if (!response.ok) throw new Error(`请求失败: ${response.status}`);
  124. return response.json();
  125. })
  126. .then(data => {
  127. pipelineData = data;
  128. window.pipelineData = data; // 同时保存到全局
  129. renderPipeline(data);
  130. loadingOverlay.style.display = 'none';
  131. document.getElementById('runRagBtn').disabled = false;
  132. })
  133. .catch(err => {
  134. alert(`RAG执行失败: ${err.message}`);
  135. loadingOverlay.style.display = 'none';
  136. document.getElementById('runRagBtn').disabled = false;
  137. });
  138. }
  139. /**
  140. * 初始化上传事件
  141. */
  142. function initUploadEvents() {
  143. uploadArea.addEventListener('click', () => fileInput.click());
  144. uploadArea.addEventListener('dragover', (e) => {
  145. e.preventDefault();
  146. uploadArea.classList.add('dragover');
  147. });
  148. uploadArea.addEventListener('dragleave', () => {
  149. uploadArea.classList.remove('dragover');
  150. });
  151. uploadArea.addEventListener('drop', (e) => {
  152. e.preventDefault();
  153. uploadArea.classList.remove('dragover');
  154. const file = e.dataTransfer.files[0];
  155. if (file) handleFile(file);
  156. });
  157. fileInput.addEventListener('change', (e) => {
  158. const file = e.target.files[0];
  159. if (file) handleFile(file);
  160. });
  161. }
  162. /**
  163. * 初始化标签页事件
  164. */
  165. function initTabEvents() {
  166. document.querySelectorAll('.tab-btn').forEach(btn => {
  167. btn.addEventListener('click', () => {
  168. const tabId = btn.dataset.tab;
  169. switchTab(tabId);
  170. });
  171. });
  172. }
  173. /**
  174. * 切换标签页
  175. */
  176. function switchTab(tabId) {
  177. document.querySelectorAll('.tab-btn').forEach(btn => {
  178. btn.classList.toggle('active', btn.dataset.tab === tabId);
  179. });
  180. document.querySelectorAll('.tab-pane').forEach(pane => {
  181. pane.classList.toggle('active', pane.id === tabId + 'Pane');
  182. });
  183. }
  184. /**
  185. * 处理上传的文件
  186. */
  187. function handleFile(file) {
  188. if (!file.name.endsWith('.json')) {
  189. alert('请上传JSON文件');
  190. return;
  191. }
  192. const reader = new FileReader();
  193. reader.onload = (e) => {
  194. try {
  195. const data = JSON.parse(e.target.result);
  196. pipelineData = data;
  197. renderPipeline(data);
  198. } catch (err) {
  199. alert('JSON解析失败: ' + err.message);
  200. }
  201. };
  202. reader.readAsText(file);
  203. }
  204. /**
  205. * 默认数据文件路径
  206. */
  207. const DEFAULT_DATA_PATH = '../../../temp/entity_bfp_recall/rag_pipeline_data.json';
  208. /**
  209. * 加载默认数据文件
  210. */
  211. function loadSampleData() {
  212. // 优先从服务器API加载
  213. if (serverConnected) {
  214. fetch(`${API_BASE}/api/data`)
  215. .then(response => {
  216. if (!response.ok) throw new Error('数据不存在');
  217. return response.json();
  218. })
  219. .then(data => {
  220. pipelineData = data;
  221. renderPipeline(data);
  222. })
  223. .catch(() => {
  224. // 服务器没有数据,尝试本地文件
  225. loadFromPath(DEFAULT_DATA_PATH);
  226. });
  227. } else {
  228. loadFromPath(DEFAULT_DATA_PATH);
  229. }
  230. }
  231. /**
  232. * 从指定路径加载JSON数据
  233. */
  234. function loadFromPath(path) {
  235. fetch(path)
  236. .then(response => {
  237. if (!response.ok) {
  238. throw new Error(`文件加载失败: ${response.status} ${response.statusText}`);
  239. }
  240. return response.json();
  241. })
  242. .then(data => {
  243. pipelineData = data;
  244. renderPipeline(data);
  245. })
  246. .catch(err => {
  247. alert(`加载数据失败: ${err.message}\n\n请确保已运行 test_rag_pipeline.py 生成数据文件,或手动上传JSON文件。`);
  248. console.error('加载数据失败:', err);
  249. });
  250. }
  251. /**
  252. * 清空数据
  253. */
  254. function clearData() {
  255. pipelineData = null;
  256. currentSelectedStep = null;
  257. document.getElementById('testInput').value = '';
  258. pipelineOverview.style.display = 'none';
  259. pipelineFlow.style.display = 'none';
  260. detailPanel.style.display = 'none';
  261. stagesDetail.style.display = 'none';
  262. flowContainer.innerHTML = '';
  263. stagesAccordion.innerHTML = '';
  264. }
  265. /**
  266. * 渲染管道数据
  267. */
  268. function renderPipeline(data) {
  269. renderOverview(data);
  270. renderFlowChart(data);
  271. renderStagesDetail(data);
  272. pipelineOverview.style.display = 'block';
  273. pipelineFlow.style.display = 'block';
  274. stagesDetail.style.display = 'block';
  275. }
  276. /**
  277. * 渲染概览信息
  278. */
  279. function renderOverview(data) {
  280. const steps = data.steps || {};
  281. const stepCount = Object.keys(steps).length;
  282. document.getElementById('stageCount').textContent = stepCount + ' 个';
  283. // 显示总执行时间
  284. if (data.total_execution_time) {
  285. document.getElementById('totalTime').textContent = data.total_execution_time + ' 秒';
  286. } else {
  287. document.getElementById('totalTime').textContent = formatTimestamp(data.timestamp);
  288. }
  289. const hasError = data.error || (data.final_result && data.final_result.retrieval_status === 'no_results');
  290. document.getElementById('execStatus').textContent = hasError ? '异常' : '成功';
  291. document.getElementById('execStatus').style.color = hasError ? '#ff5555' : '#00ff88';
  292. }
  293. /**
  294. * 渲染流程图
  295. */
  296. function renderFlowChart(data) {
  297. flowContainer.innerHTML = '';
  298. const steps = data.steps || {};
  299. Object.entries(steps).forEach(([stepKey, stepData], index) => {
  300. const config = stageConfig[stepKey] || {
  301. icon: '📦',
  302. title: stepData.name || stepKey,
  303. description: ''
  304. };
  305. const node = document.createElement('div');
  306. node.className = 'flow-node';
  307. node.dataset.step = stepKey;
  308. const inputCount = stepData.input ? Object.keys(stepData.input).length : 0;
  309. const outputCount = stepData.output ? Object.keys(stepData.output).length : 0;
  310. const execTime = stepData.execution_time ? `${stepData.execution_time}s` : '-';
  311. node.innerHTML = `
  312. <div class="node-header">
  313. <span class="node-icon">${config.icon}</span>
  314. <span class="node-title">${stepData.name || config.title}</span>
  315. <span class="node-step">Step ${index + 1}</span>
  316. </div>
  317. <div class="node-stats">
  318. <div class="node-stat">
  319. <span>输入字段</span>
  320. <span class="node-stat-value">${inputCount}</span>
  321. </div>
  322. <div class="node-stat">
  323. <span>输出字段</span>
  324. <span class="node-stat-value">${outputCount}</span>
  325. </div>
  326. </div>
  327. <div class="node-time">⏱️ ${execTime}</div>
  328. `;
  329. node.addEventListener('click', () => selectStep(stepKey, stepData));
  330. flowContainer.appendChild(node);
  331. });
  332. }
  333. /**
  334. * 选择步骤显示详情
  335. */
  336. function selectStep(stepKey, stepData) {
  337. currentSelectedStep = stepKey;
  338. // 更新节点选中状态
  339. document.querySelectorAll('.flow-node').forEach(node => {
  340. node.classList.toggle('active', node.dataset.step === stepKey);
  341. });
  342. // 显示详情面板
  343. detailPanel.style.display = 'block';
  344. // 渲染输入输出数据
  345. document.getElementById('inputViewer').innerHTML = formatJson(stepData.input || {});
  346. document.getElementById('outputViewer').innerHTML = formatJson(stepData.output || {});
  347. document.getElementById('rawViewer').innerHTML = formatJson(stepData);
  348. // 滚动到详情面板
  349. detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
  350. }
  351. /**
  352. * 渲染各阶段详情
  353. */
  354. function renderStagesDetail(data) {
  355. stagesAccordion.innerHTML = '';
  356. const steps = data.steps || {};
  357. Object.entries(steps).forEach(([stepKey, stepData], index) => {
  358. const config = stageConfig[stepKey] || {
  359. icon: '📦',
  360. title: stepKey,
  361. description: ''
  362. };
  363. const item = document.createElement('div');
  364. item.className = 'accordion-item';
  365. item.innerHTML = `
  366. <div class="accordion-header" onclick="toggleAccordion(this)">
  367. <div class="accordion-title">
  368. <span>${config.icon}</span>
  369. <span>Step ${index + 1}: ${config.title}</span>
  370. </div>
  371. <span class="accordion-icon">▼</span>
  372. </div>
  373. <div class="accordion-content">
  374. ${renderStepContent(stepKey, stepData)}
  375. </div>
  376. `;
  377. stagesAccordion.appendChild(item);
  378. });
  379. }
  380. /**
  381. * 渲染步骤内容
  382. */
  383. function renderStepContent(stepKey, stepData) {
  384. let html = `
  385. <div class="data-flow">
  386. <div class="data-section input">
  387. <h4>输入数据</h4>
  388. <pre class="json-viewer">${formatJson(stepData.input || {})}</pre>
  389. </div>
  390. <div class="data-section output">
  391. <h4>输出数据</h4>
  392. <pre class="json-viewer">${formatJson(stepData.output || {})}</pre>
  393. </div>
  394. </div>
  395. `;
  396. // 如果有子步骤(如实体增强检索的process_details)
  397. if (stepData.output && stepData.output.process_details) {
  398. html += `<div class="sub-steps"><h4>🔬 子步骤详情</h4>`;
  399. stepData.output.process_details.forEach((detail, idx) => {
  400. html += `
  401. <div class="sub-step">
  402. <div class="sub-step-header">查询对 ${detail.index || idx + 1}</div>
  403. <div class="data-flow">
  404. <div class="data-section input">
  405. <h4>输入</h4>
  406. <pre class="json-viewer">${formatJson(detail.input || {})}</pre>
  407. </div>
  408. <div class="data-section output">
  409. <h4>子步骤</h4>
  410. <pre class="json-viewer">${formatJson(detail.steps || {})}</pre>
  411. </div>
  412. </div>
  413. </div>
  414. `;
  415. });
  416. html += `</div>`;
  417. }
  418. return html;
  419. }
  420. /**
  421. * 切换手风琴
  422. */
  423. function toggleAccordion(header) {
  424. const content = header.nextElementSibling;
  425. const isActive = header.classList.contains('active');
  426. // 关闭所有
  427. document.querySelectorAll('.accordion-header').forEach(h => h.classList.remove('active'));
  428. document.querySelectorAll('.accordion-content').forEach(c => c.classList.remove('active'));
  429. // 如果之前不是激活状态,则打开当前
  430. if (!isActive) {
  431. header.classList.add('active');
  432. content.classList.add('active');
  433. }
  434. }
  435. /**
  436. * 格式化JSON为带语法高亮的HTML
  437. */
  438. function formatJson(obj) {
  439. const json = JSON.stringify(obj, null, 2);
  440. return syntaxHighlight(json);
  441. }
  442. /**
  443. * JSON语法高亮
  444. */
  445. function syntaxHighlight(json) {
  446. json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  447. return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
  448. let cls = 'json-number';
  449. if (/^"/.test(match)) {
  450. if (/:$/.test(match)) {
  451. cls = 'json-key';
  452. } else {
  453. cls = 'json-string';
  454. }
  455. } else if (/true|false/.test(match)) {
  456. cls = 'json-boolean';
  457. } else if (/null/.test(match)) {
  458. cls = 'json-null';
  459. }
  460. return '<span class="' + cls + '">' + match + '</span>';
  461. });
  462. }
  463. /**
  464. * 格式化时间戳
  465. */
  466. function formatTimestamp(timestamp) {
  467. if (!timestamp) return '-';
  468. const date = new Date(timestamp * 1000);
  469. return date.toLocaleString('zh-CN');
  470. }
  471. // ==================== 环节调试功能 ====================
  472. let currentDebugStep = 'query_extract';
  473. const stepMeta = {
  474. query_extract: { name: '查询提取', icon: '🔍', desc: '从输入内容中提取查询实体和关键词', inputType: 'text' },
  475. entity_enhance: { name: '实体增强检索', icon: '🎯', desc: '实体召回 + BFP召回,需要 query_pairs 输入(JSON)', inputType: 'json' },
  476. multi_stage_recall: { name: '多阶段召回', icon: '🔄', desc: '混合检索 + 重排序', inputType: 'text' },
  477. hybrid_search: { name: '混合检索', icon: '⚡', desc: 'Dense + Sparse 加权融合检索', inputType: 'text' },
  478. parent_doc_enhance: { name: '父文档增强', icon: '📚', desc: '使用父文档增强检索结果,需要 bfp_result_lists 输入(JSON)', inputType: 'json' },
  479. extract_results: { name: '结果提取', icon: '✂️', desc: '按查询对提取高分结果,需要 bfp_result_lists 输入(JSON)', inputType: 'json' }
  480. };
  481. function selectDebugStep(stepName) {
  482. currentDebugStep = stepName;
  483. const meta = stepMeta[stepName];
  484. // 更新按钮状态
  485. document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active'));
  486. document.querySelector(`[data-step="${stepName}"]`).classList.add('active');
  487. // 更新提示
  488. document.getElementById('currentStepInfo').innerHTML =
  489. `<span class="step-info-icon">${meta.icon}</span>` +
  490. `<span class="step-info-text">当前环节: <strong>${meta.name}</strong> — ${meta.desc}</span>`;
  491. // 更新输入框占位符
  492. const input = document.getElementById('debugInput');
  493. if (meta.inputType === 'json') {
  494. input.placeholder = '粘贴 JSON 数据...\n\n示例: [{"entity": "...", "search_keywords": [...], "background": "..."}]';
  495. } else {
  496. input.placeholder = '输入测试文本...\n\n示例:主要部件说明\n1、主梁总成\n主梁总成由主梁和导梁构成。主梁单节长12m,共7节,每节重10.87t...';
  497. }
  498. // 显示/隐藏相关参数
  499. updateParamVisibility(stepName);
  500. }
  501. function updateParamVisibility(stepName) {
  502. const visibleParams = {
  503. query_extract: [],
  504. entity_enhance: [],
  505. multi_stage_recall: ['collectionName', 'topK', 'hybridTopK'],
  506. hybrid_search: ['collectionName', 'topK', 'denseWeight'],
  507. parent_doc_enhance: ['scoreThreshold', 'maxParents'],
  508. extract_results: ['scoreThreshold']
  509. };
  510. const allParams = ['paramCollectionName', 'paramTopK', 'paramHybridTopK', 'paramScoreThreshold', 'paramDenseWeight', 'paramMaxParents'];
  511. const visible = visibleParams[stepName] || [];
  512. allParams.forEach(id => {
  513. const el = document.getElementById(id);
  514. if (!el) return;
  515. const row = el.closest('.param-row');
  516. if (!row) return;
  517. const paramKey = {
  518. 'paramCollectionName': 'collectionName',
  519. 'paramTopK': 'topK',
  520. 'paramHybridTopK': 'hybridTopK',
  521. 'paramScoreThreshold': 'scoreThreshold',
  522. 'paramDenseWeight': 'denseWeight',
  523. 'paramMaxParents': 'maxParents'
  524. }[id];
  525. row.style.display = visible.includes(paramKey) ? '' : 'none';
  526. });
  527. }
  528. function toggleParamsPanel() {
  529. const body = document.getElementById('paramsBody');
  530. const icon = document.getElementById('paramsToggleIcon');
  531. if (body.style.display === 'none') {
  532. body.style.display = 'block';
  533. icon.textContent = '▲';
  534. } else {
  535. body.style.display = 'none';
  536. icon.textContent = '▼';
  537. }
  538. }
  539. function updateDebugServerStatus() {
  540. const statusEl = document.getElementById('debugServerStatus');
  541. if (!statusEl) return;
  542. const dot = statusEl.querySelector('.status-dot');
  543. const text = statusEl.querySelector('.status-text');
  544. if (window.serverConnected) {
  545. dot.className = 'status-dot ' + (window.milvusReady ? 'online' : 'warning');
  546. text.textContent = window.milvusReady ? '服务已连接 (Milvus就绪)' : '服务已连接 (Milvus未就绪)';
  547. } else {
  548. dot.className = 'status-dot offline';
  549. text.textContent = '服务未连接';
  550. }
  551. }
  552. function getDebugParams() {
  553. return {
  554. collection_name: document.getElementById('paramCollectionName').value || 'rag_children_hybrid',
  555. top_k: parseInt(document.getElementById('paramTopK').value) || 10,
  556. hybrid_top_k: parseInt(document.getElementById('paramHybridTopK').value) || 50,
  557. score_threshold: parseFloat(document.getElementById('paramScoreThreshold').value) || 0.5,
  558. dense_weight: parseFloat(document.getElementById('paramDenseWeight').value) || 0.7,
  559. sparse_weight: 1.0 - (parseFloat(document.getElementById('paramDenseWeight').value) || 0.7),
  560. max_parents_per_pair: parseInt(document.getElementById('paramMaxParents').value) || 3
  561. };
  562. }
  563. function runDebugStep() {
  564. const content = document.getElementById('debugInput').value.trim();
  565. if (!content) {
  566. alert('请输入测试内容');
  567. return;
  568. }
  569. if (!window.serverConnected) {
  570. alert('服务未连接,请先启动 rag_pipeline_server.py');
  571. return;
  572. }
  573. const overlay = document.getElementById('debugLoadingOverlay');
  574. const loadingText = document.getElementById('debugLoadingText');
  575. overlay.style.display = 'flex';
  576. loadingText.textContent = `正在执行: ${stepMeta[currentDebugStep].name}...`;
  577. document.getElementById('runDebugStepBtn').disabled = true;
  578. document.getElementById('runDebugChainBtn').disabled = true;
  579. fetch(`${API_BASE}/api/debug/step`, {
  580. method: 'POST',
  581. headers: { 'Content-Type': 'application/json' },
  582. body: JSON.stringify({
  583. step: currentDebugStep,
  584. content: content,
  585. params: getDebugParams()
  586. })
  587. })
  588. .then(r => r.json().then(data => ({ status: r.status, data })))
  589. .then(({ status, data }) => {
  590. overlay.style.display = 'none';
  591. document.getElementById('runDebugStepBtn').disabled = false;
  592. document.getElementById('runDebugChainBtn').disabled = false;
  593. renderDebugResult(data, false);
  594. })
  595. .catch(err => {
  596. overlay.style.display = 'none';
  597. document.getElementById('runDebugStepBtn').disabled = false;
  598. document.getElementById('runDebugChainBtn').disabled = false;
  599. alert(`执行失败: ${err.message}`);
  600. });
  601. }
  602. function runDebugChain() {
  603. const content = document.getElementById('debugInput').value.trim();
  604. if (!content) {
  605. alert('请输入测试内容');
  606. return;
  607. }
  608. if (!window.serverConnected) {
  609. alert('服务未连接,请先启动 rag_pipeline_server.py');
  610. return;
  611. }
  612. const overlay = document.getElementById('debugLoadingOverlay');
  613. const loadingText = document.getElementById('debugLoadingText');
  614. overlay.style.display = 'flex';
  615. loadingText.textContent = '链式执行中: query_extract → entity_enhance → parent_doc_enhance → extract_results...';
  616. document.getElementById('runDebugStepBtn').disabled = true;
  617. document.getElementById('runDebugChainBtn').disabled = true;
  618. fetch(`${API_BASE}/api/debug/chain`, {
  619. method: 'POST',
  620. headers: { 'Content-Type': 'application/json' },
  621. body: JSON.stringify({
  622. content: content,
  623. params: getDebugParams()
  624. })
  625. })
  626. .then(r => r.json())
  627. .then(data => {
  628. overlay.style.display = 'none';
  629. document.getElementById('runDebugStepBtn').disabled = false;
  630. document.getElementById('runDebugChainBtn').disabled = false;
  631. renderChainResult(data);
  632. })
  633. .catch(err => {
  634. overlay.style.display = 'none';
  635. document.getElementById('runDebugStepBtn').disabled = false;
  636. document.getElementById('runDebugChainBtn').disabled = false;
  637. alert(`链式执行失败: ${err.message}`);
  638. });
  639. }
  640. function clearDebugResult() {
  641. document.getElementById('debugResultSection').style.display = 'none';
  642. document.getElementById('debugInput').value = '';
  643. document.getElementById('chainFlow').style.display = 'none';
  644. }
  645. // ==================== 单环节结果渲染 ====================
  646. function renderDebugResult(data, isChainStep) {
  647. const section = document.getElementById('debugResultSection');
  648. const overviewCards = document.getElementById('debugOverviewCards');
  649. const chainFlow = document.getElementById('chainFlow');
  650. const outputArea = document.getElementById('debugOutputArea');
  651. section.style.display = 'block';
  652. if (!isChainStep) {
  653. chainFlow.style.display = 'none';
  654. }
  655. const stepName = data.step || currentDebugStep;
  656. const meta = stepMeta[stepName] || { name: stepName, icon: '📦' };
  657. // 统计卡片
  658. const execTime = data.execution_time || 0;
  659. const hasError = data.status === 'error';
  660. overviewCards.innerHTML = `
  661. <div class="overview-card">
  662. <div class="card-icon">${meta.icon}</div>
  663. <div class="card-content">
  664. <span class="card-label">执行环节</span>
  665. <span class="card-value">${meta.name}</span>
  666. </div>
  667. </div>
  668. <div class="overview-card">
  669. <div class="card-icon">⏱️</div>
  670. <div class="card-content">
  671. <span class="card-label">执行时间</span>
  672. <span class="card-value">${execTime} 秒</span>
  673. </div>
  674. </div>
  675. <div class="overview-card">
  676. <div class="card-icon">${hasError ? '❌' : '✅'}</div>
  677. <div class="card-content">
  678. <span class="card-label">状态</span>
  679. <span class="card-value" style="color: ${hasError ? '#ff5555' : '#00ff88'}">${hasError ? '失败' : '成功'}</span>
  680. </div>
  681. </div>
  682. `;
  683. if (hasError) {
  684. outputArea.innerHTML = `<div class="debug-error-box">
  685. <div class="debug-error-icon">❌</div>
  686. <div class="debug-error-msg">${escapeHtml(data.error || '未知错误')}</div>
  687. </div>`;
  688. return;
  689. }
  690. // 输入摘要
  691. let summaryHtml = '<div class="debug-summary-box">';
  692. if (data.input_summary) {
  693. summaryHtml += '<h4>📥 输入摘要</h4><div class="debug-summary-grid">';
  694. for (const [k, v] of Object.entries(data.input_summary)) {
  695. summaryHtml += `<div class="debug-summary-item"><span class="debug-summary-key">${k}</span><span class="debug-summary-val">${v}</span></div>`;
  696. }
  697. summaryHtml += '</div>';
  698. }
  699. summaryHtml += '</div>';
  700. // 输出内容
  701. let outputHtml = '<div class="debug-output-box"><h4>📤 输出结果</h4>';
  702. const output = data.output;
  703. if (stepName === 'query_extract' && Array.isArray(output)) {
  704. outputHtml += renderQueryExtractTable(output);
  705. } else if ((stepName === 'entity_enhance' || stepName === 'multi_stage_recall' || stepName === 'hybrid_search') && Array.isArray(output)) {
  706. outputHtml += renderSearchResultCards(output, stepName);
  707. } else if (stepName === 'parent_doc_enhance' && typeof output === 'object') {
  708. outputHtml += renderParentDocSummary(output);
  709. } else if (stepName === 'extract_results' && Array.isArray(output)) {
  710. outputHtml += renderExtractResultsTable(output);
  711. } else {
  712. outputHtml += `<pre class="json-viewer">${formatJson(output)}</pre>`;
  713. }
  714. outputHtml += '</div>';
  715. outputArea.innerHTML = summaryHtml + outputHtml;
  716. }
  717. // ==================== 链式执行结果渲染 ====================
  718. function renderChainResult(data) {
  719. const section = document.getElementById('debugResultSection');
  720. const overviewCards = document.getElementById('debugOverviewCards');
  721. const chainFlow = document.getElementById('chainFlow');
  722. const outputArea = document.getElementById('debugOutputArea');
  723. section.style.display = 'block';
  724. chainFlow.style.display = 'block';
  725. // 总体统计
  726. const totalTime = data.execution_time || 0;
  727. const hasError = data.status === 'error';
  728. const steps = data.steps || {};
  729. const stepCount = Object.keys(steps).length;
  730. overviewCards.innerHTML = `
  731. <div class="overview-card">
  732. <div class="card-icon">🔗</div>
  733. <div class="card-content">
  734. <span class="card-label">链式执行</span>
  735. <span class="card-value">${stepCount} 个环节</span>
  736. </div>
  737. </div>
  738. <div class="overview-card">
  739. <div class="card-icon">⏱️</div>
  740. <div class="card-content">
  741. <span class="card-label">总耗时</span>
  742. <span class="card-value">${totalTime} 秒</span>
  743. </div>
  744. </div>
  745. <div class="overview-card">
  746. <div class="card-icon">${hasError ? '❌' : '✅'}</div>
  747. <div class="card-content">
  748. <span class="card-label">状态</span>
  749. <span class="card-value" style="color: ${hasError ? '#ff5555' : '#00ff88'}">${hasError ? data.error || '失败' : '成功'}</span>
  750. </div>
  751. </div>
  752. `;
  753. // 链式流程图
  754. const stepNames = ['query_extract', 'entity_enhance', 'parent_doc_enhance', 'extract_results'];
  755. let flowHtml = '<div class="chain-flow-container">';
  756. stepNames.forEach((sn, i) => {
  757. const stepData = steps[sn];
  758. const meta = stepMeta[sn] || { name: sn, icon: '📦' };
  759. const statusClass = stepData ? (stepData.status === 'success' ? 'chain-step-success' : 'chain-step-error') : 'chain-step-pending';
  760. const statusIcon = stepData ? (stepData.status === 'success' ? '✅' : '❌') : '⏳';
  761. const timeStr = stepData ? `${stepData.execution_time}s` : '-';
  762. const summary = stepData ? (stepData.summary || '') : '未执行';
  763. flowHtml += `
  764. <div class="chain-step ${statusClass}">
  765. <div class="chain-step-header">
  766. <span class="chain-step-icon">${meta.icon}</span>
  767. <span class="chain-step-status">${statusIcon}</span>
  768. </div>
  769. <div class="chain-step-name">${meta.name}</div>
  770. <div class="chain-step-time">${timeStr}</div>
  771. <div class="chain-step-summary">${escapeHtml(summary)}</div>
  772. </div>`;
  773. if (i < stepNames.length - 1) {
  774. flowHtml += '<div class="chain-arrow">→</div>';
  775. }
  776. });
  777. flowHtml += '</div>';
  778. chainFlow.innerHTML = flowHtml;
  779. // 最后一步(extract_results)的详情
  780. const lastStep = steps['extract_results'];
  781. let outputHtml = '';
  782. if (lastStep && lastStep.status === 'success' && lastStep.output) {
  783. outputHtml = '<div class="debug-output-box"><h4>📤 最终输出 (extract_results)</h4>';
  784. outputHtml += renderExtractResultsTable(lastStep.output);
  785. outputHtml += '</div>';
  786. }
  787. // 各步骤折叠详情
  788. outputHtml += '<div class="debug-output-box"><h4>📋 各环节详情</h4><div class="accordion">';
  789. stepNames.forEach(sn => {
  790. const stepData = steps[sn];
  791. if (!stepData) return;
  792. const meta = stepMeta[sn] || { name: sn, icon: '📦' };
  793. const statusClass = stepData.status === 'success' ? 'status-success' : 'status-error';
  794. outputHtml += `
  795. <div class="accordion-item">
  796. <div class="accordion-header" onclick="toggleAccordion(this)">
  797. <div class="accordion-title">
  798. <span>${meta.icon}</span>
  799. <span>${meta.name}</span>
  800. <span class="status-badge ${statusClass}">${stepData.status}</span>
  801. </div>
  802. <span class="accordion-icon">▼</span>
  803. </div>
  804. <div class="accordion-content">
  805. <div class="debug-step-detail">
  806. <div class="data-section"><h4>摘要</h4>
  807. <pre class="json-viewer">${stepData.summary || '无'}</pre>
  808. </div>`;
  809. if (stepData.output) {
  810. outputHtml += `<div class="data-section"><h4>输出数据</h4>
  811. <pre class="json-viewer">${formatJson(stepData.output)}</pre></div>`;
  812. }
  813. if (stepData.error) {
  814. outputHtml += `<div class="data-section"><h4>错误</h4>
  815. <pre class="json-viewer" style="color:#ff5555">${escapeHtml(stepData.error)}</pre></div>`;
  816. }
  817. outputHtml += '</div></div></div>';
  818. });
  819. outputHtml += '</div></div>';
  820. outputArea.innerHTML = outputHtml;
  821. }
  822. // ==================== 差异化渲染函数 ====================
  823. function renderQueryExtractTable(queryPairs) {
  824. if (!queryPairs || queryPairs.length === 0) {
  825. return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>未提取到查询对</p></div>';
  826. }
  827. let html = '<div class="debug-table-wrap"><table class="debug-table"><thead><tr>' +
  828. '<th>#</th><th>实体 (entity)</th><th>搜索关键词</th><th>背景 (background)</th><th>参数 (parameter)</th></tr></thead><tbody>';
  829. queryPairs.forEach((qp, i) => {
  830. const keywords = Array.isArray(qp.search_keywords) ? qp.search_keywords.join(', ') : (qp.search_keywords || '');
  831. html += `<tr>
  832. <td>${i + 1}</td>
  833. <td><strong>${escapeHtml(qp.entity || '')}</strong></td>
  834. <td>${escapeHtml(keywords)}</td>
  835. <td>${escapeHtml(qp.background || '')}</td>
  836. <td>${escapeHtml(qp.parameter || '')}</td>
  837. </tr>`;
  838. });
  839. html += '</tbody></table></div>';
  840. return html;
  841. }
  842. function renderSearchResultCards(results, stepName) {
  843. if (!results || results.length === 0) {
  844. return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>未检索到结果</p></div>';
  845. }
  846. let html = `<div class="debug-result-count">共 ${results.length} 个结果</div><div class="debug-result-cards">`;
  847. results.forEach((r, i) => {
  848. const textContent = (r.text_content || r.text || '').substring(0, 300);
  849. const fileName = r.file_name || r.metadata?.file_name || r.metadata?.document_id || 'N/A';
  850. const score = r.rerank_score || r.hybrid_similarity || r.bfp_rerank_score || r.distance || 0;
  851. const scoreType = r.rerank_score ? 'rerank' : (r.hybrid_similarity ? 'hybrid' : (r.bfp_rerank_score ? 'bfp' : 'score'));
  852. html += `
  853. <div class="debug-result-card">
  854. <div class="drc-header">
  855. <span class="drc-index">#${i + 1}</span>
  856. <span class="drc-score">${scoreType}: ${typeof score === 'number' ? score.toFixed(4) : score}</span>
  857. </div>
  858. <div class="drc-file">📄 ${escapeHtml(String(fileName))}</div>
  859. <div class="drc-content">${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}</div>
  860. </div>`;
  861. });
  862. html += '</div>';
  863. return html;
  864. }
  865. function renderParentDocSummary(output) {
  866. let html = '<div class="debug-summary-grid">';
  867. for (const [k, v] of Object.entries(output)) {
  868. html += `<div class="debug-summary-item"><span class="debug-summary-key">${k}</span><span class="debug-summary-val">${v}</span></div>`;
  869. }
  870. html += '</div>';
  871. if (output.parent_docs && output.parent_docs.length > 0) {
  872. html += `<div class="debug-result-count">父文档列表 (${output.parent_docs.length})</div><div class="debug-result-cards">`;
  873. output.parent_docs.forEach((p, i) => {
  874. const textContent = (p.text_content || '').substring(0, 300);
  875. const fileName = p.metadata?.file_name || p.metadata?.document_id || 'N/A';
  876. html += `
  877. <div class="debug-result-card parent-doc">
  878. <div class="drc-header">
  879. <span class="drc-index">📚 父文档 #${i + 1}</span>
  880. </div>
  881. <div class="drc-file">📄 ${escapeHtml(String(fileName))}</div>
  882. <div class="drc-content">${escapeHtml(textContent)}${textContent.length >= 300 ? '...' : ''}</div>
  883. </div>`;
  884. });
  885. html += '</div>';
  886. }
  887. return html;
  888. }
  889. function renderExtractResultsTable(entityResults) {
  890. if (!entityResults || entityResults.length === 0) {
  891. return '<div class="empty-state"><div class="empty-state-icon">📭</div><p>没有通过阈值过滤的结果</p></div>';
  892. }
  893. let html = '<div class="debug-table-wrap"><table class="debug-table"><thead><tr>' +
  894. '<th>#</th><th>实体</th><th>combined_query</th><th>分数</th><th>文件名</th><th>内容预览</th></tr></thead><tbody>';
  895. entityResults.forEach((r, i) => {
  896. const score = r.final_score || r.bfp_rerank_score || 0;
  897. const content = (r.text_content || '').substring(0, 150);
  898. html += `<tr>
  899. <td>${i + 1}</td>
  900. <td><strong>${escapeHtml(r.entity || '')}</strong></td>
  901. <td>${escapeHtml((r.combined_query || '').substring(0, 80))}</td>
  902. <td><span class="score-badge">${typeof score === 'number' ? score.toFixed(4) : score}</span></td>
  903. <td>${escapeHtml((r.file_name || '').substring(0, 50))}</td>
  904. <td class="preview-cell">${escapeHtml(content)}${content.length >= 150 ? '...' : ''}</td>
  905. </tr>`;
  906. });
  907. html += '</tbody></table></div>';
  908. return html;
  909. }
  910. // ==================== 工具函数 ====================
  911. function escapeHtml(str) {
  912. if (!str) return '';
  913. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  914. }