| <!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"> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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; |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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(); |
| |
| |
| 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); |
| |
| |
| 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) + '%'; |
| |
| |
| 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'); |
| |
| |
| 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">'; |
| |
| |
| 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>'; |
| |
| |
| 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>'; |
| |
| |
| 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; |
| |
| |
| columnMapping = {}; |
| roles.forEach(role => { |
| const select = document.getElementById('col-' + role.key); |
| if (select && select.value) { |
| columnMapping[role.key] = select.value; |
| } |
| }); |
| |
| |
| 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!'); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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'; |
| } |
| }); |
| |
| |
| 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> |