vectorplasticity's picture
Add main dashboard template
39f0935 verified
Raw
History Blame Contribute Delete
55.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Universal Model Trainer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--hover-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.navbar {
background: var(--primary-gradient);
box-shadow: var(--card-shadow);
}
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
}
.hero-section {
background: var(--primary-gradient);
color: white;
padding: 60px 0;
margin-bottom: 40px;
}
.hero-section h1 {
font-size: 3rem;
font-weight: 800;
margin-bottom: 1rem;
}
.card {
border: none;
border-radius: 16px;
box-shadow: var(--card-shadow);
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--hover-shadow);
}
.card-header {
background: var(--primary-gradient);
color: white;
border-radius: 16px 16px 0 0 !important;
font-weight: 600;
}
.stat-card {
text-align: center;
padding: 30px;
}
.stat-card i {
font-size: 3rem;
margin-bottom: 15px;
}
.stat-card h3 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 5px;
}
.task-card {
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s;
}
.task-card:hover {
border-color: #667eea;
}
.task-card.selected {
border-color: #764ba2;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
}
.progress-bar {
background: var(--primary-gradient);
}
.btn-primary {
background: var(--primary-gradient);
border: none;
padding: 12px 30px;
font-weight: 600;
border-radius: 25px;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: var(--hover-shadow);
}
.btn-outline-primary {
border: 2px solid #667eea;
color: #667eea;
padding: 10px 25px;
font-weight: 600;
border-radius: 25px;
}
.btn-outline-primary:hover {
background: var(--primary-gradient);
color: white;
border-color: transparent;
}
.form-control, .form-select {
border-radius: 10px;
padding: 12px;
border: 2px solid #e0e0e0;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.job-status {
padding: 5px 15px;
border-radius: 20px;
font-weight: 600;
font-size: 0.85rem;
}
.status-queued { background: #ffcdd2; color: #c62828; }
.status-running { background: #fff3e0; color: #ef6c00; }
.status-completed { background: #e8f5e9; color: #2e7d32; }
.status-failed { background: #ffebee; color: #c62828; }
.status-cancelled { background: #f5f5f5; color: #757575; }
.log-container {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
padding: 20px;
border-radius: 10px;
max-height: 300px;
overflow-y: auto;
}
.log-container::-webkit-scrollbar {
width: 8px;
}
.log-container::-webkit-scrollbar-track {
background: #2d2d2d;
}
.log-container::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.metric-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
display: inline-block;
margin: 5px;
}
.search-box {
position: relative;
}
.search-box i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #999;
}
.search-box input {
padding-left: 45px;
}
.accordion-item {
border: none;
margin-bottom: 15px;
border-radius: 10px !important;
overflow: hidden;
}
.accordion-button {
font-weight: 600;
background: white;
}
.accordion-button:not(.collapsed) {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
color: #764ba2;
}
.model-card {
border-left: 4px solid #667eea;
}
.loading-spinner {
display: none;
}
.loading-spinner.active {
display: inline-block;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s infinite;
}
.range-value {
background: #667eea;
color: white;
padding: 2px 10px;
border-radius: 10px;
font-size: 0.85rem;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand" href="#">
<i class="bi bi-cpu me-2"></i>Universal Model Trainer
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="#" onclick="showSection('dashboard')">
<i class="bi bi-speedometer2 me-1"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('new-training')">
<i class="bi bi-plus-circle me-1"></i>New Training
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('jobs')">
<i class="bi bi-list-task me-1"></i>Jobs
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('models')">
<i class="bi bi-box me-1"></i>Models
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('datasets')">
<i class="bi bi-database me-1"></i>Datasets
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('settings')">
<i class="bi bi-gear me-1"></i>Settings
</a>
</li>
</ul>
</div>
</div>
</nav>
<section class="hero-section">
<div class="container text-center">
<h1><i class="bi bi-rocket-takeoff me-3"></i>Train Any Model</h1>
<p class="lead mb-4">Fine-tune models from HuggingFace Hub with ease. No code required.</p>
<button class="btn btn-light btn-lg" onclick="showSection('new-training')">
<i class="bi bi-plus-circle me-2"></i>Start New Training
</button>
</div>
</section>
<div class="container pb-5">
<!-- Dashboard Section -->
<div id="section-dashboard" class="section">
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-collection text-primary"></i>
<h3 id="stat-total-jobs">0</h3>
<p class="text-muted mb-0">Total Jobs</p>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-play-circle text-warning"></i>
<h3 id="stat-active-jobs">0</h3>
<p class="text-muted mb-0">Active Jobs</p>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-check-circle text-success"></i>
<h3 id="stat-completed-jobs">0</h3>
<p class="text-muted mb-0">Completed</p>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-graph-up-arrow text-info"></i>
<h3 id="stat-success-rate">0%</h3>
<p class="text-muted mb-0">Success Rate</p>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<i class="bi bi-clock-history me-2"></i>Recent Jobs
</div>
<div class="card-body">
<div id="recent-jobs-list">
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No jobs yet. Start a new training!
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="bi bi-cpu me-2"></i>System Status
</div>
<div class="card-body">
<div id="system-status">
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>CPU</span>
<span id="cpu-usage">0%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" id="cpu-bar" style="width: 0%"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Memory</span>
<span id="memory-usage">0%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" id="memory-bar" style="width: 0%"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Disk</span>
<span id="disk-usage">0%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" id="disk-bar" style="width: 0%"></div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<span><i class="bi bi-gpu-card me-1"></i>GPU:</span>
<span id="gpu-status" class="badge bg-secondary">Checking...</span>
</div>
<div class="d-flex justify-content-between mt-2">
<span><i class="bi bi-hdd me-1"></i>Cache:</span>
<span id="cache-size">0 MB</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- New Training Section -->
<div id="section-new-training" class="section" style="display: none;">
<div class="card">
<div class="card-header">
<i class="bi bi-plus-circle me-2"></i>Configure New Training Job
</div>
<div class="card-body">
<form id="training-form">
<!-- Step 1: Task Type -->
<div class="mb-4">
<h5 class="mb-3"><i class="bi bi-1-circle me-2"></i>Select Task Type</h5>
<div class="row g-3" id="task-type-cards">
<div class="col-md-4">
<div class="card task-card" data-task="causal-lm" onclick="selectTask('causal-lm')">
<div class="card-body text-center">
<i class="bi bi-chat-text fs-2 text-primary"></i>
<h6 class="mt-2">Causal LM</h6>
<small class="text-muted">Text Generation</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card task-card" data-task="seq2seq" onclick="selectTask('seq2seq')">
<div class="card-body text-center">
<i class="bi bi-arrow-left-right fs-2 text-success"></i>
<h6 class="mt-2">Seq2Seq</h6>
<small class="text-muted">Summarization</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card task-card" data-task="token-classification" onclick="selectTask('token-classification')">
<div class="card-body text-center">
<i class="bi bi-tags fs-2 text-warning"></i>
<h6 class="mt-2">Token Classification</h6>
<small class="text-muted">NER</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card task-card" data-task="text-classification" onclick="selectTask('text-classification')">
<div class="card-body text-center">
<i class="bi bi-folder fs-2 text-info"></i>
<h6 class="mt-2">Text Classification</h6>
<small class="text-muted">Sentiment Analysis</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card task-card" data-task="question-answering" onclick="selectTask('question-answering')">
<div class="card-body text-center">
<i class="bi bi-question-circle fs-2 text-danger"></i>
<h6 class="mt-2">Question Answering</h6>
<small class="text-muted">Extractive QA</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card task-card" data-task="reasoning" onclick="selectTask('reasoning')">
<div class="card-body text-center">
<i class="bi bi-brain fs-2 text-secondary"></i>
<h6 class="mt-2">Reasoning</h6>
<small class="text-muted">Step-by-Step</small>
</div>
</div>
</div>
</div>
<input type="hidden" id="selected-task" name="task_type" required>
</div>
<!-- Step 2: Model Selection -->
<div class="mb-4">
<h5 class="mb-3"><i class="bi bi-2-circle me-2"></i>Select Model</h5>
<div class="row">
<div class="col-md-8">
<div class="search-box mb-3">
<i class="bi bi-search"></i>
<input type="text" class="form-control" id="model-search" placeholder="Search models...">
</div>
<input type="hidden" id="selected-model" name="model_name" required>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-primary w-100" onclick="searchModels()">
<i class="bi bi-search me-1"></i>Search
</button>
</div>
</div>
<div id="model-results" class="row g-2 mt-2"></div>
<div id="model-info" class="mt-3" style="display: none;"></div>
</div>
<!-- Step 3: Dataset Selection -->
<div class="mb-4">
<h5 class="mb-3"><i class="bi bi-3-circle me-2"></i>Select Dataset</h5>
<div class="row">
<div class="col-md-8">
<div class="search-box mb-3">
<i class="bi bi-search"></i>
<input type="text" class="form-control" id="dataset-search" placeholder="Search datasets...">
</div>
<input type="hidden" id="selected-dataset" name="dataset_name" required>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-primary w-100" onclick="searchDatasets()">
<i class="bi bi-search me-1"></i>Search
</button>
</div>
</div>
<div id="dataset-results" class="row g-2 mt-2"></div>
<div id="dataset-config" class="mt-3" style="display: none;"></div>
</div>
<!-- Step 4: Training Configuration -->
<div class="mb-4">
<h5 class="mb-3"><i class="bi bi-4-circle me-2"></i>Training Configuration</h5>
<div class="accordion" id="config-accordion">
<!-- Basic Settings -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#basic-settings">
<i class="bi bi-sliders me-2"></i>Basic Settings
</button>
</h2>
<div id="basic-settings" class="accordion-collapse collapse show">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Epochs</label>
<input type="number" class="form-control" id="epochs" name="epochs" value="3" min="1" max="100">
</div>
<div class="col-md-4">
<label class="form-label">Batch Size</label>
<input type="number" class="form-control" id="batch_size" name="batch_size" value="1" min="1" max="64">
</div>
<div class="col-md-4">
<label class="form-label">Learning Rate</label>
<input type="text" class="form-control" id="learning_rate" name="learning_rate" value="5e-5">
</div>
<div class="col-md-6">
<label class="form-label">Max Sequence Length</label>
<input type="range" class="form-range" id="max_length" name="max_length" min="128" max="2048" value="512" oninput="updateRangeValue(this)">
<div class="d-flex justify-content-between">
<span>128</span>
<span class="range-value" id="max_length-value">512</span>
<span>2048</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Warmup Steps</label>
<input type="number" class="form-control" id="warmup_steps" name="warmup_steps" value="100" min="0">
</div>
</div>
</div>
</div>
</div>
<!-- PEFT/LoRA Settings -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#peft-settings">
<i class="bi bi-tuning me-2"></i>PEFT/LoRA Settings
</button>
</h2>
<div id="peft-settings" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="use_peft" name="use_peft">
<label class="form-check-label" for="use_peft">Enable PEFT/LoRA (Recommended for resource-efficient training)</label>
</div>
<div id="peft-options" style="display: none;">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">LoRA Rank (r)</label>
<input type="number" class="form-control" id="lora_r" name="lora_r" value="8" min="1" max="256">
</div>
<div class="col-md-4">
<label class="form-label">LoRA Alpha</label>
<input type="number" class="form-control" id="lora_alpha" name="lora_alpha" value="32" min="1">
</div>
<div class="col-md-4">
<label class="form-label">LoRA Dropout</label>
<input type="number" class="form-control" id="lora_dropout" name="lora_dropout" value="0.1" min="0" max="1" step="0.1">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Settings -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#advanced-settings">
<i class="bi bi-gear me-2"></i>Advanced Settings
</button>
</h2>
<div id="advanced-settings" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Gradient Accumulation Steps</label>
<input type="number" class="form-control" id="gradient_accumulation" name="gradient_accumulation_steps" value="1" min="1" max="128">
</div>
<div class="col-md-6">
<label class="form-label">Weight Decay</label>
<input type="number" class="form-control" id="weight_decay" name="weight_decay" value="0.01" min="0" max="1" step="0.01">
</div>
<div class="col-md-6">
<label class="form-label">LR Scheduler</label>
<select class="form-select" id="lr_scheduler" name="lr_scheduler_type">
<option value="cosine">Cosine</option>
<option value="linear">Linear</option>
<option value="constant">Constant</option>
<option value="polynomial">Polynomial</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Save Steps</label>
<input type="number" class="form-control" id="save_steps" name="save_steps" value="500" min="100">
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="gradient_checkpointing" name="gradient_checkpointing">
<label class="form-check-label">Gradient Checkpointing</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="fp16" name="fp16" checked>
<label class="form-check-label">FP16 (if GPU available)</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Output Settings -->
<div class="mb-4">
<h5 class="mb-3"><i class="bi bi-5-circle me-2"></i>Output Settings</h5>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Output Model Name</label>
<input type="text" class="form-control" id="output_name" name="output_name" placeholder="my-finetuned-model">
</div>
<div class="col-md-6">
<label class="form-label">Push to Hub</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="push_to_hub" name="push_to_hub">
<label class="form-check-label">Push model to HuggingFace Hub after training</label>
</div>
</div>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-rocket-takeoff me-2"></i>Start Training
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Jobs Section -->
<div id="section-jobs" class="section" style="display: none;">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-list-task me-2"></i>Training Jobs</span>
<button class="btn btn-sm btn-outline-light" onclick="refreshJobs()">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button>
</div>
<div class="card-body">
<div id="jobs-list"></div>
</div>
</div>
</div>
<!-- Models Section -->
<div id="section-models" class="section" style="display: none;">
<div class="card">
<div class="card-header">
<i class="bi bi-box me-2"></i>Browse Models
</div>
<div class="card-body">
<p class="text-muted">Search and select models from HuggingFace Hub</p>
<!-- Models browser content -->
</div>
</div>
</div>
<!-- Datasets Section -->
<div id="section-datasets" class="section" style="display: none;">
<div class="card">
<div class="card-header">
<i class="bi bi-database me-2"></i>Browse Datasets
</div>
<div class="card-body">
<p class="text-muted">Search and select datasets from HuggingFace Hub</p>
<!-- Datasets browser content -->
</div>
</div>
</div>
<!-- Settings Section -->
<div id="section-settings" class="section" style="display: none;">
<div class="card">
<div class="card-header">
<i class="bi bi-gear me-2"></i>Settings
</div>
<div class="card-body">
<p class="text-muted">Configure your training environment</p>
<!-- Settings content -->
</div>
</div>
</div>
</div>
<!-- Job Detail Modal -->
<div class="modal fade" id="jobDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-info-circle me-2"></i>Job Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="job-detail-content">
<!-- Dynamic content -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// API Base URL
const API_BASE = '/api';
let selectedTask = null;
let selectedModel = null;
let selectedDataset = null;
// Navigation
function showSection(sectionId) {
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
document.getElementById(`section-${sectionId}`).style.display = 'block';
if (sectionId === 'dashboard') loadDashboard();
if (sectionId === 'jobs') loadJobs();
}
// Dashboard
async function loadDashboard() {
try {
const [statsRes, systemRes, jobsRes] = await Promise.all([
fetch(`${API_BASE}/jobs/statistics`),
fetch(`${API_BASE}/system/resources`),
fetch(`${API_BASE}/jobs/?per_page=5`)
]);
const stats = await statsRes.json();
const system = await systemRes.json();
const jobs = await jobsRes.json();
// Update stats
document.getElementById('stat-total-jobs').textContent = stats.total_jobs;
document.getElementById('stat-active-jobs').textContent = stats.active_jobs;
document.getElementById('stat-completed-jobs').textContent = stats.completed_jobs;
document.getElementById('stat-success-rate').textContent = stats.success_rate.toFixed(1) + '%';
// Update system status
document.getElementById('cpu-usage').textContent = system.cpu.percent.toFixed(1) + '%';
document.getElementById('cpu-bar').style.width = system.cpu.percent + '%';
document.getElementById('memory-usage').textContent = system.memory.percent.toFixed(1) + '%';
document.getElementById('memory-bar').style.width = system.memory.percent + '%';
document.getElementById('disk-usage').textContent = system.disk.percent.toFixed(1) + '%';
document.getElementById('disk-bar').style.width = system.disk.percent + '%';
document.getElementById('gpu-status').textContent = system.gpu?.available ? 'Available' : 'Not Available';
document.getElementById('gpu-status').className = system.gpu?.available ? 'badge bg-success' : 'badge bg-secondary';
document.getElementById('cache-size').textContent = formatBytes(system.cache?.total_bytes || 0);
// Update recent jobs
renderRecentJobs(jobs.jobs);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function renderRecentJobs(jobs) {
const container = document.getElementById('recent-jobs-list');
if (jobs.length === 0) {
container.innerHTML = `<div class="text-center text-muted py-4"><i class="bi bi-inbox fs-1 d-block mb-2"></i>No jobs yet</div>`;
return;
}
container.innerHTML = jobs.map(job => `
<div class="card mb-2 model-card" onclick="showJobDetail('${job.job_id}')">
<div class="card-body py-2">
<div class="row align-items-center">
<div class="col-md-4">
<strong>${job.model_name}</strong>
<br><small class="text-muted">${job.dataset_name}</small>
</div>
<div class="col-md-3">
<span class="job-status status-${job.status}">${job.status}</span>
</div>
<div class="col-md-3">
<div class="progress" style="height: 20px;">
<div class="progress-bar" style="width: ${job.progress}%">${job.progress.toFixed(0)}%</div>
</div>
</div>
<div class="col-md-2 text-end">
<small class="text-muted">${formatDate(job.created_at)}</small>
</div>
</div>
</div>
</div>
`).join('');
}
// Task Selection
function selectTask(task) {
selectedTask = task;
document.querySelectorAll('.task-card').forEach(card => card.classList.remove('selected'));
document.querySelector(`.task-card[data-task="${task}"]`).classList.add('selected');
document.getElementById('selected-task').value = task;
}
// Model Search
async function searchModels() {
const query = document.getElementById('model-search').value;
if (!query) return;
try {
const res = await fetch(`${API_BASE}/models/search?q=${encodeURIComponent(query)}&task=${selectedTask || ''}`);
const data = await res.json();
renderModelResults(data.models);
} catch (error) {
console.error('Error searching models:', error);
}
}
function renderModelResults(models) {
const container = document.getElementById('model-results');
container.innerHTML = models.slice(0, 12).map(model => `
<div class="col-md-3">
<div class="card task-card" onclick="selectModel('${model.id}')">
<div class="card-body py-2">
<small class="fw-bold">${model.id}</small>
<br><small class="text-muted">${formatBytes(model.size_bytes || 0)}</small>
</div>
</div>
</div>
`).join('');
}
async function selectModel(modelId) {
selectedModel = modelId;
document.getElementById('selected-model').value = modelId;
document.querySelectorAll('#model-results .task-card').forEach(card => card.classList.remove('selected'));
event.currentTarget.classList.add('selected');
// Load model info
try {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(modelId)}/info`);
const info = await res.json();
const container = document.getElementById('model-info');
container.style.display = 'block';
container.innerHTML = `
<div class="card">
<div class="card-body">
<h6>${info.model_id}</h6>
<div class="row">
<div class="col-6"><small class="text-muted">Parameters:</small></div>
<div class="col-6 text-end"><small>${info.num_parameters || 'Unknown'}</small></div>
</div>
<div class="row">
<div class="col-6"><small class="text-muted">Architecture:</small></div>
<div class="col-6 text-end"><small>${info.architectures?.[0] || 'Unknown'}</small></div>
</div>
</div>
</div>
`;
} catch (error) {
console.error('Error loading model info:', error);
}
}
// Dataset Search
async function searchDatasets() {
const query = document.getElementById('dataset-search').value;
if (!query) return;
try {
const res = await fetch(`${API_BASE}/datasets/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
renderDatasetResults(data.datasets);
} catch (error) {
console.error('Error searching datasets:', error);
}
}
function renderDatasetResults(datasets) {
const container = document.getElementById('dataset-results');
container.innerHTML = datasets.slice(0, 12).map(ds => `
<div class="col-md-3">
<div class="card task-card" onclick="selectDataset('${ds.id}')">
<div class="card-body py-2">
<small class="fw-bold">${ds.id}</small>
<br><small class="text-muted">${ds.downloads?.toLocaleString() || 0} downloads</small>
</div>
</div>
</div>
`).join('');
}
function selectDataset(datasetId) {
selectedDataset = datasetId;
document.getElementById('selected-dataset').value = datasetId;
document.querySelectorAll('#dataset-results .task-card').forEach(card => card.classList.remove('selected'));
event.currentTarget.classList.add('selected');
}
// Jobs
async function loadJobs() {
try {
const res = await fetch(`${API_BASE}/jobs/`);
const data = await res.json();
renderJobsList(data.jobs);
} catch (error) {
console.error('Error loading jobs:', error);
}
}
function renderJobsList(jobs) {
const container = document.getElementById('jobs-list');
if (jobs.length === 0) {
container.innerHTML = `<div class="text-center text-muted py-4"><i class="bi bi-inbox fs-1 d-block mb-2"></i>No jobs yet</div>`;
return;
}
container.innerHTML = jobs.map(job => `
<div class="card mb-2 model-card" onclick="showJobDetail('${job.job_id}')">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-3">
<strong>${job.model_name}</strong>
<br><small class="text-muted">${job.task_type}</small>
</div>
<div class="col-md-3">
<small>${job.dataset_name}</small>
</div>
<div class="col-md-2">
<span class="job-status status-${job.status}">${job.status}</span>
</div>
<div class="col-md-2">
<div class="progress" style="height: 20px;">
<div class="progress-bar" style="width: ${job.progress}%">${job.progress.toFixed(0)}%</div>
</div>
</div>
<div class="col-md-2 text-end">
<small class="text-muted">${formatDate(job.created_at)}</small>
<br><small>${formatDuration(job.started_at, job.completed_at)}</small>
</div>
</div>
</div>
</div>
`).join('');
}
async function showJobDetail(jobId) {
try {
const res = await fetch(`${API_BASE}/jobs/${jobId}`);
const job = await res.json();
const container = document.getElementById('job-detail-content');
container.innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Job Information</h6>
<table class="table table-sm">
<tr><td>Job ID</td><td><code>${job.job_id}</code></td></tr>
<tr><td>Status</td><td><span class="job-status status-${job.status}">${job.status}</span></td></tr>
<tr><td>Task Type</td><td>${job.task_type}</td></tr>
<tr><td>Model</td><td>${job.model_name}</td></tr>
<tr><td>Dataset</td><td>${job.dataset_name}</td></tr>
</table>
</div>
<div class="col-md-6">
<h6>Training Progress</h6>
<div class="progress mb-3" style="height: 25px;">
<div class="progress-bar" style="width: ${job.progress}%">${job.progress.toFixed(1)}%</div>
</div>
<p>Epoch: ${job.current_epoch || 0}/${job.total_epochs || '?'} | Step: ${job.current_step || 0}/${job.total_steps || '?'}</p>
${job.current_loss ? `<p>Loss: ${job.current_loss.toFixed(4)}</p>` : ''}
${job.learning_rate ? `<p>Learning Rate: ${job.learning_rate.toExponential(2)}</p>` : ''}
</div>
</div>
${Object.keys(job.metrics).length > 0 ? `
<div class="row mt-3">
<div class="col-12">
<h6>Metrics</h6>
<div>
${Object.entries(job.metrics).map(([k, v]) => `<span class="metric-badge">${k}: ${typeof v === 'number' ? v.toFixed(4) : v}</span>`).join('')}
</div>
</div>
</div>
` : ''}
<div class="row mt-3">
<div class="col-12">
<h6>Actions</h6>
${job.status === 'running' || job.status === 'queued' ? `
<button class="btn btn-warning btn-sm" onclick="cancelJob('${job.job_id}')">
<i class="bi bi-x-circle me-1"></i>Cancel
</button>
` : ''}
${job.status === 'completed' ? `
<button class="btn btn-success btn-sm" onclick="downloadModel('${job.job_id}')">
<i class="bi bi-download me-1"></i>Download Model
</button>
` : ''}
${job.status === 'failed' ? `
<button class="btn btn-primary btn-sm" onclick="retryJob('${job.job_id}')">
<i class="bi bi-arrow-clockwise me-1"></i>Retry
</button>
` : ''}
</div>
</div>
`;
new bootstrap.Modal(document.getElementById('jobDetailModal')).show();
} catch (error) {
console.error('Error loading job details:', error);
}
}
// Training Form Submission
document.getElementById('training-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
task_type: selectedTask,
model_name: selectedModel,
dataset_name: selectedDataset,
epochs: parseInt(document.getElementById('epochs').value),
batch_size: parseInt(document.getElementById('batch_size').value),
learning_rate: document.getElementById('learning_rate').value,
max_length: parseInt(document.getElementById('max_length').value),
warmup_steps: parseInt(document.getElementById('warmup_steps').value),
use_peft: document.getElementById('use_peft').checked,
output_name: document.getElementById('output_name').value || 'my-model',
push_to_hub: document.getElementById('push_to_hub').checked
};
if (formData.use_peft) {
formData.lora_r = parseInt(document.getElementById('lora_r').value);
formData.lora_alpha = parseInt(document.getElementById('lora_alpha').value);
formData.lora_dropout = parseFloat(document.getElementById('lora_dropout').value);
}
try {
const res = await fetch(`${API_BASE}/training/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.job_id) {
alert(`Training started! Job ID: ${data.job_id}`);
showSection('jobs');
} else {
alert('Error starting training: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Error starting training:', error);
alert('Error starting training');
}
});
// PEFT toggle
document.getElementById('use_peft').addEventListener('change', function() {
document.getElementById('peft-options').style.display = this.checked ? 'block' : 'none';
});
// Utility Functions
function updateRangeValue(input) {
document.getElementById(`${input.id}-value`).textContent = input.value;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}
function formatDuration(start, end) {
if (!start) return '';
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const diff = Math.floor((endDate - startDate) / 1000);
const h = Math.floor(diff / 3600);
const m = Math.floor((diff % 3600) / 60);
const s = diff % 60;
return `${h}h ${m}m ${s}s`;
}
function refreshJobs() { loadJobs(); }
async function cancelJob(jobId) {
await fetch(`${API_BASE}/jobs/${jobId}/cancel`, { method: 'POST' });
loadJobs();
}
async function retryJob(jobId) {
await fetch(`${API_BASE}/jobs/${jobId}/retry`, { method: 'POST' });
loadJobs();
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadDashboard();
setInterval(loadDashboard, 30000);
});
</script>
</body>
</html>