| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>RAGAS 评估面板</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
- <style>
- :root {
- --bg: #f6f7fb; /* light background */
- --panel: #ffffff; /* white panel */
- --card: #ffffff; /* white card */
- --muted: #64748b; /* slate-500 */
- --text: #0f172a; /* slate-900 */
- --accent: #3b82f6; /* blue-500 */
- --accent-2: #6366f1; /* indigo-500 */
- --ok: #16a34a; /* green-600 */
- --warn: #d97706; /* amber-600 */
- --bad: #dc2626; /* red-600 */
- --border: #e5e7eb; /* gray-200 */
- --shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
- background: var(--bg);
- color: var(--text);
- }
- header {
- padding: 24px 32px;
- border-bottom: 1px solid var(--border);
- background: rgba(255,255,255,0.8);
- backdrop-filter: blur(6px);
- position: sticky;
- top: 0; z-index: 10;
- }
- header h1 { margin: 0; font-size: 22px; letter-spacing: 0.4px; font-weight: 700; }
- header p { margin: 6px 0 0; color: var(--muted); font-size: 13px; }
- .container { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
- .panel {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 16px; padding: 20px; box-shadow: var(--shadow);
- }
- .uploader {
- border: 2px dashed var(--border); border-radius: 16px; padding: 24px; text-align: center;
- background: linear-gradient(180deg, #ffffff, #f8fafc);
- }
- .uploader:hover { border-color: var(--accent); }
- .uploader input { display: none; }
- .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); }
- .uploader small { display: block; margin-top: 10px; color: var(--muted); }
- .actions { margin-top: 12px; display: flex; gap: 12px; justify-content: center; }
- .btn {
- padding: 10px 14px; border-radius: 10px; border: 1px solid var(--border); background: #ffffff; color: var(--text);
- cursor: pointer; font-weight: 600; box-shadow: var(--shadow);
- }
- .btn.primary { background: var(--accent); color: #ffffff; border-color: transparent; }
- .btn:disabled { opacity: 0.6; cursor: not-allowed; }
- .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px; }
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px; box-shadow: var(--shadow); }
- .metric { display: flex; justify-content: space-between; align-items: baseline; }
- .metric h3 { margin: 0; font-size: 14px; color: var(--muted); }
- .metric .value { font-size: 20px; font-weight: 700; color: var(--accent-2); }
- .bar { height: 8px; background: #f1f5f9; border-radius: 999px; overflow: hidden; margin-top: 8px; border: 1px solid var(--border); }
- .bar > span { display: block; height: 100%; background: linear-gradient(90deg, #3b82f6, #22c55e); }
- .section-title { margin: 22px 0 10px; font-size: 15px; color: var(--muted); letter-spacing: .2px; }
- .table { width: 100%; border-collapse: collapse; background: #ffffff; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); }
- .table th, .table td { border-bottom: 1px solid var(--border); padding: 10px 12px; font-size: 13px; }
- .table th { text-align: left; color: var(--muted); font-weight: 600; background: #f8fafc; }
- .table tr:hover td { background: #f8fafc; }
- .muted { color: var(--muted); }
- .note { font-size: 12px; color: var(--muted); margin-top: 8px; }
- </style>
- </head>
- <body>
- <header>
- <h1>RAGAS 评估面板</h1>
- <p>上传包含 question/answer/contexts/ground_truth 的 JSON,查看评估结果</p>
- </header>
- <div class="container">
- <div class="panel">
- <div class="uploader" id="uploader">
- <input type="file" id="file" accept="application/json,.json" />
- <label for="file">选择 JSON 文件</label>
- <small>示例结构是一个数组,每条记录包含 question、answer、contexts、ground_truth(可选)</small>
- <div class="actions">
- <button class="btn primary" id="runBtn" disabled>开始评估</button>
- <button class="btn" id="resetBtn" disabled>重置</button>
- </div>
- </div>
- <div id="result" style="display:none;">
- <h2 class="section-title">总体指标</h2>
- <div class="grid" id="summaryGrid"></div>
- <canvas id="summaryChart" style="margin-top: 10px; background:#ffffff; border: 1px solid var(--border); border-radius:12px; padding:8px; box-shadow: var(--shadow);"></canvas>
- <h2 class="section-title">逐条详单</h2>
- <table class="table" id="rowsTable">
- <thead>
- <tr>
- <th>#</th>
- <th>question</th>
- <th>faithfulness</th>
- <th>answer_correctness</th>
- <th>answer_relevancy</th>
- <th>context_precision</th>
- <th>context_recall</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- <div class="note" id="note"></div>
- </div>
- </div>
- </div>
- <script>
- const fileInput = document.getElementById('file');
- const runBtn = document.getElementById('runBtn');
- const resetBtn = document.getElementById('resetBtn');
- const resultWrap = document.getElementById('result');
- const summaryGrid = document.getElementById('summaryGrid');
- const rowsTableBody = document.querySelector('#rowsTable tbody');
- const note = document.getElementById('note');
- let chartInstance = null;
- fileInput.addEventListener('change', () => {
- const hasFile = fileInput.files && fileInput.files.length > 0;
- runBtn.disabled = !hasFile;
- resetBtn.disabled = !hasFile;
- });
- resetBtn.addEventListener('click', () => {
- fileInput.value = '';
- runBtn.disabled = true;
- resetBtn.disabled = true;
- resultWrap.style.display = 'none';
- summaryGrid.innerHTML = '';
- rowsTableBody.innerHTML = '';
- note.textContent = '';
- if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
- });
- runBtn.addEventListener('click', async () => {
- if (!fileInput.files || fileInput.files.length === 0) return;
- runBtn.disabled = true;
- runBtn.textContent = '评估中...';
- try {
- const fd = new FormData();
- fd.append('file', fileInput.files[0]);
- const resp = await fetch('/api/evaluate', { method: 'POST', body: fd });
- if (!resp.ok) {
- const err = await resp.json().catch(() => ({}));
- throw new Error(err.detail || '评估请求失败');
- }
- const data = await resp.json();
- renderResult(data);
- } catch (e) {
- alert(e.message || e);
- } finally {
- runBtn.textContent = '开始评估';
- runBtn.disabled = false;
- }
- });
- function toPercent(v) { return Math.round((v || 0) * 1000) / 10; }
- function renderSummaryCard(name, value) {
- const card = document.createElement('div');
- card.className = 'card';
- const percent = toPercent(value);
- card.innerHTML = `
- <div class="metric">
- <h3>${name}</h3>
- <div class="value">${percent}%</div>
- </div>
- <div class="bar"><span style="width:${percent}%;"></span></div>
- `;
- summaryGrid.appendChild(card);
- }
- function renderChart(summary) {
- const ctx = document.getElementById('summaryChart');
- const labels = Object.keys(summary);
- const values = labels.map(k => Math.round((summary[k] || 0) * 1000) / 10);
- if (chartInstance) chartInstance.destroy();
- chartInstance = new Chart(ctx, {
- type: 'bar',
- data: {
- labels,
- datasets: [{
- label: '总体指标(%)',
- data: values,
- backgroundColor: ['#60a5fa','#34d399','#93c5fd','#a78bfa','#fbbf24'],
- }]
- },
- options: {
- responsive: true,
- scales: {
- y: { beginAtZero: true, max: 100, grid: { color: '#e5e7eb' } },
- x: { grid: { color: '#f1f5f9' } }
- },
- plugins: {
- legend: { labels: { color: '#0f172a' } }
- }
- }
- });
- }
- function renderRows(rows) {
- rowsTableBody.innerHTML = '';
- rows.forEach((r, i) => {
- const tr = document.createElement('tr');
- const cells = [
- i + 1,
- r.question || '',
- fmt(r.faithfulness),
- fmt(r.answer_correctness),
- fmt(r.answer_relevancy),
- fmt(r.context_precision),
- fmt(r.context_recall),
- ];
- cells.forEach(c => {
- const td = document.createElement('td');
- td.textContent = c;
- tr.appendChild(td);
- });
- rowsTableBody.appendChild(tr);
- });
- }
- function fmt(v) {
- if (v === null || v === undefined || v === '') return '';
- return (Math.round(v * 1000) / 10) + '%';
- }
- function renderResult(data) {
- resultWrap.style.display = 'block';
- summaryGrid.innerHTML = '';
- rowsTableBody.innerHTML = '';
- const summary = data.summary || {};
- Object.keys(summary).forEach(k => renderSummaryCard(k, summary[k]));
- renderChart(summary);
- renderRows(data.rows || []);
- if (data.skipped_metrics && data.skipped_metrics.length) {
- note.textContent = '已跳过指标: ' + data.skipped_metrics.join(', ') + '(缺少 ground_truth)';
- } else {
- note.textContent = '';
- }
- }
- </script>
- </body>
- </html>
|