rag_eval.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>RAGAS RAG 评估系统</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
  10. <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
  11. <style>
  12. :root {
  13. --primary: #3b82f6;
  14. --primary-dark: #2563eb;
  15. --success: #10b981;
  16. --warning: #f59e0b;
  17. --danger: #ef4444;
  18. --bg: #f8fafc;
  19. --surface: #ffffff;
  20. --text: #0f172a;
  21. --text-muted: #64748b;
  22. --border: #e2e8f0;
  23. --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  24. --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  25. }
  26. * {
  27. margin: 0;
  28. padding: 0;
  29. box-sizing: border-box;
  30. }
  31. body {
  32. font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  33. background: var(--bg);
  34. color: var(--text);
  35. line-height: 1.6;
  36. }
  37. /* Header */
  38. .header {
  39. background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
  40. color: white;
  41. padding: 2rem 0;
  42. box-shadow: var(--shadow-lg);
  43. }
  44. .header-content {
  45. max-width: 1200px;
  46. margin: 0 auto;
  47. padding: 0 2rem;
  48. }
  49. .header h1 {
  50. font-size: 2rem;
  51. font-weight: 700;
  52. margin-bottom: 0.5rem;
  53. }
  54. .header p {
  55. font-size: 1rem;
  56. opacity: 0.9;
  57. }
  58. /* Container */
  59. .container {
  60. max-width: 1200px;
  61. margin: 2rem auto;
  62. padding: 0 2rem;
  63. }
  64. /* Card */
  65. .card {
  66. background: var(--surface);
  67. border-radius: 12px;
  68. padding: 2rem;
  69. box-shadow: var(--shadow);
  70. margin-bottom: 2rem;
  71. }
  72. .card-title {
  73. font-size: 1.25rem;
  74. font-weight: 600;
  75. margin-bottom: 1.5rem;
  76. color: var(--text);
  77. }
  78. /* Upload Section */
  79. .upload-area {
  80. border: 2px dashed var(--border);
  81. border-radius: 12px;
  82. padding: 3rem 2rem;
  83. text-align: center;
  84. background: linear-gradient(to bottom, #ffffff, #f8fafc);
  85. transition: all 0.3s ease;
  86. }
  87. .upload-area:hover {
  88. border-color: var(--primary);
  89. background: linear-gradient(to bottom, #ffffff, #eff6ff);
  90. }
  91. .upload-icon {
  92. font-size: 3rem;
  93. margin-bottom: 1rem;
  94. }
  95. .upload-area input[type="file"] {
  96. display: none;
  97. }
  98. .upload-label {
  99. display: inline-block;
  100. padding: 0.75rem 2rem;
  101. background: var(--primary);
  102. color: white;
  103. border-radius: 8px;
  104. cursor: pointer;
  105. font-weight: 600;
  106. transition: all 0.3s ease;
  107. box-shadow: var(--shadow);
  108. }
  109. .upload-label:hover {
  110. background: var(--primary-dark);
  111. transform: translateY(-2px);
  112. box-shadow: var(--shadow-lg);
  113. }
  114. .upload-hint {
  115. margin-top: 1rem;
  116. color: var(--text-muted);
  117. font-size: 0.875rem;
  118. }
  119. /* Form */
  120. .form-group {
  121. margin-bottom: 1.5rem;
  122. }
  123. .form-label {
  124. display: block;
  125. margin-bottom: 0.5rem;
  126. font-weight: 600;
  127. color: var(--text);
  128. font-size: 0.875rem;
  129. }
  130. .form-input,
  131. .form-select {
  132. width: 100%;
  133. padding: 0.75rem 1rem;
  134. border: 1px solid var(--border);
  135. border-radius: 8px;
  136. font-size: 0.875rem;
  137. font-family: inherit;
  138. transition: all 0.3s ease;
  139. }
  140. .form-input:focus,
  141. .form-select:focus {
  142. outline: none;
  143. border-color: var(--primary);
  144. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  145. }
  146. .form-hint {
  147. margin-top: 0.5rem;
  148. font-size: 0.75rem;
  149. color: var(--text-muted);
  150. }
  151. /* Buttons */
  152. .btn-group {
  153. display: flex;
  154. gap: 1rem;
  155. margin-top: 2rem;
  156. }
  157. .btn {
  158. padding: 0.75rem 1.5rem;
  159. border: none;
  160. border-radius: 8px;
  161. font-weight: 600;
  162. font-size: 0.875rem;
  163. cursor: pointer;
  164. transition: all 0.3s ease;
  165. box-shadow: var(--shadow);
  166. }
  167. .btn-primary {
  168. background: var(--primary);
  169. color: white;
  170. }
  171. .btn-primary:hover:not(:disabled) {
  172. background: var(--primary-dark);
  173. transform: translateY(-2px);
  174. box-shadow: var(--shadow-lg);
  175. }
  176. .btn-secondary {
  177. background: var(--surface);
  178. color: var(--text);
  179. border: 1px solid var(--border);
  180. }
  181. .btn-secondary:hover:not(:disabled) {
  182. background: var(--bg);
  183. }
  184. .btn:disabled {
  185. opacity: 0.5;
  186. cursor: not-allowed;
  187. }
  188. /* Loading */
  189. .loading {
  190. display: none;
  191. text-align: center;
  192. padding: 2rem;
  193. }
  194. .loading.active {
  195. display: block;
  196. }
  197. .spinner {
  198. border: 4px solid var(--border);
  199. border-top: 4px solid var(--primary);
  200. border-radius: 50%;
  201. width: 40px;
  202. height: 40px;
  203. animation: spin 1s linear infinite;
  204. margin: 0 auto 1rem;
  205. }
  206. @keyframes spin {
  207. 0% { transform: rotate(0deg); }
  208. 100% { transform: rotate(360deg); }
  209. }
  210. /* Results */
  211. .results {
  212. display: none;
  213. }
  214. .results.active {
  215. display: block;
  216. }
  217. .metrics-grid {
  218. display: grid;
  219. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  220. gap: 1rem;
  221. margin-bottom: 2rem;
  222. }
  223. .metric-card {
  224. background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
  225. color: white;
  226. padding: 1.5rem;
  227. border-radius: 12px;
  228. box-shadow: var(--shadow);
  229. }
  230. .metric-name {
  231. font-size: 0.875rem;
  232. opacity: 0.9;
  233. margin-bottom: 0.5rem;
  234. }
  235. .metric-value {
  236. font-size: 2rem;
  237. font-weight: 700;
  238. }
  239. .metric-bar {
  240. height: 6px;
  241. background: rgba(255, 255, 255, 0.3);
  242. border-radius: 3px;
  243. margin-top: 0.75rem;
  244. overflow: hidden;
  245. }
  246. .metric-bar-fill {
  247. height: 100%;
  248. background: white;
  249. border-radius: 3px;
  250. transition: width 0.5s ease;
  251. }
  252. /* Chart */
  253. .chart-container {
  254. margin: 2rem 0;
  255. padding: 1.5rem;
  256. background: var(--surface);
  257. border-radius: 12px;
  258. box-shadow: var(--shadow);
  259. position: relative;
  260. height: 400px;
  261. }
  262. .chart-wrapper {
  263. position: relative;
  264. height: 100%;
  265. width: 100%;
  266. }
  267. /* Table */
  268. .table-container {
  269. overflow-x: auto;
  270. margin-top: 2rem;
  271. }
  272. .table {
  273. width: 100%;
  274. border-collapse: collapse;
  275. background: var(--surface);
  276. border-radius: 12px;
  277. overflow: hidden;
  278. box-shadow: var(--shadow);
  279. }
  280. .table th,
  281. .table td {
  282. padding: 1rem;
  283. text-align: left;
  284. border-bottom: 1px solid var(--border);
  285. }
  286. .table th {
  287. background: var(--bg);
  288. font-weight: 600;
  289. color: var(--text);
  290. font-size: 0.875rem;
  291. text-transform: uppercase;
  292. letter-spacing: 0.5px;
  293. }
  294. .table td {
  295. font-size: 0.875rem;
  296. }
  297. .table tr:hover td {
  298. background: var(--bg);
  299. }
  300. .table tr:last-child td {
  301. border-bottom: none;
  302. }
  303. /* Score Badge */
  304. .score {
  305. display: inline-block;
  306. padding: 0.25rem 0.75rem;
  307. border-radius: 6px;
  308. font-weight: 600;
  309. font-size: 0.75rem;
  310. }
  311. .score-high {
  312. background: #d1fae5;
  313. color: #065f46;
  314. }
  315. .score-medium {
  316. background: #fef3c7;
  317. color: #92400e;
  318. }
  319. .score-low {
  320. background: #fee2e2;
  321. color: #991b1b;
  322. }
  323. /* Alert */
  324. .alert {
  325. padding: 1rem;
  326. border-radius: 8px;
  327. margin-bottom: 1rem;
  328. }
  329. .alert-info {
  330. background: #dbeafe;
  331. color: #1e40af;
  332. border-left: 4px solid var(--primary);
  333. }
  334. .alert-success {
  335. background: #d1fae5;
  336. color: #065f46;
  337. border-left: 4px solid var(--success);
  338. }
  339. /* Responsive */
  340. @media (max-width: 768px) {
  341. .header h1 {
  342. font-size: 1.5rem;
  343. }
  344. .container {
  345. padding: 0 1rem;
  346. }
  347. .card {
  348. padding: 1.5rem;
  349. }
  350. .metrics-grid {
  351. grid-template-columns: 1fr;
  352. }
  353. .btn-group {
  354. flex-direction: column;
  355. }
  356. .chart-container {
  357. height: 300px;
  358. }
  359. .table-container {
  360. overflow-x: scroll;
  361. }
  362. }
  363. </style>
  364. </head>
  365. <body>
  366. <!-- Header -->
  367. <div class="header">
  368. <div class="header-content">
  369. <h1>🎯 RAGAS RAG 评估系统</h1>
  370. <p>智能评估 RAG 系统的检索和生成质量</p>
  371. </div>
  372. </div>
  373. <!-- Main Container -->
  374. <div class="container">
  375. <!-- Upload Card -->
  376. <div class="card">
  377. <h2 class="card-title">📤 上传评估数据</h2>
  378. <div class="upload-area">
  379. <div class="upload-icon">📄</div>
  380. <input type="file" id="fileInput" accept=".json">
  381. <label for="fileInput" class="upload-label">选择 JSON 文件</label>
  382. <p class="upload-hint">
  383. 上传包含 <code>question</code> 和 <code>ground_truth</code> 的 JSON 文件<br>
  384. 系统将自动通过 RAG 获取 contexts 和 answer
  385. </p>
  386. </div>
  387. <!-- Configuration -->
  388. <div style="margin-top: 2rem;">
  389. <h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem;">⚙️ RAG 配置</h3>
  390. <div class="form-group">
  391. <label class="form-label">知识库 ID *</label>
  392. <input
  393. type="text"
  394. id="knowledgeIds"
  395. class="form-input"
  396. placeholder="例如: a2963496869283893248 或 id1,id2,id3"
  397. required
  398. >
  399. <p class="form-hint">多个 ID 用英文逗号分隔</p>
  400. </div>
  401. <div class="form-group">
  402. <label class="form-label">嵌入模型</label>
  403. <select id="embeddingId" class="form-select">
  404. <option value="e5" selected>e5 (默认)</option>
  405. <option value="multilingual-e5-large-instruct">multilingual-e5-large-instruct</option>
  406. </select>
  407. <p class="form-hint">选择用于向量检索的嵌入模型</p>
  408. </div>
  409. <div class="form-group">
  410. <label class="form-label">LLM 模型</label>
  411. <select id="model" class="form-select">
  412. <option value="Qwen3-Coder-30B-loft" selected>Qwen3-Coder-30B-loft</option>
  413. <option value="Qwen3-30B">Qwen3-30B</option>
  414. </select>
  415. <p class="form-hint">选择用于生成答案的 LLM 模型</p>
  416. </div>
  417. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
  418. <div class="form-group">
  419. <label class="form-label">温度 (Temperature)</label>
  420. <input
  421. type="number"
  422. id="temperature"
  423. class="form-input"
  424. value="0.6"
  425. min="0"
  426. max="1"
  427. step="0.1"
  428. >
  429. <p class="form-hint">控制生成随机性 (0.0-1.0)</p>
  430. </div>
  431. <div class="form-group">
  432. <label class="form-label">Top P</label>
  433. <input
  434. type="number"
  435. id="topP"
  436. class="form-input"
  437. value="0.7"
  438. min="0"
  439. max="1"
  440. step="0.1"
  441. >
  442. <p class="form-hint">核采样参数 (0.0-1.0)</p>
  443. </div>
  444. </div>
  445. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
  446. <div class="form-group">
  447. <label class="form-label">最大 Token 数</label>
  448. <input
  449. type="number"
  450. id="maxTokens"
  451. class="form-input"
  452. value="4096"
  453. min="512"
  454. max="8192"
  455. step="512"
  456. >
  457. <p class="form-hint">生成答案的最大长度</p>
  458. </div>
  459. <div class="form-group">
  460. <label class="form-label">检索切片数</label>
  461. <input
  462. type="number"
  463. id="sliceCount"
  464. class="form-input"
  465. value="5"
  466. min="1"
  467. max="20"
  468. step="1"
  469. >
  470. <p class="form-hint">每次检索的文档切片数量</p>
  471. </div>
  472. </div>
  473. <div class="form-group">
  474. <label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
  475. <input type="checkbox" id="enableThink" style="width: auto; margin: 0;">
  476. <span>启用思考模式</span>
  477. </label>
  478. <p class="form-hint">启用后 LLM 会先思考再回答(适用于 Qwen3-30B)</p>
  479. </div>
  480. </div>
  481. <!-- Actions -->
  482. <div class="btn-group">
  483. <button id="evaluateBtn" class="btn btn-primary" disabled>
  484. 🚀 开始评估
  485. </button>
  486. <button id="resetBtn" class="btn btn-secondary" disabled>
  487. 🔄 重置
  488. </button>
  489. </div>
  490. </div>
  491. <!-- Loading -->
  492. <div id="loading" class="loading">
  493. <div class="spinner"></div>
  494. <p style="color: var(--text-muted);">正在评估中,请稍候...</p>
  495. </div>
  496. <!-- Results -->
  497. <div id="results" class="results">
  498. <!-- Summary Card -->
  499. <div class="card">
  500. <h2 class="card-title">📊 评估结果</h2>
  501. <div class="alert alert-success" id="successAlert" style="display: none;">
  502. <strong>✅ 评估完成!</strong> 共评估 <span id="totalCount">0</span> 条数据
  503. </div>
  504. <div class="metrics-grid" id="metricsGrid"></div>
  505. <div class="chart-container">
  506. <div class="chart-wrapper">
  507. <canvas id="metricsChart"></canvas>
  508. </div>
  509. </div>
  510. </div>
  511. <!-- Details Card -->
  512. <div class="card">
  513. <h2 class="card-title">📋 详细结果</h2>
  514. <div class="table-container">
  515. <table class="table">
  516. <thead>
  517. <tr>
  518. <th>#</th>
  519. <th>问题</th>
  520. <th>Faithfulness</th>
  521. <th>Answer Correctness</th>
  522. <th>Answer Relevancy</th>
  523. <th>Context Precision</th>
  524. <th>Context Recall</th>
  525. </tr>
  526. </thead>
  527. <tbody id="resultsTable"></tbody>
  528. </table>
  529. </div>
  530. </div>
  531. </div>
  532. </div>
  533. <script>
  534. // DOM Elements
  535. const fileInput = document.getElementById('fileInput');
  536. const knowledgeIdsInput = document.getElementById('knowledgeIds');
  537. const embeddingIdSelect = document.getElementById('embeddingId');
  538. const modelSelect = document.getElementById('model');
  539. const temperatureInput = document.getElementById('temperature');
  540. const topPInput = document.getElementById('topP');
  541. const maxTokensInput = document.getElementById('maxTokens');
  542. const sliceCountInput = document.getElementById('sliceCount');
  543. const enableThinkCheckbox = document.getElementById('enableThink');
  544. const evaluateBtn = document.getElementById('evaluateBtn');
  545. const resetBtn = document.getElementById('resetBtn');
  546. const loading = document.getElementById('loading');
  547. const results = document.getElementById('results');
  548. const metricsGrid = document.getElementById('metricsGrid');
  549. const resultsTable = document.getElementById('resultsTable');
  550. const successAlert = document.getElementById('successAlert');
  551. const totalCount = document.getElementById('totalCount');
  552. let chartInstance = null;
  553. // File Input Handler
  554. fileInput.addEventListener('change', () => {
  555. const hasFile = fileInput.files && fileInput.files.length > 0;
  556. evaluateBtn.disabled = !hasFile;
  557. resetBtn.disabled = !hasFile;
  558. });
  559. // Reset Handler
  560. resetBtn.addEventListener('click', () => {
  561. fileInput.value = '';
  562. evaluateBtn.disabled = true;
  563. resetBtn.disabled = true;
  564. results.classList.remove('active');
  565. metricsGrid.innerHTML = '';
  566. resultsTable.innerHTML = '';
  567. if (chartInstance) {
  568. chartInstance.destroy();
  569. chartInstance = null;
  570. }
  571. });
  572. // Evaluate Handler
  573. evaluateBtn.addEventListener('click', async () => {
  574. if (!fileInput.files || fileInput.files.length === 0) {
  575. alert('请选择文件');
  576. return;
  577. }
  578. const knowledgeIds = knowledgeIdsInput.value.trim();
  579. if (!knowledgeIds) {
  580. alert('请输入知识库 ID');
  581. knowledgeIdsInput.focus();
  582. return;
  583. }
  584. evaluateBtn.disabled = true;
  585. resetBtn.disabled = true;
  586. loading.classList.add('active');
  587. results.classList.remove('active');
  588. try {
  589. const formData = new FormData();
  590. formData.append('file', fileInput.files[0]);
  591. formData.append('knowledge_ids', knowledgeIds);
  592. formData.append('embedding_id', embeddingIdSelect.value);
  593. formData.append('model', modelSelect.value);
  594. formData.append('temperature', parseFloat(temperatureInput.value));
  595. formData.append('top_p', parseFloat(topPInput.value));
  596. formData.append('max_tokens', parseInt(maxTokensInput.value));
  597. formData.append('slice_count', parseInt(sliceCountInput.value));
  598. formData.append('enable_think', enableThinkCheckbox.checked);
  599. const response = await fetch('/api/evaluate', {
  600. method: 'POST',
  601. body: formData
  602. });
  603. if (!response.ok) {
  604. const error = await response.json();
  605. throw new Error(error.detail || '评估失败');
  606. }
  607. const data = await response.json();
  608. renderResults(data);
  609. } catch (error) {
  610. alert('评估失败: ' + error.message);
  611. console.error(error);
  612. } finally {
  613. loading.classList.remove('active');
  614. evaluateBtn.disabled = false;
  615. resetBtn.disabled = false;
  616. }
  617. });
  618. // Render Results
  619. function renderResults(data) {
  620. results.classList.add('active');
  621. successAlert.style.display = 'block';
  622. totalCount.textContent = data.count;
  623. // Render Metrics
  624. metricsGrid.innerHTML = '';
  625. const summary = data.summary || {};
  626. Object.keys(summary).forEach(key => {
  627. const value = summary[key];
  628. const percent = Math.round((value || 0) * 100);
  629. const card = document.createElement('div');
  630. card.className = 'metric-card';
  631. card.innerHTML = `
  632. <div class="metric-name">${formatMetricName(key)}</div>
  633. <div class="metric-value">${percent}%</div>
  634. <div class="metric-bar">
  635. <div class="metric-bar-fill" style="width: ${percent}%"></div>
  636. </div>
  637. `;
  638. metricsGrid.appendChild(card);
  639. });
  640. // Render Chart
  641. renderChart(summary);
  642. // Render Table
  643. renderTable(data.rows || []);
  644. }
  645. // Render Chart
  646. function renderChart(summary) {
  647. const ctx = document.getElementById('metricsChart');
  648. const labels = Object.keys(summary).map(formatMetricName);
  649. const values = Object.values(summary).map(v => Math.round((v || 0) * 100));
  650. if (chartInstance) {
  651. chartInstance.destroy();
  652. }
  653. chartInstance = new Chart(ctx, {
  654. type: 'bar',
  655. data: {
  656. labels: labels,
  657. datasets: [{
  658. label: '评估分数 (%)',
  659. data: values,
  660. backgroundColor: [
  661. 'rgba(59, 130, 246, 0.8)',
  662. 'rgba(16, 185, 129, 0.8)',
  663. 'rgba(245, 158, 11, 0.8)',
  664. 'rgba(139, 92, 246, 0.8)',
  665. 'rgba(236, 72, 153, 0.8)'
  666. ],
  667. borderRadius: 8,
  668. }]
  669. },
  670. options: {
  671. responsive: true,
  672. maintainAspectRatio: false,
  673. plugins: {
  674. legend: {
  675. display: false
  676. }
  677. },
  678. scales: {
  679. y: {
  680. beginAtZero: true,
  681. max: 100,
  682. ticks: {
  683. callback: value => value + '%'
  684. }
  685. }
  686. }
  687. }
  688. });
  689. }
  690. // Render Table
  691. function renderTable(rows) {
  692. resultsTable.innerHTML = '';
  693. rows.forEach((row, index) => {
  694. const tr = document.createElement('tr');
  695. tr.innerHTML = `
  696. <td>${index + 1}</td>
  697. <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${row.question}">${row.question}</td>
  698. <td>${formatScore(row.faithfulness)}</td>
  699. <td>${formatScore(row.answer_correctness)}</td>
  700. <td>${formatScore(row.answer_relevancy)}</td>
  701. <td>${formatScore(row.context_precision)}</td>
  702. <td>${formatScore(row.context_recall)}</td>
  703. `;
  704. resultsTable.appendChild(tr);
  705. });
  706. }
  707. // Format Score
  708. function formatScore(value) {
  709. if (value === null || value === undefined || isNaN(value)) {
  710. return '<span class="score score-low">N/A</span>';
  711. }
  712. const percent = Math.round(value * 100);
  713. let className = 'score-low';
  714. if (percent >= 80) className = 'score-high';
  715. else if (percent >= 60) className = 'score-medium';
  716. return `<span class="score ${className}">${percent}%</span>`;
  717. }
  718. // Format Metric Name
  719. function formatMetricName(name) {
  720. const names = {
  721. 'faithfulness': 'Faithfulness',
  722. 'answer_correctness': 'Answer Correctness',
  723. 'answer_relevancy': 'Answer Relevancy',
  724. 'context_precision': 'Context Precision',
  725. 'context_recall': 'Context Recall'
  726. };
  727. return names[name] || name;
  728. }
  729. </script>
  730. </body>
  731. </html>