Download app/templates/dashboard.html from vectorplasticity/universal-model-trainer: direct link, hf CLI and curl.
- Browser
- Download file 73.7 kB
-
https://huggingface.co/spaces/vectorplasticity/universal-model-trainer/resolve/main/app/templates/dashboard.html
- Command line
-
hf download hf://spaces/vectorplasticity/universal-model-trainer/app/templates/dashboard.html
-
curl -L -o dashboard.html https://huggingface.co/spaces/vectorplasticity/universal-model-trainer/resolve/main/app/templates/dashboard.html
73.7 kB
| <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 ; | |
| 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 ; | |
| 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> |