/* Soup Web UI — Frontend Application */ const API = ''; // same origin // --- State --- let currentPage = 'dashboard'; let runsData = []; let systemInfo = null; let chatMessages = []; let chatEndpoint = null; // --- Navigation --- function navigate(page) { currentPage = page; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('page-' + page).classList.add('active'); document.querySelector(`[data-page="${page}"]`).classList.add('active'); if (page === 'dashboard') loadDashboard(); else if (page === 'training') loadTrainingPage(); else if (page === 'data') { /* loaded on demand */ } else if (page === 'chat') loadChatPage(); } // --- API Helpers --- async function api(path, opts = {}) { const resp = await fetch(API + path, { headers: { 'Content-Type': 'application/json' }, ...opts, }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || 'API error'); } return resp.json(); } function formatDuration(secs) { if (!secs) return '-'; if (secs < 60) return `${Math.round(secs)}s`; if (secs < 3600) return `${Math.floor(secs / 60)}m ${Math.round(secs % 60)}s`; return `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`; } function formatDate(iso) { if (!iso) return '-'; const d = new Date(iso); return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function statusBadge(status) { const map = { completed: 'badge-success', failed: 'badge-danger', running: 'badge-warning', }; return `${status}`; } function truncate(str, len = 30) { if (!str) return '-'; return str.length > len ? str.substring(0, len) + '...' : str; } // --- Dashboard --- async function loadDashboard() { try { const [runsResp, sysResp] = await Promise.all([ api('/api/runs?limit=100'), api('/api/system'), ]); runsData = runsResp.runs; systemInfo = sysResp; renderDashboard(); } catch (err) { document.getElementById('dashboard-content').innerHTML = `
Error loading dashboard: ${err.message}
`; } } function renderDashboard() { const completed = runsData.filter(r => r.status === 'completed'); const failed = runsData.filter(r => r.status === 'failed'); const running = runsData.filter(r => r.status === 'running'); const bestLoss = completed.length ? Math.min(...completed.map(r => r.final_loss).filter(Boolean)).toFixed(4) : '-'; document.getElementById('dashboard-content').innerHTML = `
${runsData.length}
Total Runs
${completed.length}
Completed
${running.length}
Running
${bestLoss}
Best Loss
System
Device: ${systemInfo.device_name}  |  GPU Memory: ${systemInfo.gpu_info.memory_total}  |  Python: ${systemInfo.python_version}  |  Soup: v${systemInfo.version}
Recent Runs
${runsData.length === 0 ? '
No runs yet
Start training with "soup train" or use the New Training page
' : renderRunsTable(runsData.slice(0, 20)) }
`; } function renderRunsTable(runs) { return `
${runs.map(r => ` `).join('')}
Run ID Name Model Task Status Loss Duration Date
${r.run_id.substring(0, 20)}... ${r.experiment_name || '-'} ${truncate(r.base_model)} ${r.task || 'sft'} ${statusBadge(r.status)} ${r.final_loss ? r.final_loss.toFixed(4) : '-'} ${formatDuration(r.duration_secs)} ${formatDate(r.created_at)}
`; } async function deleteRun(runId) { if (!confirm('Delete this run and all its metrics?')) return; try { await api(`/api/runs/${runId}`, { method: 'DELETE' }); loadDashboard(); } catch (err) { alert('Error: ' + err.message); } } // --- Run Detail Modal --- let lossChart = null; async function showRunDetail(runId) { const modal = document.getElementById('run-modal'); const body = document.getElementById('run-modal-body'); modal.classList.add('active'); body.innerHTML = '
Loading...
'; try { const [run, metricsResp] = await Promise.all([ api(`/api/runs/${runId}`), api(`/api/runs/${runId}/metrics`), ]); const config = run.config_json ? JSON.parse(run.config_json) : {}; const metrics = metricsResp.metrics; body.innerHTML = `
Run ID
${run.run_id}
Status
${statusBadge(run.status)}
Model
${run.base_model || '-'}
Task
${run.task || 'sft'}
Device
${run.device_name || run.device || '-'}
Duration
${formatDuration(run.duration_secs)}
Initial Loss
${run.initial_loss ? run.initial_loss.toFixed(4) : '-'}
Final Loss
${run.final_loss ? run.final_loss.toFixed(4) : '-'}
${metrics.length > 0 ? `
Loss Curve
Learning Rate
` : ''}
Config
${JSON.stringify(config, null, 2)}
`; if (metrics.length > 0) { renderCharts(metrics); } } catch (err) { body.innerHTML = `
Error: ${err.message}
`; } } function renderCharts(metrics) { const steps = metrics.map(m => m.step); const losses = metrics.map(m => m.loss); const lrs = metrics.map(m => m.lr); // Loss chart const lossCtx = document.getElementById('loss-chart'); if (lossCtx) { if (lossChart) lossChart.destroy(); lossChart = new Chart(lossCtx, { type: 'line', data: { labels: steps, datasets: [{ label: 'Loss', data: losses, borderColor: '#c0512d', backgroundColor: 'rgba(192, 81, 45, 0.1)', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { title: { display: true, text: 'Step', color: '#a09088' }, ticks: { color: '#a09088' }, grid: { color: 'rgba(58,48,64,0.5)' } }, y: { title: { display: true, text: 'Loss', color: '#a09088' }, ticks: { color: '#a09088' }, grid: { color: 'rgba(58,48,64,0.5)' } }, }, }, }); } // LR chart const lrCtx = document.getElementById('lr-chart'); if (lrCtx) { new Chart(lrCtx, { type: 'line', data: { labels: steps, datasets: [{ label: 'Learning Rate', data: lrs, borderColor: '#e8975a', backgroundColor: 'rgba(232, 151, 90, 0.1)', fill: true, tension: 0.3, pointRadius: 0, borderWidth: 2, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { title: { display: true, text: 'Step', color: '#a09088' }, ticks: { color: '#a09088' }, grid: { color: 'rgba(58,48,64,0.5)' } }, y: { title: { display: true, text: 'LR', color: '#a09088' }, ticks: { color: '#a09088' }, grid: { color: 'rgba(58,48,64,0.5)' } }, }, }, }); } } function closeModal() { document.getElementById('run-modal').classList.remove('active'); } // --- New Training Page --- async function loadTrainingPage() { try { const [templatesResp, statusResp] = await Promise.all([ api('/api/templates'), api('/api/train/status'), ]); renderTrainingPage(templatesResp.templates, statusResp); } catch (err) { document.getElementById('training-content').innerHTML = `
Error: ${err.message}
`; } } function renderTrainingPage(templates, status) { const templateNames = Object.keys(templates); const editorId = 'config-editor'; document.getElementById('training-content').innerHTML = `
Template
Config (YAML)
Training Status
${status.running ? `
Running PID: ${status.pid}
` : '
No training in progress
' }
Quick Reference
Tasks: sft, dpo, grpo
Backends: transformers, unsloth
Modalities: text, vision
Quantization: 4bit, 8bit, none
Formats: alpaca, sharegpt, chatml, dpo, llava, sharegpt4v
`; // Store templates globally window._templates = templates; } function loadTemplate() { const sel = document.getElementById('template-select'); const editor = document.getElementById('config-editor'); if (sel.value && window._templates[sel.value]) { editor.value = window._templates[sel.value]; } } async function validateConfig() { const yaml = document.getElementById('config-editor').value; const statusEl = document.getElementById('config-status'); try { const result = await api('/api/config/validate', { method: 'POST', body: JSON.stringify({ yaml }), }); if (result.valid) { statusEl.innerHTML = 'Config is valid!'; } else { statusEl.innerHTML = `Invalid: ${result.error}`; } } catch (err) { statusEl.innerHTML = `Error: ${err.message}`; } } async function startTraining() { const yaml = document.getElementById('config-editor').value; if (!yaml.trim()) { alert('Please enter a config'); return; } if (!confirm('Start training with this config?')) return; try { const result = await api('/api/train/start', { method: 'POST', body: JSON.stringify({ config_yaml: yaml }), }); document.getElementById('config-status').innerHTML = `Training started! PID: ${result.pid}`; // Refresh status loadTrainingPage(); } catch (err) { document.getElementById('config-status').innerHTML = `Error: ${err.message}`; } } async function stopTraining() { if (!confirm('Stop the current training run?')) return; try { await api('/api/train/stop', { method: 'POST' }); loadTrainingPage(); } catch (err) { alert('Error: ' + err.message); } } // --- Data Explorer --- async function inspectData() { const path = document.getElementById('data-path').value; if (!path.trim()) { alert('Enter a file path'); return; } const limit = parseInt(document.getElementById('data-limit').value) || 50; const content = document.getElementById('data-content'); content.innerHTML = '
Loading...
'; try { const result = await api('/api/data/inspect', { method: 'POST', body: JSON.stringify({ path, limit }), }); renderDataResults(result); } catch (err) { content.innerHTML = `
Error: ${err.message}
`; } } function renderDataResults(data) { const content = document.getElementById('data-content'); content.innerHTML = `
${data.total}
Total Entries
${data.format}
Detected Format
${data.keys.length}
Fields
Fields: ${data.keys.join(', ')}
Sample Data (${data.sample.length} of ${data.total})
${data.sample.map((entry, idx) => `
#${idx + 1}
${Object.entries(entry).map(([key, val]) => `
${key}: ${typeof val === 'object' ? JSON.stringify(val).substring(0, 200) : String(val).substring(0, 200)}
`).join('')}
`).join('')}
`; } // --- Model Chat --- function loadChatPage() { // Just ensure the page renders with current messages renderChatMessages(); } function renderChatMessages() { const container = document.getElementById('chat-messages'); if (!container) return; if (chatMessages.length === 0) { container.innerHTML = `
No messages yet
Enter a server URL and start chatting
`; return; } container.innerHTML = chatMessages.map(msg => `
${msg.role}
${escapeHtml(msg.content)}
`).join(''); container.scrollTop = container.scrollHeight; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function sendChatMessage() { const input = document.getElementById('chat-input'); const serverUrl = document.getElementById('chat-server').value.trim(); const msg = input.value.trim(); if (!msg) return; if (!serverUrl) { alert('Enter a server URL (e.g., http://localhost:8000)'); return; } chatMessages.push({ role: 'user', content: msg }); input.value = ''; renderChatMessages(); try { const resp = await fetch(serverUrl + '/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: chatMessages.map(m => ({ role: m.role, content: m.content })), max_tokens: 512, temperature: 0.7, }), }); const data = await resp.json(); const reply = data.choices[0].message.content; chatMessages.push({ role: 'assistant', content: reply }); renderChatMessages(); } catch (err) { chatMessages.push({ role: 'assistant', content: `[Error: ${err.message}]` }); renderChatMessages(); } } function clearChat() { chatMessages = []; renderChatMessages(); } function handleChatKey(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendChatMessage(); } } // --- Init --- document.addEventListener('DOMContentLoaded', () => { navigate('dashboard'); });