| <!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"> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <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> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <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> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <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"> |
| |
| </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; |
| |
| |
| 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(); |
| } |
| |
| |
| 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(); |
| |
| |
| 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) + '%'; |
| |
| |
| 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); |
| |
| |
| 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(''); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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'); |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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'); |
| } |
| }); |
| |
| |
| document.getElementById('use_peft').addEventListener('change', function() { |
| document.getElementById('peft-options').style.display = this.checked ? 'block' : 'none'; |
| }); |
| |
| |
| 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(); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| loadDashboard(); |
| setInterval(loadDashboard, 30000); |
| }); |
| </script> |
| </body> |
| </html> |