index.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 评估面板</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;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. --bg: #f6f7fb; /* light background */
  14. --panel: #ffffff; /* white panel */
  15. --card: #ffffff; /* white card */
  16. --muted: #64748b; /* slate-500 */
  17. --text: #0f172a; /* slate-900 */
  18. --accent: #3b82f6; /* blue-500 */
  19. --accent-2: #6366f1; /* indigo-500 */
  20. --ok: #16a34a; /* green-600 */
  21. --warn: #d97706; /* amber-600 */
  22. --bad: #dc2626; /* red-600 */
  23. --border: #e5e7eb; /* gray-200 */
  24. --shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
  25. }
  26. * { box-sizing: border-box; }
  27. body {
  28. margin: 0;
  29. font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  30. background: var(--bg);
  31. color: var(--text);
  32. }
  33. header {
  34. padding: 24px 32px;
  35. border-bottom: 1px solid var(--border);
  36. background: rgba(255,255,255,0.8);
  37. backdrop-filter: blur(6px);
  38. position: sticky;
  39. top: 0; z-index: 10;
  40. }
  41. header h1 { margin: 0; font-size: 22px; letter-spacing: 0.4px; font-weight: 700; }
  42. header p { margin: 6px 0 0; color: var(--muted); font-size: 13px; }
  43. .container { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
  44. .panel {
  45. background: var(--panel);
  46. border: 1px solid var(--border);
  47. border-radius: 16px; padding: 20px; box-shadow: var(--shadow);
  48. }
  49. .uploader {
  50. border: 2px dashed var(--border); border-radius: 16px; padding: 24px; text-align: center;
  51. background: linear-gradient(180deg, #ffffff, #f8fafc);
  52. }
  53. .uploader:hover { border-color: var(--accent); }
  54. .uploader input { display: none; }
  55. .uploader label { display: inline-block; padding: 10px 16px; background: var(--accent); color: #ffffff; border-radius: 10px; cursor: pointer; font-weight: 600; box-shadow: 0 6px 16px rgba(59,130,246,0.25); }
  56. .uploader small { display: block; margin-top: 10px; color: var(--muted); }
  57. .actions { margin-top: 12px; display: flex; gap: 12px; justify-content: center; }
  58. .btn {
  59. padding: 10px 14px; border-radius: 10px; border: 1px solid var(--border); background: #ffffff; color: var(--text);
  60. cursor: pointer; font-weight: 600; box-shadow: var(--shadow);
  61. }
  62. .btn.primary { background: var(--accent); color: #ffffff; border-color: transparent; }
  63. .btn:disabled { opacity: 0.6; cursor: not-allowed; }
  64. .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px; }
  65. .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px; box-shadow: var(--shadow); }
  66. .metric { display: flex; justify-content: space-between; align-items: baseline; }
  67. .metric h3 { margin: 0; font-size: 14px; color: var(--muted); }
  68. .metric .value { font-size: 20px; font-weight: 700; color: var(--accent-2); }
  69. .bar { height: 8px; background: #f1f5f9; border-radius: 999px; overflow: hidden; margin-top: 8px; border: 1px solid var(--border); }
  70. .bar > span { display: block; height: 100%; background: linear-gradient(90deg, #3b82f6, #22c55e); }
  71. .section-title { margin: 22px 0 10px; font-size: 15px; color: var(--muted); letter-spacing: .2px; }
  72. .table { width: 100%; border-collapse: collapse; background: #ffffff; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); }
  73. .table th, .table td { border-bottom: 1px solid var(--border); padding: 10px 12px; font-size: 13px; }
  74. .table th { text-align: left; color: var(--muted); font-weight: 600; background: #f8fafc; }
  75. .table tr:hover td { background: #f8fafc; }
  76. .muted { color: var(--muted); }
  77. .note { font-size: 12px; color: var(--muted); margin-top: 8px; }
  78. </style>
  79. </head>
  80. <body>
  81. <header>
  82. <h1>RAGAS 评估面板</h1>
  83. <p>上传包含 question/answer/contexts/ground_truth 的 JSON,查看评估结果</p>
  84. </header>
  85. <div class="container">
  86. <div class="panel">
  87. <div class="uploader" id="uploader">
  88. <input type="file" id="file" accept="application/json,.json" />
  89. <label for="file">选择 JSON 文件</label>
  90. <small>示例结构是一个数组,每条记录包含 question、answer、contexts、ground_truth(可选)</small>
  91. <div class="actions">
  92. <button class="btn primary" id="runBtn" disabled>开始评估</button>
  93. <button class="btn" id="resetBtn" disabled>重置</button>
  94. </div>
  95. </div>
  96. <div id="result" style="display:none;">
  97. <h2 class="section-title">总体指标</h2>
  98. <div class="grid" id="summaryGrid"></div>
  99. <canvas id="summaryChart" style="margin-top: 10px; background:#ffffff; border: 1px solid var(--border); border-radius:12px; padding:8px; box-shadow: var(--shadow);"></canvas>
  100. <h2 class="section-title">逐条详单</h2>
  101. <table class="table" id="rowsTable">
  102. <thead>
  103. <tr>
  104. <th>#</th>
  105. <th>question</th>
  106. <th>faithfulness</th>
  107. <th>answer_correctness</th>
  108. <th>answer_relevancy</th>
  109. <th>context_precision</th>
  110. <th>context_recall</th>
  111. </tr>
  112. </thead>
  113. <tbody></tbody>
  114. </table>
  115. <div class="note" id="note"></div>
  116. </div>
  117. </div>
  118. </div>
  119. <script>
  120. const fileInput = document.getElementById('file');
  121. const runBtn = document.getElementById('runBtn');
  122. const resetBtn = document.getElementById('resetBtn');
  123. const resultWrap = document.getElementById('result');
  124. const summaryGrid = document.getElementById('summaryGrid');
  125. const rowsTableBody = document.querySelector('#rowsTable tbody');
  126. const note = document.getElementById('note');
  127. let chartInstance = null;
  128. fileInput.addEventListener('change', () => {
  129. const hasFile = fileInput.files && fileInput.files.length > 0;
  130. runBtn.disabled = !hasFile;
  131. resetBtn.disabled = !hasFile;
  132. });
  133. resetBtn.addEventListener('click', () => {
  134. fileInput.value = '';
  135. runBtn.disabled = true;
  136. resetBtn.disabled = true;
  137. resultWrap.style.display = 'none';
  138. summaryGrid.innerHTML = '';
  139. rowsTableBody.innerHTML = '';
  140. note.textContent = '';
  141. if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
  142. });
  143. runBtn.addEventListener('click', async () => {
  144. if (!fileInput.files || fileInput.files.length === 0) return;
  145. runBtn.disabled = true;
  146. runBtn.textContent = '评估中...';
  147. try {
  148. const fd = new FormData();
  149. fd.append('file', fileInput.files[0]);
  150. const resp = await fetch('/api/evaluate', { method: 'POST', body: fd });
  151. if (!resp.ok) {
  152. const err = await resp.json().catch(() => ({}));
  153. throw new Error(err.detail || '评估请求失败');
  154. }
  155. const data = await resp.json();
  156. renderResult(data);
  157. } catch (e) {
  158. alert(e.message || e);
  159. } finally {
  160. runBtn.textContent = '开始评估';
  161. runBtn.disabled = false;
  162. }
  163. });
  164. function toPercent(v) { return Math.round((v || 0) * 1000) / 10; }
  165. function renderSummaryCard(name, value) {
  166. const card = document.createElement('div');
  167. card.className = 'card';
  168. const percent = toPercent(value);
  169. card.innerHTML = `
  170. <div class="metric">
  171. <h3>${name}</h3>
  172. <div class="value">${percent}%</div>
  173. </div>
  174. <div class="bar"><span style="width:${percent}%;"></span></div>
  175. `;
  176. summaryGrid.appendChild(card);
  177. }
  178. function renderChart(summary) {
  179. const ctx = document.getElementById('summaryChart');
  180. const labels = Object.keys(summary);
  181. const values = labels.map(k => Math.round((summary[k] || 0) * 1000) / 10);
  182. if (chartInstance) chartInstance.destroy();
  183. chartInstance = new Chart(ctx, {
  184. type: 'bar',
  185. data: {
  186. labels,
  187. datasets: [{
  188. label: '总体指标(%)',
  189. data: values,
  190. backgroundColor: ['#60a5fa','#34d399','#93c5fd','#a78bfa','#fbbf24'],
  191. }]
  192. },
  193. options: {
  194. responsive: true,
  195. scales: {
  196. y: { beginAtZero: true, max: 100, grid: { color: '#e5e7eb' } },
  197. x: { grid: { color: '#f1f5f9' } }
  198. },
  199. plugins: {
  200. legend: { labels: { color: '#0f172a' } }
  201. }
  202. }
  203. });
  204. }
  205. function renderRows(rows) {
  206. rowsTableBody.innerHTML = '';
  207. rows.forEach((r, i) => {
  208. const tr = document.createElement('tr');
  209. const cells = [
  210. i + 1,
  211. r.question || '',
  212. fmt(r.faithfulness),
  213. fmt(r.answer_correctness),
  214. fmt(r.answer_relevancy),
  215. fmt(r.context_precision),
  216. fmt(r.context_recall),
  217. ];
  218. cells.forEach(c => {
  219. const td = document.createElement('td');
  220. td.textContent = c;
  221. tr.appendChild(td);
  222. });
  223. rowsTableBody.appendChild(tr);
  224. });
  225. }
  226. function fmt(v) {
  227. if (v === null || v === undefined || v === '') return '';
  228. return (Math.round(v * 1000) / 10) + '%';
  229. }
  230. function renderResult(data) {
  231. resultWrap.style.display = 'block';
  232. summaryGrid.innerHTML = '';
  233. rowsTableBody.innerHTML = '';
  234. const summary = data.summary || {};
  235. Object.keys(summary).forEach(k => renderSummaryCard(k, summary[k]));
  236. renderChart(summary);
  237. renderRows(data.rows || []);
  238. if (data.skipped_metrics && data.skipped_metrics.length) {
  239. note.textContent = '已跳过指标: ' + data.skipped_metrics.join(', ') + '(缺少 ground_truth)';
  240. } else {
  241. note.textContent = '';
  242. }
  243. }
  244. </script>
  245. </body>
  246. </html>