| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Efficient Transformer Toolkit – Dashboard + Terminal</title> | |
| <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" /> | |
| <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script> | |
| <style> | |
| /* same as before – I'll keep it compact */ | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: 'Segoe UI', sans-serif; height: 100vh; background: #1e1e2f; color: #ccc; } | |
| .login-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #1e1e2f; display: flex; align-items: center; justify-content: center; z-index: 1000; } | |
| .login-box { background: #2d2d44; padding: 2rem; border-radius: 8px; width: 360px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); } | |
| .login-box h2 { color: #fff; margin-bottom: 1.5rem; text-align: center; } | |
| .login-box input { width: 100%; padding: 0.7rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid #555; background: #3b3b55; color: #ddd; } | |
| .login-box button { width: 100%; padding: 0.7rem; background: #7baeff; border: none; border-radius: 4px; color: #fff; font-weight: bold; cursor: pointer; } | |
| .login-box .error { color: #ff6b6b; font-size: 0.85rem; margin-bottom: 0.5rem; } | |
| .app { display: flex; height: 100vh; } | |
| .sidebar { width: 340px; background: #2d2d44; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.8rem; } | |
| .sidebar h2 { color: #fff; margin-bottom: 0.3rem; font-size: 1.3rem; display: flex; justify-content: space-between; align-items: center; } | |
| .sidebar label { font-size: 0.85rem; color: #aaa; margin-bottom: 2px; display: block; } | |
| select, input { width: 100%; padding: 0.5rem; border-radius: 4px; border: 1px solid #555; background: #3b3b55; color: #ddd; font-size: 0.9rem; } | |
| .btn { padding: 0.6rem 1rem; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; color: #fff; } | |
| .btn-start { background: #28a745; } | |
| .btn-stop { background: #dc3545; } | |
| .btn-update { background: #ffc107; color: #000; } | |
| .btn-logout { background: #6c757d; font-size: 0.8rem; padding: 0.3rem 0.6rem; } | |
| .btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .main { flex: 1; display: flex; flex-direction: column; } | |
| .top-bar { background: #2d2d44; padding: 0.8rem 1.5rem; display: flex; align-items: center; gap: 1rem; } | |
| .progress-container { flex: 1; background: #444; border-radius: 10px; height: 20px; margin: 0 1rem; position: relative; } | |
| .progress-bar { height: 100%; background: #28a745; border-radius: 10px; width: 0%; transition: width 0.3s; } | |
| .progress-text { position: absolute; left: 50%; transform: translateX(-50%); color: #fff; font-size: 0.75rem; line-height: 20px; white-space: nowrap; } | |
| .gpu-info { display: flex; gap: 1rem; align-items: center; font-size: 0.85rem; } | |
| .panels { flex: 1; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; gap: 0.8rem; padding: 0.8rem; overflow-y: auto; } | |
| .panel { background: #2d2d44; border-radius: 6px; padding: 1rem; display: flex; flex-direction: column; } | |
| .panel h3 { margin-bottom: 0.6rem; font-size: 1rem; color: #fff; } | |
| .log-viewer { grid-column: span 2; overflow-y: auto; background: #1a1a2e; color: #0f0; font-family: monospace; font-size: 0.8rem; padding: 0.5rem; height: 200px; white-space: pre-wrap; } | |
| .job-list { font-size: 0.8rem; max-height: 200px; overflow-y: auto; background: #1a1a2e; padding: 0.3rem; border-radius: 4px; margin-top: 0.5rem; } | |
| .job-item { padding: 0.3rem 0.4rem; cursor: pointer; border-bottom: 1px solid #444; display: flex; justify-content: space-between; } | |
| .job-item:hover { background: #3b3b60; } | |
| .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; } | |
| .queued { background: yellow; } | |
| .running { background: cyan; } | |
| .completed { background: lime; } | |
| .failed { background: red; } | |
| .stopped { background: orange; } | |
| .toast { position: fixed; top: 1rem; right: 1rem; padding: 0.8rem 1.2rem; border-radius: 6px; color: #fff; z-index: 2000; animation: fadeIn 0.3s; } | |
| .toast-success { background: #28a745; } | |
| .toast-error { background: #dc3545; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } | |
| #terminal-container { height: 300px; background: black; border-radius: 6px; overflow: hidden; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Login overlay (same as before) --> | |
| <div class="login-overlay" id="loginOverlay"> | |
| <div class="login-box"> | |
| <h2 id="authTitle">🔐 Login</h2> | |
| <div class="error" id="authError"></div> | |
| <div id="loginFields"> | |
| <input type="text" id="loginUsername" placeholder="Username" autocomplete="username"> | |
| <input type="password" id="loginPassword" placeholder="Password" autocomplete="current-password"> | |
| <button onclick="doLogin()">Sign In</button> | |
| </div> | |
| <div id="registerFields" style="display:none;"> | |
| <input type="text" id="regUsername" placeholder="Choose a username"> | |
| <input type="password" id="regPassword" placeholder="Create password"> | |
| <input type="password" id="regPassword2" placeholder="Confirm password"> | |
| <button onclick="doRegister()">Create Account</button> | |
| </div> | |
| <div style="margin-top:1rem; text-align:center; font-size:0.85rem;"><span id="toggleText">Don't have an account?</span> <a href="#" id="toggleLink" onclick="toggleAuth()">Create one</a></div> | |
| </div> | |
| </div> | |
| <div class="app" id="app" style="display:none;"> | |
| <!-- Sidebar (same as before) --> | |
| <div class="sidebar"> | |
| <h2>🚀 Experiment Config <button class="btn btn-logout" onclick="doLogout()">Logout</button></h2> | |
| <label>Model</label><select id="modelSelect"></select> | |
| <label>Task</label><select id="taskSelect"></select> | |
| <label>Attention Type</label><select id="attentionSelect"></select> | |
| <label>Training Method</label><select id="methodSelect"><option value="baseline">Baseline</option><option value="lora">LoRA</option></select> | |
| <label>Epochs</label><input type="number" id="epochs" value="3"> | |
| <label>Batch Size</label><input type="number" id="batchSize" value="16"> | |
| <label>Learning Rate</label><input type="number" id="learningRate" value="2e-5" step="1e-6"> | |
| <label>Compression Rank</label><input type="number" id="rank" value="64"> | |
| <label>Max Seq Length</label><input type="number" id="maxLen" value="128"> | |
| <div style="display:flex; gap:0.5rem;"><button class="btn btn-start" id="startBtn">▶ Start Training</button><button class="btn btn-stop" id="stopBtn" disabled>⏹ Stop</button></div> | |
| <div><button class="btn btn-update" id="updateBtn" disabled>✎ Update Config</button></div> | |
| <div class="job-list" id="jobList">Loading jobs...</div> | |
| </div> | |
| <div class="main"> | |
| <div class="top-bar"> | |
| <span id="jobStatus">⚪ Idle</span> | |
| <div class="progress-container"><div class="progress-bar" id="progressBar"></div><div class="progress-text" id="progressText">0%</div></div> | |
| <div class="gpu-info" id="gpuInfo">GPU: N/A</div> | |
| </div> | |
| <div class="panels"> | |
| <div class="panel"><h3>Training Loss</h3><div id="lossPlot" style="flex:1; height:200px;"></div></div> | |
| <div class="panel"><h3>Evaluation Metrics</h3><div id="evalPlot" style="flex:1; height:200px;"></div></div> | |
| <div class="panel log-viewer"><h3>Logs</h3><div id="logContent" style="flex:1; overflow-y:auto;"></div></div> | |
| <div class="panel"><h3>System</h3><div id="systemInfo">CPU: -- | Mem: --</div></div> | |
| <!-- TERMINAL PANEL --> | |
| <div class="panel" style="grid-column: span 2; display: flex; flex-direction: column;"> | |
| <h3>Live Terminal (run commands: ls, nvidia-smi, top, etc.)</h3> | |
| <div id="terminal-container" style="background: black; border-radius: 6px; overflow: hidden;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ------------------- AUTH & STATE ------------------- | |
| const API_BASE = 'http://127.0.0.1:8000/api'; | |
| const WS_BASE = 'ws://127.0.0.1:8000/ws/jobs'; | |
| let authToken = localStorage.getItem('authToken'); | |
| let currentJobId = null; | |
| let ws = null; | |
| let terminalWs = null; | |
| let term = null; | |
| let plotData = {lossSteps:[], losses:[], evalSteps:[], evalLosses:[]}; | |
| // Toast | |
| function showToast(msg, type='success'){ /* keep as before */ } | |
| // Auth functions (same as before, keep them) | |
| async function doLogin() { /* unchanged */ } | |
| function toggleAuth() { /* unchanged */ } | |
| async function doRegister() { /* unchanged */ } | |
| function doLogout() { localStorage.removeItem('authToken'); if(ws) ws.close(); if(terminalWs) terminalWs.close(); location.reload(); } | |
| function authHeaders() { return authToken ? {'Authorization': `Token ${authToken}`} : {}; } | |
| // API helpers | |
| async function apiGet(url) { /* unchanged */ } | |
| async function apiPost(url, body) { /* unchanged */ } | |
| // Load config options | |
| async function loadConfigOptions() { /* unchanged */ } | |
| function populateSelect(id, options, defaultVal) { /* unchanged */ } | |
| // Job list | |
| async function loadJobList() { /* unchanged */ } | |
| // Config and job control | |
| function getConfig() { return { model_name: document.getElementById('modelSelect').value, task_name: document.getElementById('taskSelect').value, attention_type: document.getElementById('attentionSelect').value, method: document.getElementById('methodSelect').value, epochs: parseInt(document.getElementById('epochs').value), batch_size: parseInt(document.getElementById('batchSize').value), learning_rate: parseFloat(document.getElementById('learningRate').value), compression_rank: parseInt(document.getElementById('rank').value), max_seq_length: parseInt(document.getElementById('maxLen').value) }; } | |
| async function startTraining() { try { const data = await apiPost('/train/', getConfig()); if(data) selectJob(data.id); showToast('Training started'); } catch(e) { showToast('Start failed','error'); } } | |
| async function stopTraining() { if(currentJobId) await apiPost(`/jobs/${currentJobId}/stop/`); showToast('Stop signal sent'); } | |
| async function updateConfig() { if(currentJobId) { await fetch(`${API_BASE}/jobs/${currentJobId}/config/`, { method:'PATCH', headers:{...authHeaders(),'Content-Type':'application/json'}, body:JSON.stringify(getConfig()) }); showToast('Config updated'); } } | |
| // WebSocket for job updates | |
| function connectWebSocket(jobId) { if(ws) ws.close(); ws = new WebSocket(`${WS_BASE}/${jobId}/`); ws.onmessage = handleWSMessage; ws.onerror = (e) => console.log('WS error'); ws.onclose = () => setTimeout(() => { if(currentJobId) connectWebSocket(currentJobId); }, 3000); } | |
| // Terminal WebSocket and xterm.js initialization | |
| function initTerminal() { | |
| if (terminalWs) terminalWs.close(); | |
| terminalWs = new WebSocket('ws://127.0.0.1:8000/ws/terminal/'); | |
| term = new Terminal({ cursorBlink: true, theme: { background: '#000000', foreground: '#00ff00' } }); | |
| term.open(document.getElementById('terminal-container')); | |
| term.onData(data => { if (terminalWs.readyState === WebSocket.OPEN) terminalWs.send(data); }); | |
| terminalWs.onmessage = ev => term.write(ev.data); | |
| terminalWs.onerror = err => console.error('Terminal WS error', err); | |
| terminalWs.onclose = () => { | |
| term.write('\r\n\x1b[31mTerminal disconnected – reconnecting...\x1b[0m\r\n'); | |
| setTimeout(initTerminal, 3000); | |
| }; | |
| } | |
| async function selectJob(jobId) { | |
| currentJobId = jobId; | |
| document.getElementById('startBtn').disabled = true; | |
| document.getElementById('stopBtn').disabled = false; | |
| document.getElementById('updateBtn').disabled = false; | |
| connectWebSocket(jobId); | |
| const job = await apiGet(`/jobs/${jobId}/`); | |
| if(job) { | |
| document.getElementById('jobStatus').innerHTML = `<span class="status-dot ${job.status}"></span>${job.status}`; | |
| document.getElementById('logContent').innerText = job.logs || ''; | |
| if(job.current_step && job.total_steps) { | |
| let pct = (job.current_step/job.total_steps)*100; | |
| document.getElementById('progressBar').style.width = pct+'%'; | |
| document.getElementById('progressText').innerText = Math.round(pct)+'%'; | |
| } | |
| } | |
| } | |
| function handleWSMessage(event) { | |
| let msg = JSON.parse(event.data); | |
| switch(msg.type) { | |
| case 'status': | |
| document.getElementById('jobStatus').innerHTML = `<span class="status-dot ${msg.status}"></span>${msg.status}`; | |
| if(['completed','stopped','failed'].includes(msg.status)) { | |
| document.getElementById('startBtn').disabled = false; | |
| document.getElementById('stopBtn').disabled = true; | |
| document.getElementById('updateBtn').disabled = true; | |
| if(ws) ws.close(); | |
| loadJobList(); | |
| } | |
| break; | |
| case 'log': | |
| let logEl = document.getElementById('logContent'); | |
| logEl.innerText += msg.data + '\n'; | |
| logEl.scrollTop = logEl.scrollHeight; | |
| break; | |
| case 'metrics': | |
| if(msg.data.loss !== undefined) { plotData.lossSteps.push(plotData.lossSteps.length+1); plotData.losses.push(msg.data.loss); Plotly.update('lossPlot', {x:[plotData.lossSteps], y:[plotData.losses]}, {}, [0]); } | |
| if(msg.data.eval_loss !== undefined) { plotData.evalSteps.push(plotData.evalSteps.length+1); plotData.evalLosses.push(msg.data.eval_loss); Plotly.update('evalPlot', {x:[plotData.evalSteps], y:[plotData.evalLosses]}, {}, [0]); } | |
| break; | |
| case 'progress': | |
| let pct = Math.min(100, Math.round(msg.progress || 0)); | |
| document.getElementById('progressBar').style.width = pct+'%'; | |
| document.getElementById('progressText').innerText = pct+'% (Remaining: '+formatTime(msg.eta_seconds)+')'; | |
| break; | |
| } | |
| } | |
| function formatTime(sec) { if(!sec || sec<0) return '--'; let h=Math.floor(sec/3600), m=Math.floor((sec%3600)/60), s=Math.round(sec%60); return `${h}h ${m}m ${s}s`; } | |
| function getAuthToken() { | |
| return localStorage.getItem('authToken'); | |
| } | |
| async function apiFetch(url, options = {}) { | |
| const token = getAuthToken(); | |
| const headers = { | |
| 'Content-Type': 'application/json', | |
| ...(token ? {'Authorization': `Token ${token}`} : {}), | |
| ...options.headers | |
| }; | |
| const response = await fetch(url, {...options, headers}); | |
| if (response.status === 401) { | |
| window.location.href = '/login/'; // or redirect to login | |
| } | |
| return response; | |
| } | |
| async function pollSystem() { | |
| const res = await apiFetch('/api/system/status/'); | |
| const data = await res.json(); | |
| // ... | |
| } | |
| // System polling | |
| async function pollSystem() { | |
| try { | |
| let sys = await apiGet('/system/status/'); | |
| if(sys) document.getElementById('systemInfo').innerHTML = `CPU: ${sys.cpu}% | Mem: ${sys.memory}%`; | |
| let gpu = await apiGet('/system/gpu/'); | |
| if(gpu?.cuda_available) document.getElementById('gpuInfo').innerHTML = `GPU: ${gpu.memory_allocated?.toFixed(2)} / ${gpu.memory_total?.toFixed(2)} GB`; | |
| else document.getElementById('gpuInfo').innerHTML = 'GPU: N/A'; | |
| } catch(e) {} | |
| } | |
| // Initialize app | |
| async function initApp() { | |
| await loadConfigOptions(); | |
| await loadJobList(); | |
| setInterval(loadJobList, 10000); | |
| setInterval(pollSystem, 3000); | |
| pollSystem(); | |
| initTerminal(); // start terminal connection | |
| } | |
| // Event listeners | |
| document.getElementById('startBtn').addEventListener('click', startTraining); | |
| document.getElementById('stopBtn').addEventListener('click', stopTraining); | |
| document.getElementById('updateBtn').addEventListener('click', updateConfig); | |
| // Login check | |
| if(authToken) { document.getElementById('loginOverlay').style.display = 'none'; document.getElementById('app').style.display = 'flex'; initApp(); } | |
| else { document.getElementById('loginOverlay').style.display = 'flex'; document.getElementById('app').style.display = 'none'; } | |
| </script> | |
| </body> | |
| </html> |
Xet Storage Details
- Size:
- 16.4 kB
- Xet hash:
- 2b7fd743b8fd4cafee5e4902ec927d180338f1338c461f0eb7f6990b4d575337
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.