| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ExocoreV1 — Train</title> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> |
| <style> |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { font-family: 'Segoe UI', sans-serif; background: #0d0d0f; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } |
| header { background: #111116; border-bottom: 1px solid #222; padding: 10px 20px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; } |
| header h1 { font-size: 1.1rem; color: #a78bfa; font-weight: 700; letter-spacing: 1px; } |
| header nav a { color: #888; text-decoration: none; margin-left: 14px; font-size: 0.85rem; } |
| header nav a:hover { color: #a78bfa; } |
| .layout { display: flex; flex: 1; overflow: hidden; } |
| .left { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } |
| .right { width: 360px; border-left: 1px solid #1e1e2a; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } |
| .card { background: #111116; border: 1px solid #1e1e2a; border-radius: 10px; padding: 14px; } |
| .card h3 { font-size: 0.82rem; color: #c4b5fd; font-weight: 600; margin-bottom: 11px; text-transform: uppercase; letter-spacing: 0.5px; } |
| .tabs { display: flex; gap: 4px; margin-bottom: 14px; } |
| .tab { background: #16161f; border: 1px solid #252535; color: #666; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; } |
| .tab.active { background: #1a1628; border-color: #a78bfa; color: #a78bfa; } |
| .fr { display: flex; flex-direction: column; gap: 3px; margin-bottom: 9px; } |
| .fr label { font-size: 0.7rem; color: #555; } |
| .fr input, .fr textarea, .fr select { background: #16161f; border: 1px solid #252535; color: #ddd; border-radius: 5px; padding: 7px 10px; font-size: 0.82rem; width: 100%; font-family: inherit; } |
| .fr input:focus, .fr textarea:focus { outline: none; border-color: #7c3aed; } |
| .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } |
| .btn { border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; font-size: 0.82rem; font-weight: 500; } |
| .btn-p { background: #7c3aed; color: white; } |
| .btn-p:hover { background: #6d28d9; } |
| .btn-p:disabled { background: #252535; color: #555; cursor: not-allowed; } |
| .btn-d { background: #4a1010; color: #fca5a5; } |
| .btn-d:hover { background: #5f1414; } |
| .btn-s { background: #16161f; border: 1px solid #252535; color: #888; } |
| .btn-s:hover { border-color: #a78bfa; color: #a78bfa; } |
| .btn-sm { padding: 4px 10px; font-size: 0.75rem; } |
| .progress-track { background: #16161f; border-radius: 20px; height: 6px; margin: 7px 0; overflow: hidden; } |
| .progress-fill { background: linear-gradient(90deg, #7c3aed, #a78bfa); height: 100%; border-radius: 20px; transition: width 0.3s; } |
| .log-box { background: #080810; border: 1px solid #1a1a24; border-radius: 5px; padding: 8px; font-family: monospace; font-size: 0.73rem; color: #666; max-height: 130px; overflow-y: auto; white-space: pre-wrap; } |
| .stat-row { display: flex; align-items: center; gap: 7px; font-size: 0.78rem; color: #666; margin-bottom: 7px; } |
| .dot { width: 7px; height: 7px; border-radius: 50%; background: #333; flex-shrink: 0; } |
| .dot.running { background: #22c55e; animation: pulse 1s infinite; } |
| .dot.done { background: #22c55e; } |
| .dot.error { background: #ef4444; } |
| @keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} } |
| .mcard { background: #16161f; border: 1px solid #252535; border-radius: 7px; padding: 9px 12px; margin-bottom: 6px; display: flex; align-items: center; justify-content: space-between; } |
| .mcard .mn { font-size: 0.83rem; color: #ddd; font-weight: 500; } |
| .mcard .mt { font-size: 0.7rem; color: #555; margin-top: 2px; } |
| .pb { font-size: 0.68rem; padding: 2px 7px; border-radius: 20px; background: #1a1628; border: 1px solid #4c1d95; color: #a78bfa; margin-left: 5px; } |
| .chart-wrap { position: relative; height: 220px; margin-top: 8px; } |
| .arch-tree { font-size: 0.78rem; } |
| .arch-node { margin: 3px 0; display: flex; align-items: center; gap: 6px; } |
| .arch-node .label { background: #16161f; border: 1px solid #252535; border-radius: 5px; padding: 4px 10px; font-size: 0.75rem; color: #c4b5fd; flex: 1; } |
| .arch-node.block .label { border-color: #4c1d95; background: #1a1628; } |
| .arch-node.sub .label { border-color: #1e3a5f; background: #111820; color: #93c5fd; margin-left: 20px; font-size: 0.72rem; } |
| .arch-node.head-node .label { border-color: #134e2a; background: #0d1f16; color: #86efac; } |
| .arch-line { color: #333; font-size: 0.65rem; margin-left: 8px; } |
| ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-thumb { background: #2a2a3a; border-radius: 3px; } |
| </style> |
| </head> |
| <body> |
| <header> |
| <h1>⬡ ExocoreV1</h1> |
| <nav><a href="/">Chat</a><a href="/train">Train</a></nav> |
| </header> |
| <div class="layout"> |
| |
| <div class="left"> |
| <div class="tabs"> |
| <div class="tab active" onclick="showTab('train')">Train</div> |
| <div class="tab" onclick="showTab('auto')">Auto-Train (Gemini)</div> |
| </div> |
|
|
| |
| <div id="tab-train"> |
| <div class="card"> |
| <h3>Training Data</h3> |
| <div class="fr"> |
| <label>Training Text</label> |
| <textarea id="trainText" rows="7" placeholder="Paste training text here (min 100 chars)…"></textarea> |
| </div> |
| <div style="display:flex;align-items:center;gap:8px;"> |
| <button class="btn btn-s btn-sm" onclick="loadTrainTxt()">Load train.txt</button> |
| <span id="charCount" style="font-size:0.73rem;color:#555;"></span> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h3>Model Configuration</h3> |
| <div class="fr"><label>Model Name</label><input type="text" id="modelName" value="Exocore"></div> |
| <div class="grid2"> |
| <div class="fr"><label>Epochs</label><input type="number" id="epochs" value="60" min="1"></div> |
| <div class="fr"><label>Embedding Dim</label><input type="number" id="nEmbd" value="384" min="64"></div> |
| <div class="fr"><label>Layers</label><input type="number" id="nLayer" value="6" min="1"></div> |
| <div class="fr"><label>Block Size</label><input type="number" id="blockSize" value="256" min="32"></div> |
| <div class="fr"><label>Batch Size</label><input type="number" id="batchSize" value="16" min="1"></div> |
| <div class="fr"><label>Learning Rate</label><input type="number" id="lr" value="0.0002" step="0.00001"></div> |
| </div> |
| <div style="display:flex;gap:8px;"> |
| <button class="btn btn-p" id="trainBtn" onclick="startTraining()">Start Training</button> |
| <button class="btn btn-d" id="stopBtn" onclick="stopTraining()" style="display:none;">Stop</button> |
| </div> |
| </div> |
|
|
| <div class="card" id="trainStatusCard" style="display:none;"> |
| <h3>Training Progress</h3> |
| <div class="stat-row"> |
| <span class="dot" id="tDot"></span> |
| <span id="tStatus">Idle</span> |
| <span id="tEpoch" style="margin-left:auto;color:#555;font-size:0.73rem;"></span> |
| <span id="tLoss" style="color:#a78bfa;font-size:0.73rem;margin-left:8px;"></span> |
| </div> |
| <div class="progress-track"><div class="progress-fill" id="tProg" style="width:0%"></div></div> |
| <div class="chart-wrap"><canvas id="lossChart"></canvas></div> |
| <div class="log-box" id="tLog"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-auto" style="display:none;"> |
| <div class="card"> |
| <h3>Gemini Auto-Train</h3> |
| <p style="font-size:0.78rem;color:#555;margin-bottom:10px;">Gemini generates training data automatically, then ExocoreV1 trains on it.</p> |
| <div class="fr"><label>Initial Prompt</label><textarea id="autoPrompt" rows="3" placeholder="e.g. Explain quantum computing in detail."></textarea></div> |
| <div class="fr"><label>Continue Message</label><input type="text" id="autoContinue" value="ok next train"></div> |
| <div class="fr"><label>Model Name</label><input type="text" id="autoModel" value="Exocore"></div> |
| <div style="display:flex;gap:8px;"> |
| <button class="btn btn-p" id="autoStartBtn" onclick="startAuto()">Start</button> |
| <button class="btn btn-d" id="autoStopBtn" onclick="stopAuto()" style="display:none;">Stop</button> |
| </div> |
| </div> |
| <div class="card" id="autoStatusCard" style="display:none;"> |
| <h3>Auto-Train Status</h3> |
| <div class="stat-row"><span class="dot" id="aDot"></span><span id="aStatus">Idle</span></div> |
| <div class="log-box" id="aLog"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="right"> |
| |
| <div class="card"> |
| <h3>ExocoreV1 Architecture</h3> |
| <div class="arch-tree"> |
| <div class="arch-node"><div class="label" style="text-align:center;color:#f59e0b;border-color:#78350f;background:#1c1208;">Input Tokens</div></div> |
| <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div> |
| <div class="arch-node"><div class="label">Token Embedding <span style="color:#555;">151,936 → 1024</span></div></div> |
| <div style="text-align:center;color:#333;font-size:0.7rem;">↓ ×28 Transformer Blocks</div> |
|
|
| <div class="arch-node block"> |
| <div class="label">Transformer Block (×28)</div> |
| </div> |
| <div class="arch-node sub"><div class="label">Input LayerNorm <span style="color:#555;">[1024]</span></div></div> |
| <div class="arch-node sub"><div class="label">GQA Attention <span style="color:#555;">16Q / 8KV heads · dim 128</span></div></div> |
| <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">Q proj 1024→2048 · K,V 1024→1024 · O 2048→1024</div></div> |
| <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">Q-Norm & K-Norm (RMSNorm per head)</div></div> |
| <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">RoPE · θ=1,000,000 · max 40,960 tokens</div></div> |
| <div class="arch-node sub"><div class="label">Post-Attn LayerNorm <span style="color:#555;">[1024]</span></div></div> |
| <div class="arch-node sub"><div class="label">SwiGLU FFN <span style="color:#555;">1024→3072→1024</span></div></div> |
| <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div> |
|
|
| <div class="arch-node"><div class="label">Final RMSNorm <span style="color:#555;">[1024]</span></div></div> |
| <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div> |
| <div class="arch-node head-node"><div class="label">Language Model Head <span style="color:#555;">1024 → 151,936</span></div></div> |
| <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div> |
| <div class="arch-node"><div class="label" style="text-align:center;color:#f59e0b;border-color:#78350f;background:#1c1208;">Output Tokens</div></div> |
|
|
| <div style="margin-top:12px;padding-top:10px;border-top:1px solid #1e1e2a;"> |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"> |
| <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">751.6M</div><div style="font-size:0.65rem;color:#555;">Parameters</div></div> |
| <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">28</div><div style="font-size:0.65rem;color:#555;">Layers</div></div> |
| <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">bfloat16</div><div style="font-size:0.65rem;color:#555;">Precision</div></div> |
| <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">40,960</div><div style="font-size:0.65rem;color:#555;">Max Context</div></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <h3>Trained Models</h3> |
| <div id="modelList"><p style="color:#444;font-size:0.78rem;">Loading…</p></div> |
| <button class="btn btn-s btn-sm" onclick="loadModels()" style="margin-top:8px;">Refresh</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| let trainInterval = null; |
| let autoInterval = null; |
| let lossChart = null; |
| |
| function showTab(name) { |
| ['train','auto'].forEach(t => { |
| document.getElementById('tab-' + t).style.display = t === name ? '' : 'none'; |
| }); |
| document.querySelectorAll('.tab').forEach((el, i) => { |
| el.classList.toggle('active', ['train','auto'][i] === name); |
| }); |
| } |
| |
| async function loadTrainTxt() { |
| const r = await fetch('/api/train/text'); |
| const d = await r.json(); |
| if (d.found) { |
| document.getElementById('trainText').value = d.text; |
| document.getElementById('charCount').textContent = d.chars.toLocaleString() + ' chars'; |
| } else { alert('train.txt not found.'); } |
| } |
| |
| document.getElementById('trainText').addEventListener('input', function() { |
| document.getElementById('charCount').textContent = this.value.length.toLocaleString() + ' chars'; |
| }); |
| |
| function initChart() { |
| const ctx = document.getElementById('lossChart').getContext('2d'); |
| lossChart = new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: [], |
| datasets: [{ |
| label: 'Training Loss', |
| data: [], |
| borderColor: '#a78bfa', |
| backgroundColor: 'rgba(167,139,250,0.08)', |
| borderWidth: 2, |
| pointRadius: 3, |
| pointBackgroundColor: '#a78bfa', |
| tension: 0.3, |
| fill: true, |
| }] |
| }, |
| options: { |
| responsive: true, maintainAspectRatio: false, |
| plugins: { legend: { labels: { color: '#888', font: { size: 11 } } } }, |
| scales: { |
| x: { grid: { color: '#1a1a24' }, ticks: { color: '#555', font: { size: 10 } }, title: { display: true, text: 'Epoch', color: '#555', font: { size: 10 } } }, |
| y: { grid: { color: '#1a1a24' }, ticks: { color: '#555', font: { size: 10 } }, title: { display: true, text: 'Loss', color: '#555', font: { size: 10 } } }, |
| }, |
| animation: { duration: 200 } |
| } |
| }); |
| } |
| |
| function updateChart(history) { |
| if (!lossChart) initChart(); |
| lossChart.data.labels = history.map(h => 'E' + h.epoch); |
| lossChart.data.datasets[0].data = history.map(h => h.loss); |
| lossChart.update('none'); |
| } |
| |
| async function startTraining() { |
| const text = document.getElementById('trainText').value; |
| if (text.length < 100) { alert('Need at least 100 characters of training text.'); return; } |
| const config = { |
| epochs: parseInt(document.getElementById('epochs').value), |
| n_embd: parseInt(document.getElementById('nEmbd').value), |
| n_layer: parseInt(document.getElementById('nLayer').value), |
| block_size: parseInt(document.getElementById('blockSize').value), |
| batch_size: parseInt(document.getElementById('batchSize').value), |
| lr: parseFloat(document.getElementById('lr').value), |
| }; |
| const model_name = document.getElementById('modelName').value || 'Exocore'; |
| const r = await fetch('/api/train/start', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({ text, config, model_name }) |
| }); |
| const d = await r.json(); |
| if (d.error) { alert(d.error); return; } |
| document.getElementById('trainStatusCard').style.display = ''; |
| document.getElementById('trainBtn').disabled = true; |
| document.getElementById('stopBtn').style.display = ''; |
| if (!lossChart) initChart(); |
| lossChart.data.labels = []; |
| lossChart.data.datasets[0].data = []; |
| lossChart.update('none'); |
| trainInterval = setInterval(pollTrain, 1500); |
| } |
| |
| async function stopTraining() { |
| await fetch('/api/train/stop', { method: 'POST' }); |
| } |
| |
| async function pollTrain() { |
| const r = await fetch('/api/train/status'); |
| const d = await r.json(); |
| const dot = document.getElementById('tDot'); |
| document.getElementById('tStatus').textContent = d.status || (d.running ? 'Training…' : (d.error ? 'Error' : 'Done')); |
| dot.className = 'dot ' + (d.running ? 'running' : (d.error ? 'error' : 'done')); |
| const pct = d.total_epochs > 0 ? (d.epoch / d.total_epochs * 100) : 0; |
| document.getElementById('tProg').style.width = pct + '%'; |
| document.getElementById('tEpoch').textContent = d.total_epochs ? `Epoch ${d.epoch}/${d.total_epochs}` : ''; |
| document.getElementById('tLoss').textContent = d.loss ? `Loss: ${d.loss}` : ''; |
| if (d.loss_history && d.loss_history.length) updateChart(d.loss_history); |
| if (d.log && d.log.length) { |
| const lb = document.getElementById('tLog'); |
| lb.textContent = d.log.slice(-30).join('\n'); |
| lb.scrollTop = lb.scrollHeight; |
| } |
| if (!d.running) { |
| clearInterval(trainInterval); |
| document.getElementById('trainBtn').disabled = false; |
| document.getElementById('stopBtn').style.display = 'none'; |
| if (d.loss_history) updateChart(d.loss_history); |
| loadModels(); |
| } |
| } |
| |
| async function startAuto() { |
| const prompt = document.getElementById('autoPrompt').value.trim(); |
| if (!prompt) { alert('Enter an initial prompt.'); return; } |
| const r = await fetch('/api/gemini/autotrain/start', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({ |
| initial_prompt: prompt, |
| continue_msg: document.getElementById('autoContinue').value, |
| model_name: document.getElementById('autoModel').value, |
| config: {} |
| }) |
| }); |
| const d = await r.json(); |
| if (d.error) { alert(d.error); return; } |
| document.getElementById('autoStatusCard').style.display = ''; |
| document.getElementById('autoStartBtn').disabled = true; |
| document.getElementById('autoStopBtn').style.display = ''; |
| autoInterval = setInterval(pollAuto, 2000); |
| } |
| async function stopAuto() { |
| await fetch('/api/gemini/autotrain/stop', { method: 'POST' }); |
| } |
| async function pollAuto() { |
| const r = await fetch('/api/gemini/autotrain/status'); |
| const d = await r.json(); |
| document.getElementById('aDot').className = 'dot ' + (d.running ? 'running' : (d.error ? 'error' : 'done')); |
| document.getElementById('aStatus').textContent = d.status || (d.running ? 'Running…' : 'Done'); |
| if (d.log && d.log.length) { |
| const lb = document.getElementById('aLog'); |
| lb.textContent = d.log.slice(-20).join('\n'); |
| lb.scrollTop = lb.scrollHeight; |
| } |
| if (!d.running) { |
| clearInterval(autoInterval); |
| document.getElementById('autoStartBtn').disabled = false; |
| document.getElementById('autoStopBtn').style.display = 'none'; |
| loadModels(); |
| } |
| } |
| |
| const PROTECTED = ['exocore.pt']; |
| |
| async function loadModels() { |
| const r = await fetch('/api/train/models'); |
| const d = await r.json(); |
| const list = document.getElementById('modelList'); |
| if (!d.models || !d.models.length) { |
| list.innerHTML = '<p style="color:#444;font-size:0.78rem;">No trained models yet.</p>'; |
| return; |
| } |
| list.innerHTML = ''; |
| d.models.forEach(m => { |
| const name = typeof m === 'string' ? m : (m.name || m); |
| const isProt = PROTECTED.includes(name.toLowerCase()); |
| const div = document.createElement('div'); |
| div.className = 'mcard'; |
| div.innerHTML = ` |
| <div> |
| <div class="mn">${name}${isProt ? '<span class="pb">Protected</span>' : ''}</div> |
| <div class="mt">ExocoreV1 model</div> |
| </div> |
| <div style="display:flex;gap:5px;"> |
| <button class="btn btn-s btn-sm" onclick="viewInfo('${name}')">Info</button> |
| ${isProt ? '' : `<button class="btn btn-d btn-sm" onclick="deleteModel('${name}')">Delete</button>`} |
| </div>`; |
| list.appendChild(div); |
| }); |
| } |
| |
| async function viewInfo(name) { |
| const r = await fetch('/api/train/model-info/' + encodeURIComponent(name)); |
| const d = await r.json(); |
| if (d.error) { alert('Error: ' + d.error); return; } |
| const lines = Object.entries(d).map(([k,v]) => `${k}: ${JSON.stringify(v)}`).join('\n'); |
| alert(lines); |
| } |
| |
| async function deleteModel(name) { |
| if (!confirm(`Delete "${name}"?`)) return; |
| const r = await fetch('/api/model/delete', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({ local_name: name }) |
| }); |
| const d = await r.json(); |
| if (d.error) { alert('Error: ' + d.error); } |
| else { loadModels(); } |
| } |
| |
| loadModels(); |
| </script> |
| </body> |
| </html> |
|
|