app.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. }