Camais03's picture
V1.5
29b445b verified
<!DOCTYPE html>
<html>
<head>
<title>Deep Learning Training Monitor</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
<style>
/* Base styles */
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 16px;
background-color: #f5f5f5;
color: #333;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); /* Increased minimum width */
gap: 16px;
margin-bottom: 16px;
}
.chart-container {
position: relative;
width: 100%;
}
.card {
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* Prediction styles */
.predictions-container {
margin-top: 16px;
}
.category-section {
background: white;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.category-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.stats-badges {
display: flex;
gap: 8px;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge-tp {
background-color: #d1fae5;
color: #065f46;
}
.badge-fp {
background-color: #ffedd5;
color: #9a3412;
}
.badge-fn {
background-color: #fee2e2;
color: #991b1b;
}
.tag-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid;
}
.tag-tp {
background-color: #ecfdf5;
border-color: #6ee7b7;
color: #065f46;
}
.tag-fp {
background-color: #fff7ed;
border-color: #fdba74;
color: #9a3412;
}
.tag-fn {
background-color: #fef2f2;
border-color: #fca5a5;
color: #991b1b;
}
.tag-info {
display: flex;
align-items: center;
gap: 12px;
}
.tag-name {
font-weight: 500;
}
.tag-probability {
font-size: 0.9em;
opacity: 0.85;
}
.tag-status {
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
}
.status-tp {
background-color: #d1fae5;
}
.status-fp {
background-color: #ffedd5;
}
.status-fn {
background-color: #fee2e2;
}
.badge {
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
}
.badge-success {
background-color: #d1fae5;
color: #065f46;
}
.badge-warning {
background-color: #ffedd5;
color: #9a3412;
}
.badge-error {
background-color: #fee2e2;
color: #991b1b;
}
.stage-header {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 16px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e5e7eb;
}
.category-section {
margin-bottom: 20px;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.category-stats {
display: flex;
gap: 8px;
}
.selected-tags {
margin-top: 16px;
}
.selected-tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 4px;
margin-bottom: 4px;
}
.ground-truth-marker {
font-weight: bold;
margin-left: 8px;
}
.ground-truth-yes {
color: #059669;
}
.ground-truth-no {
color: #dc2626;
}
.filter-btn {
padding: 4px 8px;
margin: 0 4px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
background: #e5e7eb;
border: none;
}
.filter-btn.active {
background: #3b82f6;
color: white;
}
.category-section {
background: white;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.tag-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
background: #f9fafb;
border: 1px solid #e5e7eb;
}
.tag-item.correct {
background-color: #ecfdf5;
border-color: #6ee7b7;
}
.tag-item.incorrect {
background-color: #fff7ed;
border-color: #fdba74;
}
.tag-item.missing {
background-color: #fef2f2;
border-color: #fca5a5;
}
.probability-bar {
width: 100px;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.probability-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.category-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
background: #f3f4f6;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 12px;
}
.selected-tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
background: #f9fafb;
border: 1px solid #e5e7eb;
}
.selected-tag-item.ground-truth {
background-color: #ecfdf5;
border-color: #6ee7b7;
}
.selected-tag-item.non-ground-truth {
background-color: #fff7ed;
border-color: #fdba74;
}
.tag-confidence {
display: flex;
align-items: center;
gap: 8px;
}
.confidence-bar {
width: 100px;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
transition: width 0.3s ease;
}
.ground-truth .confidence-fill {
background: #059669;
}
.non-ground-truth .confidence-fill {
background: #f97316;
}
</style>
</head>
<body>
<h1>Training Monitor</h1>
<div class="tabs">
<div class="tab active" onclick="showTab('overview')">Overview</div>
<div class="tab" onclick="showTab('predictions')">Predictions</div>
<div class="tab" onclick="showTab('selection')">Selection Analysis</div>
</div>
<div class="card">
<h3>Training Progress</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Current Epoch</div>
<div class="metric-value" id="current-epoch">0/0</div>
</div>
<div class="metric-card">
<div class="metric-label">Batch Progress</div>
<div class="metric-value" id="current-batch">0/0</div>
</div>
<div class="metric-card">
<div class="metric-label">Speed (it/s)</div>
<div class="metric-value" id="iter-speed">0.00</div>
</div>
<div class="metric-card">
<div class="metric-label">Time Left</div>
<div class="metric-value" id="time-remaining">--:--:--</div>
</div>
<div class="metric-card">
<div class="metric-label">Time Elapsed</div>
<div class="metric-value" id="time-elapsed">00:00:00</div>
</div>
<div class="metric-card">
<div class="metric-label">Loss</div>
<div class="metric-value" id="current-loss">0.00000</div>
</div>
<div class="metric-card">
<div class="metric-label">Initial F1</div>
<div class="metric-value" id="current-initial-f1">0.00000</div>
</div>
<div class="metric-card">
<div class="metric-label">Refined F1</div>
<div class="metric-value" id="current-refined-f1">0.00000</div>
</div>
</div>
</div>
<div id="overview">
<div class="dashboard-grid">
<div class="card" style="height: 400px;"> <!-- Increased height -->
<h3>Loss</h3>
<div class="chart-container" style="height: 350px;"> <!-- Increased height -->
<canvas id="loss-chart"></canvas>
</div>
</div>
<div class="card" style="height: 400px;"> <!-- Increased height -->
<h3>F1 Scores</h3>
<div class="chart-container" style="height: 350px;"> <!-- Increased height -->
<canvas id="f1-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- Predictions Section -->
<div id="predictions" class="tab-content">
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<!-- Prediction Controls -->
<div class="card">
<div class="flex justify-between items-center mb-4">
<h3>Predictions</h3>
<div class="prediction-controls">
<button onclick="togglePredictionType()" id="prediction-type-toggle" class="px-3 py-1 bg-blue-500 text-white rounded text-sm">
Show Refined
</button>
</div>
</div>
<!-- Navigation Controls -->
<div class="flex justify-between items-center mb-4">
<button class="px-3 py-1 bg-blue-500 text-white rounded text-sm" onclick="previousPrediction()">Previous</button>
<span id="prediction-counter" class="text-sm font-medium">Sample 0 / 0</span>
<button class="px-3 py-1 bg-blue-500 text-white rounded text-sm" onclick="nextPrediction()">Next</button>
</div>
<!-- Image Display -->
<div class="h-96 flex items-center justify-center bg-gray-100 rounded-lg mb-4">
<img id="current-image" src="" alt="Current sample" class="max-h-full object-contain" />
</div>
</div>
<!-- Ground Truth and Predictions -->
<div class="card">
<div class="flex justify-between items-center mb-4">
<h3>Tag Analysis</h3>
<div class="tag-filters">
<button class="filter-btn active" onclick="filterTags('all')">All</button>
<button class="filter-btn" onclick="filterTags('correct')">Correct</button>
<button class="filter-btn" onclick="filterTags('incorrect')">Incorrect</button>
<button class="filter-btn" onclick="filterTags('missing')">Missing</button>
</div>
</div>
<div id="category-predictions" class="overflow-auto" style="max-height: 600px;">
<!-- Predictions will be inserted here -->
</div>
</div>
</div>
</div>
<!-- Selection Analysis tab content -->
<div id="selection" class="tab-content">
<div class="card mb-4">
<h3>Selection Analysis Metrics</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Total Ground Truth Tags</div>
<div class="metric-value" id="total-gt-tags">0</div>
</div>
<div class="metric-card">
<div class="metric-label">Selected Ground Truth Tags</div>
<div class="metric-value" id="selected-gt-tags">0</div>
</div>
<div class="metric-card">
<div class="metric-label">Ground Truth Recall</div>
<div class="metric-value" id="gt-recall">0.0000</div>
</div>
<div class="metric-card">
<div class="metric-label">Avg Prob (GT)</div>
<div class="metric-value" id="avg-prob-gt">0.0000</div>
</div>
<div class="metric-card">
<div class="metric-label">Avg Prob (Non-GT)</div>
<div class="metric-value" id="avg-prob-non-gt">0.0000</div>
</div>
<div class="metric-card">
<div class="metric-label">Unique Tags Selected</div>
<div class="metric-value" id="unique-tags-selected">0</div>
</div>
</div>
</div>
<div class="card">
<h3>Selection Analysis Graph</h3>
<div class="chart-container" style="height: 400px;">
<canvas id="selection-chart"></canvas>
</div>
</div>
<div class="card">
<h3>Selected Tags Details</h3>
<div class="tag-filters mb-4">
<button class="filter-btn active" onclick="filterSelectedTags('all')">All Selected</button>
<button class="filter-btn" onclick="filterSelectedTags('ground-truth')">Ground Truth</button>
<button class="filter-btn" onclick="filterSelectedTags('non-ground-truth')">Non-Ground Truth</button>
</div>
<div id="selected-tags-list" class="overflow-auto" style="max-height: 500px;">
<!-- Selected tags will be inserted here -->
</div>
</div>
</div>
<script>
const socket = io();
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.style.display = 'none';
});
// Show selected tab
const selectedTab = document.getElementById(tabName);
if (selectedTab) {
selectedTab.style.display = 'block';
}
// Update active state on tab buttons
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
if (tab.textContent.toLowerCase().includes(tabName)) {
tab.classList.add('active');
}
});
}
const charts = {};
const CHART_WINDOW_SIZE = 200; // Increased from 50 to 200
document.addEventListener('DOMContentLoaded', function() {
// Chart initialization function
function initializeCharts() {
const tooltipCallback = (context) => {
let label = context.dataset.label || '';
let value = context.parsed.y;
if (value !== null) {
value = value.toFixed(6); // Increased from 4 to 6
}
return `${label}: ${value}`;
};
// Define shared chart options
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
suggestedMin: -0.2,
suggestedMax: 1.2
}
},
plugins: {
legend: {
position: 'top',
},
tooltip: {
callbacks: {
label: tooltipCallback
}
}
}
};
// Initialize charts using shared options
const chartElements = {
loss: document.getElementById('loss-chart'),
f1: document.getElementById('f1-chart'),
selection: document.getElementById('selection-chart')
};
if (chartElements.loss) {
charts.loss = new Chart(chartElements.loss.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Training Loss',
data: [],
borderColor: '#ff6384',
tension: 0.1
}, {
label: 'Validation Loss',
data: [],
borderColor: '#4bc0c0',
tension: 0.1,
borderDash: [5, 5]
}]
},
options: {
...chartOptions,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
if (chartElements.f1) {
charts.f1 = new Chart(chartElements.f1.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Train Initial F1',
data: [],
borderColor: '#36a2eb',
tension: 0.1
}, {
label: 'Train Refined F1',
data: [],
borderColor: '#4bc0c0',
tension: 0.1
}, {
label: 'Val Initial F1',
data: [],
borderColor: '#ff6384',
tension: 0.1,
borderDash: [5, 5]
}, {
label: 'Val Refined F1',
data: [],
borderColor: '#ffcd56',
tension: 0.1,
borderDash: [5, 5]
}]
},
options: {
...chartOptions,
scales: {
y: {
beginAtZero: true,
max: 1
}
}
}
});
}
if (chartElements.selection) {
charts.selection = new Chart(chartElements.selection.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Ground Truth Recall',
data: [],
borderColor: '#4bc0c0',
tension: 0.1
}, {
label: 'Avg Prob (GT)',
data: [],
borderColor: '#36a2eb',
tension: 0.1
}, {
label: 'Avg Prob (Non-GT)',
data: [],
borderColor: '#ff6384',
tension: 0.1
}, {
label: 'GT/Non-GT Prob Difference',
data: [],
borderColor: '#9966ff',
tension: 0.1,
borderDash: [5, 5]
}]
},
options: chartOptions // Use the shared options directly for selection chart
});
}
}
// Call initializeCharts inside DOMContentLoaded
initializeCharts();
// Show initial tab
showTab('overview');
});
// Prediction history management
let currentPredictionType = 'initial';
let predictionHistory = [];
let currentPredictionIndex = 0;
let currentFilter = 'all';
let currentSelectedFilter = 'all';
// Initialize charts when document is ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize charts
if (document.getElementById('loss-chart')) {
initializeCharts();
}
// Show initial tab
showTab('overview');
});
function denormalizeImage(imageData) {
if (!imageData) return '';
// If imageData is already a complete data URL
if (typeof imageData === 'string' && imageData.startsWith('data:image/')) {
return imageData;
}
// If imageData is just a base64 string without the data URL prefix
if (typeof imageData === 'string') {
return `data:image/jpeg;base64,${imageData}`;
}
console.error('Unsupported image data format:', imageData);
return '';
}
function updateGroundTruthTags(predictionData, filter = 'all') {
const container = document.getElementById('ground-truth-tags');
if (!container || !predictionData || !predictionData.category_predictions) return;
container.innerHTML = '';
// Collect all tags from category predictions
const allTags = [];
Object.entries(predictionData.category_predictions).forEach(([category, data]) => {
// Add true positives
data.true_positives?.forEach(tag => {
allTags.push({...tag, status: 'tp', category});
});
// Add false positives
data.false_positives?.forEach(tag => {
allTags.push({...tag, status: 'fp', category});
});
// Add false negatives (missing)
data.false_negatives?.forEach(tag => {
allTags.push({...tag, status: 'fn', category});
});
});
// Filter tags based on selected filter
const filteredTags = allTags.filter(tag => {
if (filter === 'all') return true;
if (filter === 'true-positive') return tag.status === 'tp';
if (filter === 'false-positive') return tag.status === 'fp';
if (filter === 'missing') return tag.status === 'fn';
return true;
});
// Sort by probability (if available)
filteredTags.sort((a, b) => (b.probability || 0) - (a.probability || 0));
// Render tags
filteredTags.forEach(tag => {
const tagDiv = document.createElement('div');
tagDiv.className = `tag-item tag-${tag.status}`;
const statusText = {
tp: 'TP',
fp: 'FP',
fn: 'Missing'
};
tagDiv.innerHTML = `
<div class="tag-info">
<span class="tag-name">${tag.tag}</span>
<span class="tag-probability">
${tag.status === 'fn' ? 'Missed' : `${(tag.probability * 100).toFixed(1)}%`}
</span>
</div>
<span class="tag-status status-${tag.status}">${statusText[tag.status]}</span>
`;
container.appendChild(tagDiv);
});
}
function togglePredictionType() {
currentPredictionType = currentPredictionType === 'initial' ? 'refined' : 'initial';
const button = document.getElementById('prediction-type-toggle');
if (button) {
button.textContent = currentPredictionType === 'initial' ? 'Show Refined' : 'Show Initial';
}
if (currentPredictionIndex < predictionHistory.length) {
updatePredictionDisplay(predictionHistory[currentPredictionIndex]);
}
}
function updateCategoryPredictions(predictionData) {
const container = document.getElementById('category-predictions');
if (!container || !predictionData) return;
const groupedPredictions = {};
Object.entries(predictionData.category_predictions || {}).forEach(([category, data]) => {
groupedPredictions[category] = {
correct: data.true_positives || [],
incorrect: data.false_positives || [],
missing: data.false_negatives || []
};
});
let html = '';
Object.entries(groupedPredictions).forEach(([category, predictions]) => {
let tagsToShow = [];
if (currentFilter === 'all') {
tagsToShow = [
...predictions.correct.map(t => ({...t, type: 'correct'})),
...predictions.incorrect.map(t => ({...t, type: 'incorrect'})),
...predictions.missing.map(t => ({...t, type: 'missing'}))
];
} else {
tagsToShow = predictions[currentFilter].map(t => ({...t, type: currentFilter}));
}
if (tagsToShow.length > 0) {
html += `
<div class="category-section">
<div class="category-title">${category}</div>
<div class="tag-list">
${tagsToShow.map(tag => `
<div class="tag-item ${tag.type}">
<span class="tag-name">${tag.tag}</span>
<div class="tag-confidence">
<div class="confidence-bar">
<div class="confidence-fill"
style="width: ${tag.probability * 100}%;
background: ${tag.type === 'correct' ? '#059669' :
tag.type === 'incorrect' ? '#f97316' : '#ef4444'}">
</div>
</div>
<span class="confidence-value">${(tag.probability * 100).toFixed(1)}%</span>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
});
container.innerHTML = html;
}
function getTagStatus(prediction) {
if (prediction.probability === 0) return 'missing';
return prediction.correct ? 'correct' : 'incorrect';
}
function filterTags(filter) {
currentFilter = filter;
document.querySelectorAll('.tag-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(filter));
});
if (currentPredictionIndex < predictionHistory.length) {
updatePredictionDisplay(predictionHistory[currentPredictionIndex]);
}
}
function updatePredictionDisplay(predictionData) {
if (!predictionData) return;
// Update image
const imageElement = document.getElementById('current-image');
if (imageElement && predictionData.image) {
imageElement.src = predictionData.image.startsWith('data:') ?
predictionData.image : `data:image/jpeg;base64,${predictionData.image}`;
}
// Update counter
const counterElement = document.getElementById('prediction-counter');
if (counterElement) {
counterElement.textContent = `Sample ${currentPredictionIndex + 1} / ${predictionHistory.length}`;
}
// Update predictions
updateCategoryPredictions(predictionData);
updateSelectedTagsList(predictionData);
}
function previousPrediction() {
if (currentPredictionIndex > 0) {
currentPredictionIndex--;
updatePredictionDisplay(predictionHistory[currentPredictionIndex]);
}
}
function nextPrediction() {
if (currentPredictionIndex < predictionHistory.length - 1) {
currentPredictionIndex++;
updatePredictionDisplay(predictionHistory[currentPredictionIndex]);
}
}
function filterSelectedTags(filter) {
currentSelectedFilter = filter;
document.querySelectorAll('.tag-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(filter));
});
if (currentPredictionIndex < predictionHistory.length) {
updateSelectedTagsList(predictionHistory[currentPredictionIndex]);
}
}
function updateSelectedTagsList(predictionData) {
const container = document.getElementById('selected-tags-list');
if (!container || !predictionData || !predictionData.tag_info) return;
let tagsToShow = predictionData.tag_info;
if (currentSelectedFilter === 'ground-truth') {
tagsToShow = tagsToShow.filter(tag => tag.is_ground_truth);
} else if (currentSelectedFilter === 'non-ground-truth') {
tagsToShow = tagsToShow.filter(tag => !tag.is_ground_truth);
}
// Sort by probability
tagsToShow.sort((a, b) => b.probability - a.probability);
const html = tagsToShow.map(tag => `
<div class="selected-tag-item ${tag.is_ground_truth ? 'ground-truth' : 'non-ground-truth'}">
<div class="tag-info">
<span class="tag-name">${tag.tag_name}</span>
<span class="tag-category text-sm text-gray-500">${tag.category}</span>
</div>
<div class="tag-confidence">
<div class="confidence-bar">
<div class="confidence-fill"
style="width: ${tag.probability * 100}%;
background: ${tag.is_ground_truth ? '#059669' : '#f97316'}">
</div>
</div>
<span class="confidence-value">${(tag.probability * 100).toFixed(1)}%</span>
</div>
</div>
`).join('');
container.innerHTML = html;
}
function formatKey(key) {
return key
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
socket.on('training_update', (data) => {
const update = JSON.parse(data);
const progress = update.progress;
// Format progress displays with more precision
document.getElementById('current-epoch').textContent =
`${progress.epoch + 1}/${progress.total_epochs}`;
document.getElementById('current-batch').textContent =
`${progress.batch}/${progress.total_batches}`;
document.getElementById('iter-speed').textContent =
`${progress.iter_speed.toFixed(2)}`;
document.getElementById('time-remaining').textContent =
formatTime(progress.time_remaining);
document.getElementById('time-elapsed').textContent =
formatTime(progress.elapsed_time);
// Update metrics with higher precision
if (progress.current_metrics) {
document.getElementById('current-loss').textContent =
progress.current_metrics.loss.toFixed(6);
document.getElementById('current-initial-f1').textContent =
progress.current_metrics.initial_f1.toFixed(6);
document.getElementById('current-refined-f1').textContent =
progress.current_metrics.refined_f1.toFixed(6);
}
});
socket.on('metrics_update', (data) => {
try {
const update = JSON.parse(data);
const timestamp = new Date().toLocaleTimeString();
// Handle training and validation metrics
if (update.metrics) {
const train = update.metrics.train;
const val = update.metrics.val;
// Update metrics display
if (train) {
const updateMetric = (id, value, decimals = 6) => { // Increased from 4 to 6
const element = document.getElementById(id);
if (element && value !== undefined) {
element.textContent = typeof value === 'number' ? value.toFixed(decimals) : value;
}
};
updateMetric('current-initial-f1', train.initial_f1);
updateMetric('current-refined-f1', train.refined_f1);
}
// Update loss chart
if (charts.loss) {
charts.loss.data.labels.push(timestamp);
charts.loss.data.datasets[0].data.push(train?.loss || 0);
if (val && val.loss !== null) {
charts.loss.data.datasets[1].data.push(val.loss);
}
if (charts.loss.data.labels.length > CHART_WINDOW_SIZE) {
charts.loss.data.labels.shift();
charts.loss.data.datasets.forEach(dataset => dataset.data.shift());
}
charts.loss.update();
}
// Update F1 chart
if (charts.f1) {
charts.f1.data.labels.push(timestamp);
charts.f1.data.datasets[0].data.push(train?.initial_f1 || 0);
charts.f1.data.datasets[1].data.push(train?.refined_f1 || 0);
if (val && val.initial_f1 !== null) {
charts.f1.data.datasets[2].data.push(val.initial_f1);
charts.f1.data.datasets[3].data.push(val.refined_f1);
}
if (charts.f1.data.labels.length > CHART_WINDOW_SIZE) {
charts.f1.data.labels.shift();
charts.f1.data.datasets.forEach(dataset => dataset.data.shift());
}
charts.f1.update();
}
}
// Handle predictions and selection analysis
if (update.predictions) {
const predictions = update.predictions;
// Handle tag selection analysis
if (predictions.tag_selection) {
const tagSelection = predictions.tag_selection;
const tagInfo = predictions.tag_info || [];
// Count ground truth matches
const groundTruthTags = tagInfo.filter(tag => tag.is_ground_truth);
const selectedGroundTruthTags = groundTruthTags.length;
// Calculate averages
const gtProbs = groundTruthTags.map(tag => tag.probability);
const nonGtProbs = tagInfo.filter(tag => !tag.is_ground_truth).map(tag => tag.probability);
const avgGtProb = gtProbs.length > 0 ? gtProbs.reduce((a, b) => a + b, 0) / gtProbs.length : 0;
const avgNonGtProb = nonGtProbs.length > 0 ? nonGtProbs.reduce((a, b) => a + b, 0) / nonGtProbs.length : 0;
const probDifference = avgGtProb - avgNonGtProb;
// Calculate recall - handle case when there are no ground truth tags
const recall = tagSelection.total_ground_truth > 0 ?
selectedGroundTruthTags / tagSelection.total_ground_truth : 0;
// Update metrics
const updateMetric = (id, value, decimals = 4) => {
const element = document.getElementById(id);
if (element && value !== undefined) {
element.textContent = typeof value === 'number' ? value.toFixed(decimals) : value;
}
};
updateMetric('total-gt-tags', tagSelection.total_ground_truth, 0);
updateMetric('selected-gt-tags', selectedGroundTruthTags, 0);
updateMetric('gt-recall', recall);
updateMetric('avg-prob-gt', avgGtProb);
updateMetric('avg-prob-non-gt', avgNonGtProb);
updateMetric('unique-tags-selected', tagInfo.length, 0);
// Update selection chart
if (charts.selection) {
charts.selection.data.labels.push(timestamp);
charts.selection.data.datasets[0].data.push(recall);
charts.selection.data.datasets[1].data.push(avgGtProb);
charts.selection.data.datasets[2].data.push(avgNonGtProb);
charts.selection.data.datasets[3].data.push(probDifference);
if (charts.selection.data.labels.length > CHART_WINDOW_SIZE) {
charts.selection.data.labels.shift();
charts.selection.data.datasets.forEach(dataset => dataset.data.shift());
}
// Dynamically adjust y-axis based on data
const allValues = [
...charts.selection.data.datasets[0].data,
...charts.selection.data.datasets[1].data,
...charts.selection.data.datasets[2].data,
...charts.selection.data.datasets[3].data
].filter(val => val !== null && !isNaN(val));
if (allValues.length > 0) {
const maxValue = Math.max(...allValues);
const minValue = Math.min(...allValues);
charts.selection.options.scales.y.min = Math.min(-0.2, minValue - 0.1);
charts.selection.options.scales.y.max = Math.max(1.2, maxValue + 0.1);
}
charts.selection.update('none'); // Use 'none' mode for better performance
}
// Update selected tags list if available
if (typeof updateSelectedTagsList === 'function') {
updateSelectedTagsList(predictions);
}
}
// Update prediction history and display
predictionHistory.push(predictions);
currentPredictionIndex = predictionHistory.length - 1;
updatePredictionDisplay(predictions);
}
} catch (error) {
console.error('Error in metrics update:', error);
}
});
</script>
</body>
</html>