vectorplasticity's picture
Simplify dashboard - fix column mapping, job tracking, and form submission
14f2bdc verified
Raw
History Blame Contribute Delete
73.7 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;
}
.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;
}
.range-value {
background: #667eea;
color: white;
padding: 2px 10px;
border-radius: 10px;
font-size: 0.85rem;
}
.search-loading {
opacity: 0.6;
pointer-events: none;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 10px 15px;
border-radius: 8px;
margin-top: 10px;
}
.success-message {
background: #e8f5e9;
color: #2e7d32;
padding: 10px 15px;
border-radius: 8px;
margin-top: 10px;
}
.step-indicator {
display: flex;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
}
.step-indicator .step {
display: flex;
align-items: center;
margin: 5px;
}
.step-indicator .step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 10px;
}
.step-indicator .step.active .step-number {
background: var(--primary-gradient);
color: white;
}
.step-indicator .step.completed .step-number {
background: #4caf50;
color: white;
}
.step-indicator .step-connector {
width: 30px;
height: 2px;
background: #e0e0e0;
margin: 0 5px;
}
.step-indicator .step-connector.completed {
background: #4caf50;
}
.split-chip {
display: inline-block;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.85rem;
margin: 3px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.split-chip.available { background: #e8f5e9; color: #2e7d32; }
.split-chip.selected { border-color: #667eea; background: #667eea; color: white; }
.split-chip:hover { border-color: #667eea; }
.variable-tag {
background: #667eea;
color: white;
padding: 3px 10px;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
cursor: pointer;
margin: 2px;
display: inline-block;
}
.variable-tag:hover {
background: #764ba2;
}
.template-preview {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
padding: 15px;
border-radius: 8px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.template-preview .system { color: #569cd6; }
.template-preview .user { color: #4ec9b0; }
.template-preview .assistant { color: #ce9178; }
.template-preview .context { color: #dcdcaa; }
.template-preview .reasoning { color: #c586c0; }
.prompt-section {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 10px;
overflow: hidden;
}
.prompt-section-header {
background: #f8f9fa;
padding: 10px 15px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt-section-body {
padding: 15px;
display: none;
}
.prompt-section-body.show {
display: block;
}
.logout-btn {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3);
color: white;
}
.logout-btn:hover {
background: rgba(255,255,255,0.3);
color: white;
}
.column-role-card {
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
transition: all 0.2s;
}
.column-role-card:hover {
border-color: #667eea;
}
.column-role-card.active {
border-color: #667eea;
background: #f8f9ff;
}
</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 me-auto">
<li class="nav-item">
<a class="nav-link active" href="#" onclick="showSection('dashboard'); return false;">
<i class="bi bi-speedometer2 me-1"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('new-training'); return false;">
<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'); return false;">
<i class="bi bi-list-task me-1"></i>Jobs
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('models'); return false;">
<i class="bi bi-box me-1"></i>Models
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('datasets'); return false;">
<i class="bi bi-database me-1"></i>Datasets
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="showSection('settings'); return false;">
<i class="bi bi-gear me-1"></i>Settings
</a>
</li>
</ul>
<div class="d-flex">
<button class="btn logout-btn btn-sm" onclick="logout()">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</button>
</div>
</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 full control over data formatting</p>
<button class="btn btn-light btn-lg" onclick="showSection('new-training'); return false;">
<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 d-flex justify-content-between align-items-center">
<span><i class="bi bi-clock-history me-2"></i>Current Training</span>
<button class="btn btn-sm btn-outline-light" onclick="refreshCurrentJob()">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button>
</div>
<div class="card-body">
<div id="current-job-container">
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No active training. Start a new training job!
</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">
<!-- Step Indicator -->
<div class="step-indicator mb-4">
<div class="step" id="step-1">
<div class="step-number">1</div>
<span class="step-label">Task</span>
</div>
<div class="step-connector" id="connector-1-2"></div>
<div class="step" id="step-2">
<div class="step-number">2</div>
<span class="step-label">Model</span>
</div>
<div class="step-connector" id="connector-2-3"></div>
<div class="step" id="step-3">
<div class="step-number">3</div>
<span class="step-label">Dataset</span>
</div>
<div class="step-connector" id="connector-3-4"></div>
<div class="step" id="step-4">
<div class="step-number">4</div>
<span class="step-label">Columns</span>
</div>
<div class="step-connector" id="connector-4-5"></div>
<div class="step" id="step-5">
<div class="step-number">5</div>
<span class="step-label">Train</span>
</div>
</div>
<form id="training-form">
<!-- Step 1: Task Type -->
<div class="mb-4" id="form-step-1">
<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/Translation</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/Categories</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">Chain-of-Thought</small>
</div>
</div>
</div>
</div>
<input type="hidden" id="selected-task" name="task_type" required>
</div>
<!-- Step 2: Model Selection -->
<div class="mb-4" id="form-step-2">
<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 (e.g., gpt2, bert, llama, mistral)...">
</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 id="model-recommendations" class="mt-3"></div>
</div>
<!-- Step 3: Dataset Selection -->
<div class="mb-4" id="form-step-3">
<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 (e.g., squad, imdb, alpaca)...">
</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-recommendations" class="mt-3"></div>
</div>
<!-- Step 4: Column Mapping (SIMPLIFIED) -->
<div class="mb-4" id="form-step-4" style="display: none;">
<h5 class="mb-3"><i class="bi bi-4-circle me-2"></i>Map Dataset Columns</h5>
<div id="column-mapping-container">
<div class="text-center text-muted py-4">
<i class="bi bi-database fs-1 d-block mb-2"></i>
Select a dataset to configure columns
</div>
</div>
</div>
<!-- Step 5: Training Configuration -->
<div class="mb-4" id="form-step-5" style="display: none;">
<h5 class="mb-3"><i class="bi bi-5-circle me-2"></i>Training Configuration</h5>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Job Name</label>
<input type="text" class="form-control" id="job_name" name="job_name" placeholder="my-training-job">
</div>
<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-4">
<label class="form-label">Max Sequence Length</label>
<input type="range" class="form-range" id="max_length" name="max_length" min="128" max="4096" 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>4096</span>
</div>
</div>
<div class="col-md-4">
<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>
<!-- PEFT Settings -->
<div class="card mb-3">
<div class="card-header py-2">
<i class="bi bi-tuning me-2"></i>PEFT/LoRA Settings
</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="use_peft" name="use_peft" checked>
<label class="form-check-label" for="use_peft">Enable PEFT/LoRA (Recommended for CPU training)</label>
</div>
<div id="peft-options">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Method</label>
<select class="form-select" id="peft_method" name="peft_method">
<option value="lora">LoRA</option>
<option value="adalora">AdaLoRA</option>
<option value="ia3">IA3</option>
</select>
</div>
<div class="col-md-3">
<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-3">
<label class="form-label">LoRA Alpha</label>
<input type="number" class="form-control" id="lora_alpha" name="lora_alpha" value="16" min="1">
</div>
<div class="col-md-3">
<label class="form-label">LoRA Dropout</label>
<input type="number" class="form-control" id="lora_dropout" name="lora_dropout" value="0.05" min="0" max="1" step="0.01">
</div>
</div>
</div>
</div>
</div>
<!-- Output Settings -->
<div class="card">
<div class="card-header py-2">
<i class="bi bi-cloud-upload me-2"></i>Output Settings
</div>
<div class="card-body">
<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">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" id="push_to_hub" name="push_to_hub">
<label class="form-check-label">Push to HuggingFace Hub after training</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg" id="start-training-btn">
<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="loadJobs()">
<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">
<div class="row mb-3">
<div class="col-md-8">
<div class="search-box">
<i class="bi bi-search"></i>
<input type="text" class="form-control" id="models-page-search" placeholder="Search models..." onkeypress="if(event.key==='Enter')searchModelsPage()">
</div>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-primary w-100" onclick="searchModelsPage()">
<i class="bi bi-search me-1"></i>Search
</button>
</div>
</div>
<div id="models-page-results" class="row g-3"></div>
</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">
<div class="row mb-3">
<div class="col-md-8">
<div class="search-box">
<i class="bi bi-search"></i>
<input type="text" class="form-control" id="datasets-page-search" placeholder="Search datasets..." onkeypress="if(event.key==='Enter')searchDatasetsPage()">
</div>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-primary w-100" onclick="searchDatasetsPage()">
<i class="bi bi-search me-1"></i>Search
</button>
</div>
</div>
<div id="datasets-page-results" class="row g-3"></div>
</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>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">HuggingFace Token</label>
<input type="password" class="form-control" id="hf-token" placeholder="hf_...">
<small class="text-muted">Required for pushing models to Hub and accessing gated models</small>
</div>
<div class="col-md-6">
<label class="form-label">WANDB API Key (Optional)</label>
<input type="password" class="form-control" id="wandb-key" placeholder="Optional for experiment tracking">
</div>
</div>
<button class="btn btn-primary mt-3" onclick="saveSettings()">
<i class="bi bi-save me-1"></i>Save Settings
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
let selectedTask = null;
let selectedModel = null;
let selectedDataset = null;
let datasetColumns = [];
let datasetSplits = [];
let datasetInfo = null;
let columnMapping = {};
let selectedPromptPreset = 'none';
let currentJobId = null;
// Check authentication on load
async function checkAuth() {
try {
const res = await fetch('/api/auth/check');
const data = await res.json();
if (!data.authenticated && !data.no_password_required) {
window.location.href = '/login';
}
} catch (error) {
console.error('Auth check failed:', error);
}
}
// Logout function
async function logout() {
try {
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
}
}
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();
if (sectionId === 'new-training') loadRecommendations();
if (sectionId === 'models') loadPopularModels();
if (sectionId === 'datasets') loadPopularDatasets();
}
function updateStepIndicator() {
for (let i = 1; i <= 5; i++) {
const stepEl = document.getElementById('step-' + i);
if (!stepEl) continue;
stepEl.classList.remove('active', 'completed');
if (i === 1 && selectedTask) stepEl.classList.add('completed');
else if (i === 1) stepEl.classList.add('active');
else if (i === 2 && selectedModel) stepEl.classList.add('completed');
else if (i === 2 && selectedTask) stepEl.classList.add('active');
else if (i === 3 && selectedDataset) stepEl.classList.add('completed');
else if (i === 3 && selectedModel) stepEl.classList.add('active');
else if (i === 4 && Object.keys(columnMapping).length > 0) stepEl.classList.add('completed');
else if (i === 4 && selectedDataset) stepEl.classList.add('active');
else if (i === 5) stepEl.classList.add('active');
}
}
async function loadDashboard() {
try {
const [systemRes, jobsRes] = await Promise.all([
fetch(API_BASE + '/system/resources'),
fetch(API_BASE + '/jobs/?limit=10')
]);
const system = await systemRes.json();
const jobsData = await jobsRes.json();
// System stats
document.getElementById('cpu-usage').textContent = (system.cpu?.percent || 0).toFixed(1) + '%';
document.getElementById('cpu-bar').style.width = (system.cpu?.percent || 0) + '%';
document.getElementById('memory-usage').textContent = (system.memory?.percent || 0).toFixed(1) + '%';
document.getElementById('memory-bar').style.width = (system.memory?.percent || 0) + '%';
document.getElementById('disk-usage').textContent = (system.disk?.percent || 0).toFixed(1) + '%';
document.getElementById('disk-bar').style.width = (system.disk?.percent || 0) + '%';
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);
// Job stats
const jobs = jobsData.jobs || [];
const totalJobs = jobs.length;
const activeJobs = jobs.filter(j => j.status === 'running' || j.status === 'queued').length;
const completedJobs = jobs.filter(j => j.status === 'completed').length;
const successRate = totalJobs > 0 ? (completedJobs / totalJobs * 100) : 0;
document.getElementById('stat-total-jobs').textContent = totalJobs;
document.getElementById('stat-active-jobs').textContent = activeJobs;
document.getElementById('stat-completed-jobs').textContent = completedJobs;
document.getElementById('stat-success-rate').textContent = successRate.toFixed(1) + '%';
// Current job
renderCurrentJob(jobs.find(j => j.status === 'running' || j.status === 'queued') || jobs[0]);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function renderCurrentJob(job) {
const container = document.getElementById('current-job-container');
if (!job) {
container.innerHTML = '<div class="text-center text-muted py-4"><i class="bi bi-inbox fs-1 d-block mb-2"></i>No active training. Start a new training job!</div>';
return;
}
currentJobId = job.job_id;
container.innerHTML =
'<div class="card model-card">' +
'<div class="card-body">' +
'<div class="row align-items-center mb-3">' +
'<div class="col-md-4"><strong>' + (job.name || job.model_name || 'Training Job') + '</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-5">' +
'<div class="progress" style="height: 25px;">' +
'<div class="progress-bar" style="width: ' + (job.progress || 0) + '%">' +
(job.progress || 0).toFixed(1) + '%</div></div>' +
'</div></div>' +
(job.current_loss ? '<div class="row mt-2"><div class="col-md-6"><small>Loss: ' + (job.current_loss || 0).toFixed(4) + '</small></div>' +
'<div class="col-md-6"><small>Step: ' + (job.current_step || 0) + '/' + (job.total_steps || '?') + '</small></div></div>' : '') +
(job.status === 'running' ? '<div class="mt-3"><button class="btn btn-danger btn-sm" onclick="cancelJob(\'' + job.job_id + '\')"><i class="bi bi-stop-circle me-1"></i>Cancel</button></div>' : '') +
'</div></div>';
}
async function refreshCurrentJob() {
if (!currentJobId) {
loadDashboard();
return;
}
try {
const res = await fetch(API_BASE + '/jobs/' + currentJobId);
const job = await res.json();
renderCurrentJob(job);
} catch (error) {
console.error('Error refreshing job:', error);
}
}
async function cancelJob(jobId) {
if (!confirm('Are you sure you want to cancel this job?')) return;
try {
await fetch(API_BASE + '/jobs/' + jobId + '/cancel', { method: 'POST' });
loadDashboard();
} catch (error) {
console.error('Error canceling job:', error);
}
}
function selectTask(task) {
selectedTask = task;
document.querySelectorAll('.task-card').forEach(card => card.classList.remove('selected'));
const card = document.querySelector('.task-card[data-task="' + task + '"]');
if (card) card.classList.add('selected');
document.getElementById('selected-task').value = task;
updateStepIndicator();
loadRecommendations();
}
async function loadRecommendations() {
if (!selectedTask) return;
try {
const modelRecRes = await fetch(API_BASE + '/models/recommend/' + selectedTask);
if (modelRecRes.ok) {
const modelRecs = await modelRecRes.json();
const container = document.getElementById('model-recommendations');
if (modelRecs && modelRecs.length > 0) {
container.innerHTML = '<h6 class="mb-2">Recommended Models:</h6><div class="row g-2">' +
modelRecs.slice(0, 4).map(m => '<div class="col-md-3"><div class="card task-card" onclick="selectModel(\'' + (m.model_id || m.id) + '\')">' +
'<div class="card-body py-2"><small class="fw-bold">' + (m.model_id || m.id) + '</small><br>' +
'<small class="text-muted">' + (m.estimated_vram || 'N/A') + '</small></div></div></div>').join('') + '</div>';
}
}
const datasetRecRes = await fetch(API_BASE + '/datasets/recommend/' + selectedTask);
if (datasetRecRes.ok) {
const datasetRecs = await datasetRecRes.json();
const container = document.getElementById('dataset-recommendations');
if (datasetRecs && datasetRecs.length > 0) {
container.innerHTML = '<h6 class="mb-2">Recommended Datasets:</h6><div class="row g-2">' +
datasetRecs.slice(0, 4).map(d => '<div class="col-md-3"><div class="card task-card" onclick="selectDataset(\'' + (d.dataset_id || d.id) + '\')">' +
'<div class="card-body py-2"><small class="fw-bold">' + (d.dataset_id || d.id) + '</small><br>' +
'<small class="text-muted">' + (d.size || '') + '</small></div></div></div>').join('') + '</div>';
}
}
} catch (error) {
console.error('Error loading recommendations:', error);
}
}
async function searchModels() {
const query = document.getElementById('model-search').value.trim();
if (!query) return;
const container = document.getElementById('model-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/models/search?query=' + encodeURIComponent(query) + '&limit=12');
if (!res.ok) throw new Error('Search failed');
const data = await res.json();
renderModelResults(data);
} catch (error) {
container.innerHTML = '<div class="col-12"><div class="error-message">Error: ' + error.message + '</div></div>';
}
}
function renderModelResults(models) {
const container = document.getElementById('model-results');
if (!models || models.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-3">No models found</div>';
return;
}
container.innerHTML = models.slice(0, 12).map(model => {
const id = model.id || model.model_id || model.modelId || 'unknown';
const downloads = model.downloads || 0;
return '<div class="col-md-3"><div class="card task-card" onclick="selectModel(\'' + id + '\')">' +
'<div class="card-body py-2"><small class="fw-bold text-truncate d-block">' + id + '</small>' +
'<small class="text-muted">' + downloads.toLocaleString() + ' downloads</small></div></div></div>';
}).join('');
}
function selectModel(modelId) {
selectedModel = modelId;
document.getElementById('selected-model').value = modelId;
document.querySelectorAll('#model-results .task-card').forEach(card => card.classList.remove('selected'));
if (event && event.currentTarget) event.currentTarget.classList.add('selected');
updateStepIndicator();
}
async function searchDatasets() {
const query = document.getElementById('dataset-search').value.trim();
if (!query) return;
const container = document.getElementById('dataset-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/datasets/search?query=' + encodeURIComponent(query) + '&limit=12');
if (!res.ok) throw new Error('Search failed');
const data = await res.json();
renderDatasetResults(data);
} catch (error) {
container.innerHTML = '<div class="col-12"><div class="error-message">Error: ' + error.message + '</div></div>';
}
}
function renderDatasetResults(datasets) {
const container = document.getElementById('dataset-results');
if (!datasets || datasets.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-3">No datasets found</div>';
return;
}
container.innerHTML = datasets.slice(0, 12).map(ds => {
const id = ds.id || ds.dataset_id || ds.datasetId || 'unknown';
const downloads = ds.downloads || 0;
return '<div class="col-md-3"><div class="card task-card" onclick="selectDataset(\'' + id + '\')">' +
'<div class="card-body py-2"><small class="fw-bold text-truncate d-block">' + id + '</small>' +
'<small class="text-muted">' + downloads.toLocaleString() + ' downloads</small></div></div></div>';
}).join('');
}
async function selectDataset(datasetId) {
selectedDataset = datasetId;
document.getElementById('selected-dataset').value = datasetId;
document.querySelectorAll('#dataset-results .task-card').forEach(card => card.classList.remove('selected'));
if (event && event.currentTarget) event.currentTarget.classList.add('selected');
await loadDatasetColumns(datasetId);
updateStepIndicator();
}
async function loadDatasetColumns(datasetId) {
const container = document.getElementById('column-mapping-container');
document.getElementById('form-step-4').style.display = 'block';
container.innerHTML = '<div class="text-center"><div class="spinner-border text-primary"></div> Loading dataset columns...</div>';
try {
const res = await fetch(API_BASE + '/training/dataset/preview/' + encodeURIComponent(datasetId));
if (!res.ok) throw new Error('Failed to load dataset');
datasetInfo = await res.json();
datasetColumns = datasetInfo.columns || [];
datasetSplits = datasetInfo.splits || [];
renderColumnMapping();
} catch (error) {
container.innerHTML = '<div class="error-message">Error loading dataset: ' + error.message + '</div>';
}
}
function renderColumnMapping() {
const container = document.getElementById('column-mapping-container');
// Define the roles for columns
const roles = [
{ key: 'system', label: 'System Prompt', desc: 'System instructions (optional)', icon: 'gear' },
{ key: 'input', label: 'User Input', desc: 'The user query/prompt', icon: 'person' },
{ key: 'output', label: 'Target Output', desc: 'The expected response', icon: 'robot' },
{ key: 'context', label: 'Context', desc: 'Additional context (optional)', icon: 'file-text' },
{ key: 'instruction', label: 'Instruction', desc: 'Task instruction (optional)', icon: 'lightbulb' }
];
let html = '<div class="row">';
// Splits selection
html += '<div class="col-md-3"><div class="card h-100"><div class="card-header py-2">Data Splits</div><div class="card-body">';
html += '<div class="mb-3"><label class="form-label small fw-bold">Training Split</label><select class="form-select form-select-sm" id="train-split-select">';
datasetSplits.forEach(split => {
const selected = split === 'train' ? 'selected' : '';
html += '<option value="' + split + '" ' + selected + '>' + split + '</option>';
});
html += '</select></div>';
html += '<div class="mb-3"><label class="form-label small fw-bold">Validation Split</label><select class="form-select form-select-sm" id="val-split-select">';
html += '<option value="">None</option>';
datasetSplits.forEach(split => {
const selected = split === 'validation' || split === 'val' ? 'selected' : '';
html += '<option value="' + split + '" ' + selected + '>' + split + '</option>';
});
html += '</select></div></div></div></div>';
// Column mapping
html += '<div class="col-md-5"><div class="card h-100"><div class="card-header py-2">Map Columns to Roles</div><div class="card-body">';
const suggestedMapping = datasetInfo.suggested_column_mapping || {};
roles.forEach(role => {
const suggestedCol = suggestedMapping[role.key + '_column'] || suggestedMapping[role.key];
html += '<div class="row align-items-center mb-2">';
html += '<div class="col-5"><label class="form-label small mb-0"><i class="bi bi-' + role.icon + ' me-1"></i>' + role.label + '</label></div>';
html += '<div class="col-7"><select class="form-select form-select-sm" id="col-' + role.key + '" onchange="updateColumnMapping(\'' + role.key + '\', this.value)">';
html += '<option value="">-- None --</option>';
datasetColumns.forEach(col => {
const colName = col.name || col;
let selected = '';
if (suggestedCol === colName) selected = 'selected';
else if (!suggestedCol && colName.toLowerCase().includes(role.key)) selected = 'selected';
html += '<option value="' + colName + '" ' + selected + '>' + colName + '</option>';
});
html += '</select></div></div>';
});
html += '</div></div></div>';
// Data preview
html += '<div class="col-md-4"><div class="card h-100"><div class="card-header py-2">Data Preview</div>';
html += '<div class="card-body" style="max-height: 300px; overflow-y: auto;"><table class="table table-sm table-bordered table-hover"><thead><tr>';
datasetColumns.slice(0, 4).forEach(col => {
const colName = col.name || col;
html += '<th>' + colName + '</th>';
});
html += '</tr></thead><tbody>';
const sampleData = datasetInfo.sample_data || [];
sampleData.slice(0, 5).forEach(row => {
html += '<tr>';
datasetColumns.slice(0, 4).forEach(col => {
const colName = col.name || col;
let val = row[colName] || '';
if (typeof val === 'object') val = JSON.stringify(val);
if (val.length > 30) val = val.substring(0, 30) + '...';
html += '<td>' + escapeHtml(val) + '</td>';
});
html += '</tr>';
});
html += '</tbody></table></div></div></div></div>';
container.innerHTML = html;
// Initialize column mapping
columnMapping = {};
roles.forEach(role => {
const select = document.getElementById('col-' + role.key);
if (select && select.value) {
columnMapping[role.key] = select.value;
}
});
// Show step 5
document.getElementById('form-step-5').style.display = 'block';
updateStepIndicator();
}
function updateColumnMapping(role, value) {
if (value) {
columnMapping[role] = value;
} else {
delete columnMapping[role];
}
updateStepIndicator();
}
function escapeHtml(text) {
if (text === null || text === undefined) return '';
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
function updateRangeValue(input) {
const el = document.getElementById(input.id + '-value');
if (el) el.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 '';
return new Date(dateStr).toLocaleDateString();
}
async function loadJobs() {
try {
const res = await fetch(API_BASE + '/jobs/?limit=20');
const data = await res.json();
renderJobsList(data.jobs || []);
} catch (error) {
console.error('Error loading jobs:', error);
document.getElementById('jobs-list').innerHTML = '<div class="error-message">Error loading jobs: ' + error.message + '</div>';
}
}
function renderJobsList(jobs) {
const container = document.getElementById('jobs-list');
if (!jobs || 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. Start a new training!</div>';
return;
}
container.innerHTML = jobs.map(job =>
'<div class="card mb-2 model-card" style="cursor:pointer" onclick="showJobDetail(\'' + job.job_id + '\')">' +
'<div class="card-body"><div class="row align-items-center">' +
'<div class="col-md-3"><strong>' + (job.name || 'Training Job') + '</strong><br>' +
'<small class="text-muted">' + (job.model_name || '') + '</small></div>' +
'<div class="col-md-2"><span class="job-status status-' + job.status + '">' + job.status + '</span></div>' +
'<div class="col-md-4"><div class="progress" style="height: 20px;"><div class="progress-bar" style="width: ' + (job.progress || 0) + '%">' +
(job.progress || 0).toFixed(0) + '%</div></div></div>' +
'<div class="col-md-3 text-end"><small class="text-muted">' + formatDate(job.created_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();
alert('Job: ' + job.name + '\nStatus: ' + job.status + '\nProgress: ' + (job.progress || 0).toFixed(1) + '%\nModel: ' + job.model_name + '\nDataset: ' + job.dataset_name);
} catch (error) {
console.error('Error loading job:', error);
}
}
async function loadPopularModels() {
const container = document.getElementById('models-page-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/models/popular?limit=20');
if (res.ok) {
const data = await res.json();
renderModelsPage(data);
}
} catch (error) {
container.innerHTML = '<div class="col-12 text-center text-muted">Error loading models</div>';
}
}
async function searchModelsPage() {
const query = document.getElementById('models-page-search').value.trim();
if (!query) { loadPopularModels(); return; }
const container = document.getElementById('models-page-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/models/search?query=' + encodeURIComponent(query) + '&limit=20');
if (res.ok) {
const data = await res.json();
renderModelsPage(data);
}
} catch (error) {
container.innerHTML = '<div class="col-12 text-center text-muted">Error</div>';
}
}
function renderModelsPage(models) {
const container = document.getElementById('models-page-results');
if (!models || models.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-3">No models found</div>';
return;
}
container.innerHTML = models.map(m => {
const id = m.id || m.model_id || 'unknown';
return '<div class="col-md-3"><div class="card model-card"><div class="card-body"><h6>' + id + '</h6>' +
'<small class="text-muted">' + (m.downloads || 0).toLocaleString() + ' downloads</small></div></div></div>';
}).join('');
}
async function loadPopularDatasets() {
const container = document.getElementById('datasets-page-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/datasets/popular?limit=20');
if (res.ok) {
const data = await res.json();
renderDatasetsPage(data);
}
} catch (error) {
container.innerHTML = '<div class="col-12 text-center text-muted">Error loading datasets</div>';
}
}
async function searchDatasetsPage() {
const query = document.getElementById('datasets-page-search').value.trim();
if (!query) { loadPopularDatasets(); return; }
const container = document.getElementById('datasets-page-results');
container.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary"></div></div>';
try {
const res = await fetch(API_BASE + '/datasets/search?query=' + encodeURIComponent(query) + '&limit=20');
if (res.ok) {
const data = await res.json();
renderDatasetsPage(data);
}
} catch (error) {
container.innerHTML = '<div class="col-12 text-center text-muted">Error</div>';
}
}
function renderDatasetsPage(datasets) {
const container = document.getElementById('datasets-page-results');
if (!datasets || datasets.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-3">No datasets found</div>';
return;
}
container.innerHTML = datasets.map(d => {
const id = d.id || d.dataset_id || 'unknown';
return '<div class="col-md-3"><div class="card model-card"><div class="card-body"><h6>' + id + '</h6>' +
'<small class="text-muted">' + (d.downloads || 0).toLocaleString() + ' downloads</small></div></div></div>';
}).join('');
}
function saveSettings() {
const hfToken = document.getElementById('hf-token').value;
const wandbKey = document.getElementById('wandb-key').value;
localStorage.setItem('hf_token', hfToken);
localStorage.setItem('wandb_key', wandbKey);
alert('Settings saved!');
}
// Training form submission
document.getElementById('training-form').addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedTask || !selectedModel || !selectedDataset) {
alert('Please complete all required steps: select a task, model, and dataset');
return;
}
// Check if at least input and output are mapped
if (!columnMapping.input && !columnMapping.text) {
alert('Please map at least the Input column from your dataset');
return;
}
if (!columnMapping.output && !columnMapping.text) {
alert('Please map at least the Output column from your dataset');
return;
}
const btn = document.getElementById('start-training-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting...';
const formData = {
name: document.getElementById('job_name').value || 'training-job-' + Date.now(),
task_type: selectedTask,
base_model: selectedModel,
dataset: {
name: selectedDataset,
train_split: document.getElementById('train-split-select')?.value || 'train',
validation_split: document.getElementById('val-split-select')?.value || '',
column_mapping: columnMapping,
max_length: parseInt(document.getElementById('max_length').value)
},
training_args: {
epochs: parseInt(document.getElementById('epochs').value),
batch_size: parseInt(document.getElementById('batch_size').value),
learning_rate: parseFloat(document.getElementById('learning_rate').value),
warmup_steps: parseInt(document.getElementById('warmup_steps').value)
},
peft_config: document.getElementById('use_peft').checked ? {
enabled: true,
method: document.getElementById('peft_method').value,
r: parseInt(document.getElementById('lora_r').value),
alpha: parseInt(document.getElementById('lora_alpha').value),
dropout: parseFloat(document.getElementById('lora_dropout').value)
} : null,
prompt_template: {
preset: selectedPromptPreset
}
};
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 (res.ok && data.job_id) {
alert('Training started successfully! Job ID: ' + data.job_id);
currentJobId = data.job_id;
showSection('dashboard');
} else {
alert('Error starting training: ' + (data.detail || JSON.stringify(data)));
}
} catch (error) {
alert('Error: ' + error.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-rocket-takeoff me-2"></i>Start Training';
}
});
// Auto-refresh current job
setInterval(() => {
if (currentJobId) {
refreshCurrentJob();
}
}, 5000);
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
const savedToken = localStorage.getItem('hf_token');
const savedWandb = localStorage.getItem('wandb_key');
if (savedToken) document.getElementById('hf-token').value = savedToken;
if (savedWandb) document.getElementById('wandb-key').value = savedWandb;
loadDashboard();
setInterval(loadDashboard, 30000);
updateStepIndicator();
});
</script>
</body>
</html>