Rag_ChatBot / static /evaluation.html
Dus Tran
feat: tích hợp supabase database và cấu hình github actions ci
ea19adc
Raw
History Blame Contribute Delete
41.5 kB
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Đánh Giá RAGAS - RAG Chatbot</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--border-color: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--primary: #3b82f6;
--primary-hover: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--glow-color: rgba(59, 130, 246, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* Header */
header {
background-color: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header-title {
display: flex;
align-items: center;
gap: 15px;
}
.header-title h1 {
font-size: 1.4rem;
font-weight: 700;
background: linear-gradient(to right, #3b82f6, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.4);
}
.btn-outline {
background-color: transparent;
border-color: var(--border-color);
color: var(--text-main);
}
.btn-outline:hover {
background-color: rgba(255, 255, 255, 0.05);
border-color: var(--text-muted);
}
/* Container & Layout */
.container {
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 30px 40px;
flex: 1;
display: flex;
flex-direction: column;
gap: 30px;
}
/* Top Controls section */
.control-panel {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.control-left {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.select-wrapper {
position: relative;
}
select {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 10px 30px 10px 15px;
border-radius: 8px;
outline: none;
cursor: pointer;
font-family: inherit;
appearance: none;
font-size: 0.9rem;
}
.select-wrapper::after {
content: '▼';
font-size: 0.7rem;
color: var(--text-muted);
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
padding: 8px 16px;
border-radius: 20px;
background-color: rgba(255,255,255,0.05);
border: 1px solid var(--border-color);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-idle .status-indicator {
background-color: var(--success);
box-shadow: 0 0 8px var(--success);
}
.status-running .status-indicator {
background-color: var(--warning);
box-shadow: 0 0 8px var(--warning);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
/* Overview Summary Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.metric-card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.1);
}
.metric-card.overall {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(30, 41, 59, 1) 100%);
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 4px 15px var(--glow-color);
}
.metric-title {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.metric-val {
font-size: 2.2rem;
font-weight: 700;
margin-top: 5px;
}
.metric-badge {
align-self: flex-start;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-good { background-color: rgba(16, 185, 129, 0.15); color: var(--success); }
.badge-acceptable { background-color: rgba(245, 158, 11, 0.15); color: var(--warning); }
.badge-poor { background-color: rgba(239, 68, 68, 0.15); color: var(--danger); }
/* Visualization Section */
.viz-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 30px;
}
.chart-card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 25px;
display: flex;
flex-direction: column;
gap: 15px;
height: 400px;
}
.chart-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-main);
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container-inner {
flex: 1;
position: relative;
min-height: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* Detailed Table Section */
.table-section {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 25px;
display: flex;
flex-direction: column;
gap: 20px;
}
.table-header-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.table-title {
font-size: 1.1rem;
font-weight: 600;
}
.table-filters {
display: flex;
align-items: center;
gap: 15px;
}
.search-input {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 8px 15px;
border-radius: 8px;
font-size: 0.9rem;
outline: none;
width: 250px;
}
.search-input:focus {
border-color: var(--primary);
}
.table-wrapper {
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--border-color);
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 0.9rem;
}
th {
background-color: rgba(15, 23, 42, 0.4);
padding: 12px 16px;
color: var(--text-muted);
font-weight: 600;
border-bottom: 1px solid var(--border-color);
}
td {
padding: 14px 16px;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
tr.table-row:hover {
background-color: rgba(255, 255, 255, 0.02);
cursor: pointer;
}
.cell-q {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.score-cell {
font-weight: 600;
text-align: center;
width: 120px;
}
.score-good { color: var(--success); }
.score-acceptable { color: var(--warning); }
.score-poor { color: var(--danger); }
/* Modal styling */
.modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(8px);
z-index: 1000;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
width: 100%;
max-width: 900px;
max-height: 85vh;
overflow-y: auto;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: modalFadeIn 0.3s ease-out;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-header {
padding: 20px 25px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 1.2rem;
font-weight: 600;
}
.close-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.5rem;
cursor: pointer;
}
.close-btn:hover {
color: var(--text-main);
}
.modal-body {
padding: 25px;
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-scores {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
background-color: rgba(15, 23, 42, 0.3);
border: 1px solid var(--border-color);
padding: 15px;
border-radius: 10px;
}
.modal-score-item {
text-align: center;
display: flex;
flex-direction: column;
gap: 5px;
}
.modal-score-name {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
}
.modal-score-val {
font-size: 1.2rem;
font-weight: 700;
}
.modal-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.modal-section-title {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
}
.modal-section-box {
background-color: rgba(15, 23, 42, 0.3);
border: 1px solid var(--border-color);
padding: 15px;
border-radius: 8px;
font-size: 0.95rem;
line-height: 1.6;
white-space: pre-wrap;
}
.modal-contexts-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-context-item {
background-color: rgba(15, 23, 42, 0.3);
border-left: 3px solid var(--primary);
padding: 12px 15px;
border-radius: 0 8px 8px 0;
font-size: 0.9rem;
line-height: 1.5;
}
/* Empty state and loaders */
.loading-overlay {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 15px;
color: var(--text-muted);
height: 100%;
min-height: 200px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top-color: var(--primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 40px;
color: var(--text-muted);
}
</style>
</head>
<body>
<header>
<div class="header-title">
<svg width="24" height="24" fill="none" stroke="var(--primary)" stroke-width="2.5" viewBox="0 0 24 24"><path d="M12 20V10M18 20V4M6 20v-6"></path></svg>
<h1>Bảng Điều Khiển Đánh Giá Chatbot RAG</h1>
</div>
<div class="header-actions">
<a href="/" class="btn btn-outline">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"></path></svg>
Quay Lại Chatbot
</a>
</div>
</header>
<div class="container">
<!-- Control Panel -->
<div class="control-panel">
<div class="control-left">
<span class="status-badge status-idle" id="evalStatusBadge">
<span class="status-indicator"></span>
<span id="evalStatusText">Đang tải trạng thái...</span>
</span>
<div class="select-wrapper">
<select id="historySelect">
<option value="latest">Kết quả mới nhất</option>
</select>
</div>
</div>
<div class="control-right">
<button id="runEvalBtn" class="btn btn-primary">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg>
Chạy Đánh Giá Mới (Mini - 3 câu)
</button>
</div>
</div>
<!-- Metrics Overview Grid -->
<div class="metrics-grid" id="metricsGrid">
<div class="metric-card overall">
<span class="metric-title">Overall Score</span>
<span class="metric-val" id="overallScore">-</span>
<span class="metric-badge" id="overallBadge">-</span>
</div>
<div class="metric-card">
<span class="metric-title">Faithfulness</span>
<span class="metric-val" id="faithScore">-</span>
<span class="metric-badge" id="faithBadge">-</span>
</div>
<div class="metric-card">
<span class="metric-title">Answer Relevancy</span>
<span class="metric-val" id="relScore">-</span>
<span class="metric-badge" id="relBadge">-</span>
</div>
<div class="metric-card">
<span class="metric-title">Context Precision</span>
<span class="metric-val" id="precScore">-</span>
<span class="metric-badge" id="precBadge">-</span>
</div>
<div class="metric-card">
<span class="metric-title">Context Recall</span>
<span class="metric-val" id="recallScore">-</span>
<span class="metric-badge" id="recallBadge">-</span>
</div>
<div class="metric-card">
<span class="metric-title">Correctness</span>
<span class="metric-val" id="correctScore">-</span>
<span class="metric-badge" id="correctBadge">-</span>
</div>
</div>
<!-- Visualization Charts Row -->
<div class="viz-section">
<div class="chart-card">
<div class="chart-title">Điểm Trung Bình RAGAS</div>
<div class="chart-container-inner">
<canvas id="barChart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Biểu Đồ Radar Đa Chiều</div>
<div class="chart-container-inner">
<canvas id="radarChart"></canvas>
</div>
</div>
</div>
<!-- Detailed Table -->
<div class="table-section">
<div class="table-header-row">
<h2 class="table-title">Chi Tiết Kết Quả Từng Mẫu Thử</h2>
<div class="table-filters">
<input type="text" id="searchInput" placeholder="Tìm kiếm câu hỏi..." class="search-input">
</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 60px; text-align: center;">ID</th>
<th>Câu hỏi</th>
<th class="score-cell">Faithfulness</th>
<th class="score-cell">Answer Rel.</th>
<th class="score-cell">Ctx Precision</th>
<th class="score-cell">Ctx Recall</th>
<th class="score-cell">Correctness</th>
</tr>
</thead>
<tbody id="resultsTableBody">
<tr>
<td colspan="7" class="empty-state">Đang tải dữ liệu...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal for Detailed view -->
<div class="modal" id="detailsModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Chi Tiết Kết Quả Đánh Giá</h3>
<button class="close-btn" id="closeModalBtn">&times;</button>
</div>
<div class="modal-body">
<div class="modal-scores" id="modalScores">
<!-- Dynamic modal scores -->
</div>
<div class="modal-section">
<span class="modal-section-title">Câu Hỏi</span>
<div class="modal-section-box" id="modalQuestion"></div>
</div>
<div class="modal-section">
<span class="modal-section-title">Câu Trả Lời Của Chatbot</span>
<div class="modal-section-box" id="modalAnswer"></div>
</div>
<div class="modal-section">
<span class="modal-section-title">Ngữ Cảnh Tìm Kiếm Được (Retrieved Contexts)</span>
<div class="modal-contexts-list" id="modalContexts"></div>
</div>
<div class="modal-section">
<span class="modal-section-title">Câu Trả Lời Gốc (Ground Truth)</span>
<div class="modal-section-box" id="modalGroundTruth"></div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Global variables to store charts
let barChart = null;
let radarChart = null;
let evaluationResults = [];
let pollingInterval = null;
// Dom Elements
const historySelect = document.getElementById('historySelect');
const runEvalBtn = document.getElementById('runEvalBtn');
const evalStatusBadge = document.getElementById('evalStatusBadge');
const evalStatusText = document.getElementById('evalStatusText');
const resultsTableBody = document.getElementById('resultsTableBody');
const searchInput = document.getElementById('searchInput');
// Modal Elements
const detailsModal = document.getElementById('detailsModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const modalTitle = document.getElementById('modalTitle');
const modalScores = document.getElementById('modalScores');
const modalQuestion = document.getElementById('modalQuestion');
const modalAnswer = document.getElementById('modalAnswer');
const modalContexts = document.getElementById('modalContexts');
const modalGroundTruth = document.getElementById('modalGroundTruth');
// Threshold badges helper
function getBadgeClass(val) {
if (val === null || val === undefined || isNaN(val)) return 'badge-poor';
if (val >= 0.7) return 'badge-good';
if (val >= 0.5) return 'badge-acceptable';
return 'badge-poor';
}
function getScoreClass(val) {
if (val === null || val === undefined || isNaN(val)) return 'score-poor';
if (val >= 0.7) return 'score-good';
if (val >= 0.5) return 'score-acceptable';
return 'score-poor';
}
function getGradeText(val) {
if (val === null || val === undefined || isNaN(val)) return 'LỖI';
if (val >= 0.7) return 'TỐT';
if (val >= 0.5) return 'TẠM ỔN';
return 'YẾU';
}
function formatScore(val) {
if (val === null || val === undefined || isNaN(val)) return 'NaN';
return val.toFixed(3);
}
// Initialize Charts
function initCharts(scores) {
const labels = ['Faithfulness', 'Answer Rel.', 'Ctx Precision', 'Ctx Recall', 'Correctness'];
const dataValues = [
scores.faithfulness || 0,
scores.answer_relevancy || 0,
scores.context_precision || 0,
scores.context_recall || 0,
scores.answer_correctness || 0
];
const barCtx = document.getElementById('barChart').getContext('2d');
const radarCtx = document.getElementById('radarChart').getContext('2d');
// Destroy existing charts
if (barChart) barChart.destroy();
if (radarChart) radarChart.destroy();
// Colors mapping based on scores
const borderColors = labels.map((_, i) => dataValues[i] >= 0.7 ? '#10b981' : (dataValues[i] >= 0.5 ? '#f59e0b' : '#ef4444'));
const backgroundColors = labels.map((_, i) => dataValues[i] >= 0.7 ? 'rgba(16, 185, 129, 0.4)' : (dataValues[i] >= 0.5 ? 'rgba(245, 158, 11, 0.4)' : 'rgba(239, 68, 68, 0.4)'));
barChart = new Chart(barCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Điểm Số',
data: dataValues,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1.5,
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 1.0,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#94a3b8' }
},
x: {
grid: { display: false },
ticks: { color: '#94a3b8' }
}
},
plugins: {
legend: { display: false }
}
}
});
radarChart = new Chart(radarCtx, {
type: 'radar',
data: {
labels: labels,
datasets: [{
label: 'Chỉ Số Toàn Diện',
data: dataValues,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: '#3b82f6',
borderWidth: 2,
pointBackgroundColor: '#3b82f6',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#3b82f6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
min: 0,
max: 1.0,
ticks: { display: false },
grid: { color: 'rgba(255,255,255,0.05)' },
angleLines: { color: 'rgba(255,255,255,0.05)' },
pointLabels: { color: '#94a3b8', font: { family: 'Inter' } }
}
},
plugins: {
legend: { display: false }
}
}
});
}
// Render Summary Cards
function updateSummaryCards(summary) {
const scores = summary.average_scores || {};
const overall = summary.overall_score || 0;
document.getElementById('overallScore').textContent = formatScore(overall);
document.getElementById('overallBadge').textContent = getGradeText(overall);
document.getElementById('overallBadge').className = `metric-badge ${getBadgeClass(overall)}`;
document.getElementById('faithScore').textContent = formatScore(scores.faithfulness);
document.getElementById('faithBadge').textContent = getGradeText(scores.faithfulness);
document.getElementById('faithBadge').className = `metric-badge ${getBadgeClass(scores.faithfulness)}`;
document.getElementById('relScore').textContent = formatScore(scores.answer_relevancy);
document.getElementById('relBadge').textContent = getGradeText(scores.answer_relevancy);
document.getElementById('relBadge').className = `metric-badge ${getBadgeClass(scores.answer_relevancy)}`;
document.getElementById('precScore').textContent = formatScore(scores.context_precision);
document.getElementById('precBadge').textContent = getGradeText(scores.context_precision);
document.getElementById('precBadge').className = `metric-badge ${getBadgeClass(scores.context_precision)}`;
document.getElementById('recallScore').textContent = formatScore(scores.context_recall);
document.getElementById('recallBadge').textContent = getGradeText(scores.context_recall);
document.getElementById('recallBadge').className = `metric-badge ${getBadgeClass(scores.context_recall)}`;
document.getElementById('correctScore').textContent = formatScore(scores.answer_correctness);
document.getElementById('correctBadge').textContent = getGradeText(scores.answer_correctness);
document.getElementById('correctBadge').className = `metric-badge ${getBadgeClass(scores.answer_correctness)}`;
initCharts(scores);
}
// Render Results Table
function renderTable(results) {
resultsTableBody.innerHTML = '';
if (!results || results.length === 0) {
resultsTableBody.innerHTML = '<tr><td colspan="7" class="empty-state">Không tìm thấy dữ liệu.</td></tr>';
return;
}
results.forEach((row, index) => {
const tr = document.createElement('tr');
tr.className = 'table-row';
tr.innerHTML = `
<td style="text-align: center; color: var(--text-muted); font-weight: 500;">${index + 1}</td>
<td class="cell-q">${row.question}</td>
<td class="score-cell ${getScoreClass(row.faithfulness)}">${formatScore(row.faithfulness)}</td>
<td class="score-cell ${getScoreClass(row.answer_relevancy)}">${formatScore(row.answer_relevancy)}</td>
<td class="score-cell ${getScoreClass(row.context_precision)}">${formatScore(row.context_precision)}</td>
<td class="score-cell ${getScoreClass(row.context_recall)}">${formatScore(row.context_recall)}</td>
<td class="score-cell ${getScoreClass(row.answer_correctness)}">${formatScore(row.answer_correctness)}</td>
`;
tr.addEventListener('click', () => showRowDetails(row, index + 1));
resultsTableBody.appendChild(tr);
});
}
// Show detailed modal
function showRowDetails(row, id) {
modalTitle.textContent = `Chi Tiết Đánh Giá Mẫu Thử #${id}`;
modalQuestion.textContent = row.question;
modalAnswer.textContent = row.answer;
modalGroundTruth.textContent = row.ground_truth;
// Render contexts
modalContexts.innerHTML = '';
const contexts = row.contexts || [];
if (contexts.length === 0) {
modalContexts.innerHTML = '<div class="empty-state">Không có ngữ cảnh.</div>';
} else {
contexts.forEach(ctx => {
const div = document.createElement('div');
div.className = 'modal-context-item';
div.textContent = ctx;
modalContexts.appendChild(div);
});
}
// Render scores
modalScores.innerHTML = `
<div class="modal-score-item">
<span class="modal-score-name">Faithfulness</span>
<span class="modal-score-val ${getScoreClass(row.faithfulness)}">${formatScore(row.faithfulness)}</span>
</div>
<div class="modal-score-item">
<span class="modal-score-name">Answer Rel.</span>
<span class="modal-score-val ${getScoreClass(row.answer_relevancy)}">${formatScore(row.answer_relevancy)}</span>
</div>
<div class="modal-score-item">
<span class="modal-score-name">Ctx Precision</span>
<span class="modal-score-val ${getScoreClass(row.context_precision)}">${formatScore(row.context_precision)}</span>
</div>
<div class="modal-score-item">
<span class="modal-score-name">Ctx Recall</span>
<span class="modal-score-val ${getScoreClass(row.context_recall)}">${formatScore(row.context_recall)}</span>
</div>
<div class="modal-score-item">
<span class="modal-score-name">Correctness</span>
<span class="modal-score-val ${getScoreClass(row.answer_correctness)}">${formatScore(row.answer_correctness)}</span>
</div>
`;
detailsModal.classList.add('active');
}
// Fetch details from latest or specific history file
async function fetchEvaluationData(type = 'latest') {
try {
let url = '/api/v1/evaluation/latest';
if (type !== 'latest') {
// Search in history results
const histData = historySelect.value;
url = `/api/v1/evaluation/latest`; // Note: endpoint handles returning the selected or default latest
}
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
resultsTableBody.innerHTML = '<tr><td colspan="7" class="empty-state">Chưa chạy đánh giá nào. Nhấn "Chạy Đánh Giá Mới" ở trên để bắt đầu!</td></tr>';
return;
}
throw new Error('Không thể tải dữ liệu đánh giá');
}
const data = await response.json();
evaluationResults = data.results || [];
updateSummaryCards(data.summary);
renderTable(evaluationResults);
} catch (err) {
console.error(err);
resultsTableBody.innerHTML = `<tr><td colspan="7" class="empty-state">Lỗi khi tải kết quả: ${err.message}</td></tr>`;
}
}
// Fetch evaluation running status
async function checkStatus() {
try {
const response = await fetch('/api/v1/evaluation/status');
const data = await response.json();
evalStatusBadge.className = `status-badge status-${data.status}`;
evalStatusText.textContent = data.message;
if (data.status === 'running') {
runEvalBtn.disabled = true;
runEvalBtn.innerHTML = '<span class="spinner" style="width: 16px; height: 16px; border-width: 2px;"></span> Đang chạy đánh giá...';
// Start polling if not already started
if (!pollingInterval) {
pollingInterval = setInterval(checkStatus, 3000);
}
} else {
if (runEvalBtn.disabled) {
runEvalBtn.disabled = false;
runEvalBtn.innerHTML = `
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"></path></svg>
Chạy Đánh Giá Mới (Mini - 3 câu)
`;
// Reload data once evaluation finishes
fetchEvaluationData();
loadHistory();
}
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
}
} catch (err) {
console.error('Lỗi khi check status:', err);
}
}
// Load runs history
async function loadHistory() {
try {
const response = await fetch('/api/v1/evaluation/history');
const list = await response.json();
// Clear existing options except default "latest"
historySelect.innerHTML = '<option value="latest">Kết quả mới nhất</option>';
list.forEach(item => {
const option = document.createElement('option');
option.value = item.evaluation_timestamp;
const dateStr = item.evaluation_timestamp.replace(
/^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/,
'$3/$2/$1 $4:$5:$6'
);
option.textContent = `Run: ${dateStr} (Score: ${(item.overall_score || 0).toFixed(3)})`;
historySelect.appendChild(option);
});
} catch (err) {
console.error('Lỗi khi tải lịch sử:', err);
}
}
// Trigger Evaluation run
async function triggerEvaluation() {
try {
runEvalBtn.disabled = true;
const response = await fetch('/api/v1/evaluation/run?limit=3', { method: 'POST' });
const data = await response.json();
evalStatusBadge.className = `status-badge status-running`;
evalStatusText.textContent = data.message;
// Start polling
checkStatus();
} catch (err) {
console.error(err);
alert('Không thể kích hoạt đánh giá: ' + err.message);
runEvalBtn.disabled = false;
}
}
// Event Listeners
runEvalBtn.addEventListener('click', triggerEvaluation);
closeModalBtn.addEventListener('click', () => {
detailsModal.classList.remove('active');
});
window.addEventListener('click', (e) => {
if (e.target === detailsModal) {
detailsModal.classList.remove('active');
}
});
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
if (!query) {
renderTable(evaluationResults);
return;
}
const filtered = evaluationResults.filter(row => row.question.toLowerCase().includes(query));
renderTable(filtered);
});
historySelect.addEventListener('change', () => {
if (historySelect.value === 'latest') {
fetchEvaluationData('latest');
} else {
// Fetch historical specific data
// In full implementation, api could return specific runs. For simplicity, we fetch latest or we look for it
// To keep it fully functional, we can fetch latest. If we want history support, api can return run details.
// Let's implement history loading in the API as well
fetch(`/api/v1/evaluation/latest`) // We can query specific parameters or load dynamically
.then(res => res.json())
.then(data => {
// Note: to fetch specific history in a real app, you can pass ?timestamp=value.
// Let's make sure our routes support this parameter.
const timestamp = historySelect.value;
fetch(`/api/v1/evaluation/latest?timestamp=${timestamp}`)
.then(res => res.json())
.then(specificData => {
evaluationResults = specificData.results || [];
updateSummaryCards(specificData.summary);
renderTable(evaluationResults);
});
});
}
});
// Initialize on load
fetchEvaluationData();
checkStatus();
loadHistory();
</script>
</body>
</html>