vectorplasticity's picture
Add dashboard JavaScript
278a1e7 verified
Raw
History Blame Contribute Delete
24 kB
/**
* Universal Model Trainer - Dashboard JavaScript
*/
// API Base URL
const API_BASE = '/api';
// State Management
const state = {
jobs: [],
models: [],
datasets: [],
systemJob: null,
searchResults: {
models: [],
datasets: []
}
};
// Utility Functions
const utils = {
async fetchAPI(endpoint, options = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || 'Request failed');
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
},
formatDate(dateStr) {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleString();
},
formatDuration(seconds) {
if (!seconds) return 'N/A';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
},
formatNumber(num) {
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
return num?.toString() || '0';
},
getStatusBadgeClass(status) {
const classes = {
'queued': 'badge-queued',
'running': 'badge-running',
'completed': 'badge-completed',
'failed': 'badge-failed',
'cancelled': 'badge-cancelled',
'paused': 'badge-paused'
};
return classes[status?.toLowerCase()] || 'badge-queued';
},
showToast(message, type = 'info') {
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
const toast = document.createElement('div');
toast.className = `alert alert-${type} alert-dismissible fade show`;
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
toastContainer.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
};
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toastContainer';
container.style.cssText = 'position: fixed; top: 80px; right: 20px; z-index: 9999; max-width: 400px;';
document.body.appendChild(container);
return container;
}
// API Functions
const api = {
// Jobs
async getJobs(filters = {}) {
const params = new URLSearchParams(filters).toString();
return utils.fetchAPI(`/jobs?${params}`);
},
async getJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}`);
},
async getActiveJobs() {
return utils.fetchAPI('/jobs/active');
},
async getJobStatistics() {
return utils.fetchAPI('/jobs/statistics');
},
async cancelJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}/cancel`, { method: 'POST' });
},
async pauseJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}/pause`, { method: 'POST' });
},
async resumeJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}/resume`, { method: 'POST' });
},
async retryJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}/retry`, { method: 'POST' });
},
async deleteJob(jobId) {
return utils.fetchAPI(`/jobs/${jobId}`, { method: 'DELETE' });
},
async getJobLogs(jobId, limit = 100) {
return utils.fetchAPI(`/jobs/${jobId}/logs?limit=${limit}`);
},
async getJobCheckpoints(jobId) {
return utils.fetchAPI(`/jobs/${jobId}/checkpoints`);
},
// Training
async startTraining(config) {
return utils.fetchAPI('/training/start', {
method: 'POST',
body: JSON.stringify(config)
});
},
async getTrainingTemplates() {
return utils.fetchAPI('/training/templates');
},
// Models
async searchModels(query, task = null, limit = 20) {
const params = new URLSearchParams({ query, limit });
if (task) params.append('task', task);
return utils.fetchAPI(`/models/search?${params}`);
},
async getModelInfo(modelId) {
return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}`);
},
async getModelRecommendations(task) {
return utils.fetchAPI(`/models/recommendations/${task}`);
},
async checkModelCompatibility(modelId, task) {
return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}/compatibility?task=${task}`);
},
// Datasets
async searchDatasets(query, limit = 20) {
return utils.fetchAPI(`/datasets/search?query=${encodeURIComponent(query)}&limit=${limit}`);
},
async getDatasetInfo(datasetId) {
return utils.fetchAPI(`/datasets/${encodeURIComponent(datasetId)}`);
},
async getDatasetRecommendations(task) {
return utils.fetchAPI(`/datasets/recommendations/${task}`);
},
async getDatasetPreview(datasetId, config = null, split = null) {
let url = `/datasets/${encodeURIComponent(datasetId)}/preview`;
const params = new URLSearchParams();
if (config) params.append('config', config);
if (split) params.append('split', split);
if (params.toString()) url += `?${params}`;
return utils.fetchAPI(url);
},
// System
async getSystemInfo() {
return utils.fetchAPI('/system/info');
},
async getResourceUsage() {
return utils.fetchAPI('/system/resources');
},
async clearCache() {
return utils.fetchAPI('/system/cache/clear', { method: 'POST' });
}
};
// UI Components
const ui = {
// Dashboard Stats
async loadDashboardStats() {
try {
const [stats, system] = await Promise.all([
api.getJobStatistics(),
api.getResourceUsage()
]);
document.getElementById('totalJobs').textContent = stats.total_jobs || 0;
document.getElementById('runningJobs').textContent = stats.running_jobs || 0;
document.getElementById('completedJobs').textContent = stats.completed_jobs || 0;
document.getElementById('failedJobs').textContent = stats.failed_jobs || 0;
// Update resource bars
if (system.cpu_percent !== undefined) {
document.getElementById('cpuUsage').style.width = `${system.cpu_percent}%`;
document.getElementById('cpuUsageText').textContent = `${system.cpu_percent.toFixed(1)}%`;
}
if (system.memory_percent !== undefined) {
document.getElementById('memoryUsage').style.width = `${system.memory_percent}%`;
document.getElementById('memoryUsageText').textContent = `${system.memory_percent.toFixed(1)}%`;
}
if (system.disk_percent !== undefined) {
document.getElementById('diskUsage').style.width = `${system.disk_percent}%`;
document.getElementById('diskUsageText').textContent = `${system.disk_percent.toFixed(1)}%`;
}
} catch (error) {
console.error('Error loading stats:', error);
}
},
// Jobs Table
async loadJobsTable() {
try {
const response = await api.getJobs();
state.jobs = response.jobs || [];
this.renderJobsTable();
} catch (error) {
utils.showToast('Failed to load jobs', 'danger');
}
},
renderJobsTable() {
const tbody = document.getElementById('jobsTableBody');
if (!tbody) return;
if (state.jobs.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted py-5">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No training jobs yet. Start a new training to see it here.
</td>
</tr>
`;
return;
}
tbody.innerHTML = state.jobs.map(job => `
<tr onclick="ui.showJobDetail('${job.id}')" style="cursor: pointer;">
<td>
<span class="fw-medium">${job.id.substring(0, 8)}...</span>
</td>
<td>
<span class="badge bg-secondary">${job.task_type}</span>
</td>
<td>
<div class="small">${job.model_name}</div>
<div class="text-muted small">${job.dataset_name}</div>
</td>
<td>
<span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span>
</td>
<td>
${job.progress !== undefined ? `
<div class="progress" style="height: 6px;">
<div class="progress-bar" style="width: ${job.progress}%"></div>
</div>
<small class="text-muted">${job.progress.toFixed(1)}%</small>
` : '-'}
</td>
<td>${utils.formatDate(job.created_at)}</td>
<td>
<div class="btn-group btn-group-sm">
${job.status === 'running' ? `
<button class="btn btn-outline-warning" onclick="event.stopPropagation(); ui.pauseJob('${job.id}')" title="Pause">
<i class="bi bi-pause"></i>
</button>
` : ''}
${job.status === 'paused' ? `
<button class="btn btn-outline-success" onclick="event.stopPropagation(); ui.resumeJob('${job.id}')" title="Resume">
<i class="bi bi-play"></i>
</button>
` : ''}
${job.status === 'queued' || job.status === 'running' ? `
<button class="btn btn-outline-danger" onclick="event.stopPropagation(); ui.cancelJob('${job.id}')" title="Cancel">
<i class="bi bi-x"></i>
</button>
` : ''}
${job.status === 'failed' ? `
<button class="btn btn-outline-primary" onclick="event.stopPropagation(); ui.retryJob('${job.id}')" title="Retry">
<i class="bi bi-arrow-clockwise"></i>
</button>
` : ''}
</div>
</td>
</tr>
`).join('');
},
async showJobDetail(jobId) {
try {
const job = await api.getJob(jobId);
state.currentJob = job;
const modal = new bootstrap.Modal(document.getElementById('jobDetailModal'));
// Populate modal content
document.getElementById('jobDetailTitle').textContent = `Job ${job.id.substring(0, 8)}`;
document.getElementById('jobDetailStatus').innerHTML = `
<span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span>
`;
document.getElementById('jobDetailModel').textContent = job.model_name;
document.getElementById('jobDetailDataset').textContent = job.dataset_name;
document.getElementById('jobDetailTask').textContent = job.task_type;
document.getElementById('jobDetailProgress').style.width = `${job.progress || 0}%`;
document.getElementById('jobDetailProgressText').textContent = `${(job.progress || 0).toFixed(1)}%`;
document.getElementById('jobDetailEpochs').textContent = `${job.current_epoch || 0}/${job.total_epochs || 0}`;
document.getElementById('jobDetailLoss').textContent = job.current_loss?.toFixed(4) || 'N/A';
document.getElementById('jobDetailLR').textContent = job.learning_rate || 'N/A';
document.getElementById('jobDetailCreated').textContent = utils.formatDate(job.created_at);
document.getElementById('jobDetailStarted').textContent = utils.formatDate(job.started_at);
document.getElementById('jobDetailFinished').textContent = utils.formatDate(job.finished_at);
document.getElementById('jobDetailDuration').textContent = utils.formatDuration(job.duration);
// Load logs
const logs = await api.getJobLogs(jobId, 50);
const logsContainer = document.getElementById('jobDetailLogs');
logsContainer.innerHTML = logs.logs?.map(log => `
<div class="log-entry log-${log.level?.toLowerCase() || 'info'}">
<span class="text-muted">[${new Date(log.timestamp).toLocaleTimeString()}]</span> ${log.message}
</div>
`).join('') || '<div class="text-muted">No logs available</div>';
modal.show();
} catch (error) {
utils.showToast('Failed to load job details', 'danger');
}
},
async cancelJob(jobId) {
if (confirm('Are you sure you want to cancel this job?')) {
try {
await api.cancelJob(jobId);
utils.showToast('Job cancelled', 'warning');
await this.loadJobsTable();
} catch (error) {
utils.showToast('Failed to cancel job', 'danger');
}
}
},
async pauseJob(jobId) {
try {
await api.pauseJob(jobId);
utils.showToast('Job paused', 'info');
await this.loadJobsTable();
} catch (error) {
utils.showToast('Failed to pause job', 'danger');
}
},
async resumeJob(jobId) {
try {
await api.resumeJob(jobId);
utils.showToast('Job resumed', 'success');
await this.loadJobsTable();
} catch (error) {
utils.showToast('Failed to resume job', 'danger');
}
},
async retryJob(jobId) {
if (confirm('Retry this job?')) {
try {
await api.retryJob(jobId);
utils.showToast('Job retry started', 'info');
await this.loadJobsTable();
} catch (error) {
utils.showToast('Failed to retry job', 'danger');
}
}
},
// Model Search
async searchModels(query) {
if (!query || query.length < 2) return;
const task = document.getElementById('taskType')?.value;
try {
const results = await api.searchModels(query, task);
state.searchResults.models = results.models || [];
this.renderModelResults();
} catch (error) {
utils.showToast('Model search failed', 'danger');
}
},
renderModelResults() {
const container = document.getElementById('modelResults');
if (!container) return;
container.innerHTML = state.searchResults.models.map(model => `
<div class="search-result" onclick="ui.selectModel('${model.id}')">
<div class="result-title">${model.id}</div>
<div class="result-meta">
<span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(model.downloads)}</span>
<span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(model.likes)}</span>
<span><i class="bi bi-tag"></i> ${model.pipeline_tag || model.task}</span>
</div>
<small class="text-muted">${model.description?.substring(0, 100) || ''}...</small>
</div>
`).join('') || '<div class="text-muted text-center py-3">No models found</div>';
},
selectModel(modelId) {
document.getElementById('modelName').value = modelId;
document.getElementById('modelResults').style.display = 'none';
utils.showToast(`Selected model: ${modelId}`, 'success');
},
// Dataset Search
async searchDatasets(query) {
if (!query || query.length < 2) return;
try {
const results = await api.searchDatasets(query);
state.searchResults.datasets = results.datasets || [];
this.renderDatasetResults();
} catch (error) {
utils.showToast('Dataset search failed', 'danger');
}
},
renderDatasetResults() {
const container = document.getElementById('datasetResults');
if (!container) return;
container.innerHTML = state.searchResults.datasets.map(dataset => `
<div class="search-result" onclick="ui.selectDataset('${dataset.id}')">
<div class="result-title">${dataset.id}</div>
<div class="result-meta">
<span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(dataset.downloads)}</span>
<span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(dataset.likes)}</span>
</div>
<small class="text-muted">${dataset.description?.substring(0, 100) || ''}...</small>
</div>
`).join('') || '<div class="text-muted text-center py-3">No datasets found</div>';
},
selectDataset(datasetId) {
document.getElementById('datasetName').value = datasetId;
document.getElementById('datasetResults').style.display = 'none';
utils.showToast(`Selected dataset: ${datasetId}`, 'success');
},
// Training Form
async submitTrainingForm() {
const form = document.getElementById('trainingForm');
const formData = new FormData(form);
const config = {
model_name: formData.get('modelName'),
dataset_config: {
dataset_name: formData.get('datasetName'),
split: formData.get('split') || 'train',
text_column: formData.get('textColumn'),
label_column: formData.get('labelColumn'),
},
training_args: {
task_type: formData.get('taskType'),
epochs: parseInt(formData.get('epochs')) || 3,
batch_size: parseInt(formData.get('batchSize')) || 8,
learning_rate: parseFloat(formData.get('learningRate')) || 5e-5,
max_length: parseInt(formData.get('maxLength')) || 512,
warmup_steps: parseInt(formData.get('warmupSteps')) || 0,
weight_decay: parseFloat(formData.get('weightDecay')) || 0.01,
},
peft_config: {
use_peft: formData.get('usePeft') === 'on',
lora_r: parseInt(formData.get('loraR')) || 8,
lora_alpha: parseInt(formData.get('loraAlpha')) || 16,
lora_dropout: parseFloat(formData.get('loraDropout')) || 0.05,
},
output_config: {
output_dir: formData.get('outputDir') || 'output',
push_to_hub: formData.get('pushToHub') === 'on',
hub_repo_name: formData.get('hubRepoName'),
}
};
try {
utils.showToast('Starting training...', 'info');
const result = await api.startTraining(config);
utils.showToast(`Training started: ${result.job_id}`, 'success');
form.reset();
await this.loadJobsTable();
await this.loadDashboardStats();
} catch (error) {
utils.showToast(`Failed to start training: ${error.message}`, 'danger');
}
},
// System Status
async loadSystemStatus() {
try {
const info = await api.getSystemInfo();
const container = document.getElementById('systemInfo');
if (container) {
container.innerHTML = `
<div class="row">
<div class="col-md-6">
<p><strong>Platform:</strong> ${info.platform || 'N/A'}</p>
<p><strong>Python:</strong> ${info.python_version || 'N/A'}</p>
<p><strong>CUDA Available:</strong> ${info.cuda_available ? 'Yes' : 'No'}</p>
</div>
<div class="col-md-6">
<p><strong>Transformers:</strong> ${info.transformers_version || 'N/A'}</p>
<p><strong>Torch:</strong> ${info.torch_version || 'N/A'}</p>
<p><strong>GPU:</strong> ${info.gpu_info || 'None'}</p>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to load system info:', error);
}
},
async clearCache() {
if (confirm('Clear all cached models and datasets?')) {
try {
await api.clearCache();
utils.showToast('Cache cleared', 'success');
await this.loadSystemStatus();
} catch (error) {
utils.showToast('Failed to clear cache', 'danger');
}
}
}
};
// Event Listeners
document.addEventListener('DOMContentLoaded', () => {
// Load initial data
ui.loadDashboardStats();
ui.loadJobsTable();
ui.loadSystemStatus();
// Auto-refresh
setInterval(() => {
ui.loadDashboardStats();
ui.loadJobsTable();
}, 30000);
// Model search with debounce
const modelInput = document.getElementById('modelName');
if (modelInput) {
modelInput.addEventListener('input', utils.debounce((e) => {
ui.searchModels(e.target.value);
}, 300));
modelInput.addEventListener('focus', () => {
document.getElementById('modelResults').style.display = 'block';
});
}
// Dataset search with debounce
const datasetInput = document.getElementById('datasetName');
if (datasetInput) {
datasetInput.addEventListener('input', utils.debounce((e) => {
ui.searchDatasets(e.target.value);
}, 300));
datasetInput.addEventListener('focus', () => {
document.getElementById('datasetResults').style.display = 'block';
});
}
// Training form submission
const trainingForm = document.getElementById('trainingForm');
if (trainingForm) {
trainingForm.addEventListener('submit', (e) => {
e.preventDefault();
ui.submitTrainingForm();
});
}
// Close search results on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('#modelName') && !e.target.closest('#modelResults')) {
document.getElementById('modelResults')?.style.display = 'none';
}
if (!e.target.closest('#datasetName') && !e.target.closest('#datasetResults')) {
document.getElementById('datasetResults')?.style.display = 'none';
}
});
});
// Export for global access
window.ui = ui;
window.api = api;
window.utils = utils;