koodilabran v0.1

This commit is contained in:
2026-04-02 10:07:48 +03:00
parent 11bd802be5
commit 8e20b06344
6 changed files with 881 additions and 62 deletions

View File

@@ -97,6 +97,69 @@
h1 span { color: var(--accent-color); }
.sub { color: #8b949e; margin-bottom: 25px; }
.main-tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 0;
}
.main-tab {
padding: 10px 20px;
font-size: 15px;
font-weight: 500;
color: #8b949e;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
}
.main-tab:hover { color: var(--text-color); }
.main-tab.active { color: var(--accent-color); border-bottom-color: var(--accent-color); }
.main-panel { display: none; }
.main-panel.active { display: block; }
.code-output {
font-family: 'Courier New', Courier, monospace;
background: #010409;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 14px;
font-size: 13px;
line-height: 1.6;
color: var(--success-color);
white-space: pre-wrap;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.code-output .keyword { color: #ff7b72; }
.code-output .string { color: #a5d6ff; }
.code-output .comment { color: #8b949e; }
.code-task-card {
background: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 14px;
margin-bottom: 12px;
}
.code-task-card .prompt { color: #d29922; font-size: 14px; margin-bottom: 10px; }
.code-task-card .meta { color: #8b949e; font-size: 12px; margin-top: 10px; }
.code-step {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #8b949e;
padding: 6px 0;
}
.code-step.active { color: var(--accent-color); }
.code-step.done { color: var(--success-color); }
.code-step.error { color: #f85149; }
.step-icon { font-size: 16px; width: 20px; text-align: center; }
.status-box {
font-family: 'Courier New', Courier, monospace;
background-color: #010409;
@@ -309,7 +372,16 @@
<div class="container">
<h1>Kipinä <span>Agent Dashboard</span></h1>
<p class="sub">Hajautettu WebGPU Laskentaverkko · <span id="hub-version" style="color:#58a6ff">-</span></p>
<!-- Päävälilehdet -->
<div class="main-tabs">
<div class="main-tab active" onclick="switchMainTab('network')">Laskentaverkko</div>
<div class="main-tab" onclick="switchMainTab('codelab')">Koodilaboratorio</div>
</div>
<!-- PANEELI 1: Laskentaverkko -->
<div id="panel-network" class="main-panel active">
<!-- Global Cluster Statistics (UI) -->
<div class="dashboard-panel">
<div class="stat-box" style="border-right: 1px solid #30363d;">
@@ -413,7 +485,7 @@
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-weight:600;font-size:15px">Resurssien hallinta</span>
<span id="node-status" style="font-size:12px;color:var(--success-color)">Aktiivinen</span>
<span id="node-status" style="font-size:12px;color:#8b949e">Ei yhdistetty</span>
</div>
<!-- Kuormitussäädin -->
@@ -449,6 +521,14 @@
</div>
</div>
<div id="user-input-box" class="hidden" style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:12px;margin-bottom:12px">
<div style="font-size:13px;color:#8b949e;margin-bottom:8px">Kokeile omaa tekstiä:</div>
<div style="display:flex;gap:8px">
<input type="text" id="user-text" placeholder="Kirjoita teksti tokenisoitavaksi tai promptiksi..." style="flex:1;background:var(--panel-bg);border:1px solid var(--border-color);border-radius:4px;padding:8px 12px;color:var(--text-color);font-size:14px;outline:none">
<button id="send-btn" style="background:#238636;color:#fff;border:1px solid rgba(240,246,252,0.1);border-radius:4px;padding:8px 16px;font-size:14px;cursor:pointer;white-space:nowrap">Tokenisoi</button>
</div>
</div>
<div id="chat-box" class="chat-box hidden">
<div style="color: #8b949e; text-align: center; margin-top: 80px;">Odotetaan Generointitehtäviä Hubilta...</div>
</div>
@@ -457,11 +537,127 @@
<p>> Odotetaan uusia tehtäviä Hubulta...</p>
</div>
</div>
</div><!-- /panel-network -->
<!-- PANEELI 2: Koodilaboratorio -->
<div id="panel-codelab" class="main-panel">
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600;font-size:15px">Qwen2.5-Coder-0.5B-Instruct</span>
<span id="coder-status" style="font-size:12px;color:#8b949e">Ei yhdistetty</span>
</div>
<p style="font-size:12px;color:#8b949e;line-height:1.5;margin-bottom:12px">
Code-specialized language model trained on 5.5T tokens of source code.
Generates Python code in your browser via WebAssembly. Choose model size and write your own prompt.
</p>
<!-- Model size selector -->
<div style="display:flex;gap:8px;margin-bottom:10px">
<label style="flex:1;display:flex;align-items:center;gap:6px;background:var(--panel-bg);border:2px solid var(--accent-color);border-radius:4px;padding:8px 12px;cursor:pointer;font-size:13px" id="coder-opt-05b">
<input type="radio" name="coder-size" value="05b" checked style="accent-color:var(--accent-color)">
<div>
<strong style="color:var(--text-color)">0.5B</strong>
<span style="color:#8b949e"> — 990 MB, ~0.4 tok/s</span>
</div>
</label>
<label style="flex:1;display:flex;align-items:center;gap:6px;background:var(--panel-bg);border:2px solid var(--border-color);border-radius:4px;padding:8px 12px;cursor:pointer;font-size:13px" id="coder-opt-3b">
<input type="radio" name="coder-size" value="3b" style="accent-color:var(--accent-color)">
<div>
<strong style="color:var(--text-color)">3B</strong>
<span style="color:#8b949e"> — 6.2 GB, better quality, slower</span>
</div>
</label>
</div>
<div style="display:flex;gap:8px">
<input type="text" id="code-input" placeholder="e.g. Write a Python function that checks if a number is prime" style="flex:1;background:var(--panel-bg);border:1px solid var(--border-color);border-radius:4px;padding:8px 12px;color:var(--text-color);font-size:14px;outline:none">
<button id="code-send-btn" style="background:#238636;color:#fff;border:1px solid rgba(240,246,252,0.1);border-radius:4px;padding:8px 16px;font-size:14px;cursor:pointer">Generate</button>
</div>
<div id="code-loading" style="display:none;margin-top:8px;font-size:12px;color:#d29922">Starting Coder model...</div>
</div>
<!-- Koodilaboratorion metriikat -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px">
<div class="metric-card">
<div class="metric-val" id="code-m-tasks">0</div>
<div class="metric-label">Tehtäviä</div>
</div>
<div class="metric-card">
<div class="metric-val" id="code-m-tokens">0</div>
<div class="metric-label">Tokeneita</div>
</div>
<div class="metric-card">
<div class="metric-val" id="code-m-speed">-</div>
<div class="metric-label">tok/s</div>
</div>
</div>
<!-- Latausvaiheet -->
<div id="code-pipeline" style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px;display:none">
<div style="font-size:13px;font-weight:600;margin-bottom:12px">Valmistautuminen</div>
<div id="code-steps" style="display:flex;flex-direction:column;gap:8px">
<div class="code-step" id="step-wasm">
<span class="step-icon">&#9711;</span>
<span>WebAssembly-ytimen lataus</span>
</div>
<div class="code-step" id="step-tokenizer">
<span class="step-icon">&#9711;</span>
<span>Tokenizer (7 MB)</span>
</div>
<div class="code-step" id="step-model">
<span class="step-icon">&#9711;</span>
<span>Qwen2.5-Coder-0.5B painot (990 MB)</span>
<span id="step-model-pct" style="color:var(--accent-color);margin-left:auto;font-size:12px"></span>
</div>
<div class="code-step" id="step-build">
<span class="step-icon">&#9711;</span>
<span>Mallin rakentaminen muistiin</span>
</div>
<div class="code-step" id="step-ready">
<span class="step-icon">&#9711;</span>
<span>Valmis generoimaan</span>
</div>
</div>
</div>
<!-- Kooditulokset -->
<div id="code-results" style="display:flex;flex-direction:column;gap:12px">
<div data-placeholder style="color:#8b949e;text-align:center;padding:40px">Kirjoita ohjelmointitehtävä ja paina Koodaa</div>
</div>
</div><!-- /panel-codelab -->
</div>
<script type="module">
import init, { start_agent_node, set_gpu_load } from './pkg/node.js';
// Päävälilehtien vaihto
window.switchMainTab = function(tab) {
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active'));
document.getElementById('panel-' + tab).classList.add('active');
event.target.classList.add('active');
};
// Koodilaboratorion tila
const codeMetrics = { tasks: 0, tokens: 0, lastSpeed: 0 };
let coderJoined = false;
let coderSize = '05b'; // '05b' tai '3b'
// Mallivalinnan radio-napit
document.querySelectorAll('input[name="coder-size"]').forEach(radio => {
radio.addEventListener('change', (e) => {
coderSize = e.target.value;
// Visuaalinen korostus
document.getElementById('coder-opt-05b').style.borderColor = coderSize === '05b' ? 'var(--accent-color)' : 'var(--border-color)';
document.getElementById('coder-opt-3b').style.borderColor = coderSize === '3b' ? 'var(--accent-color)' : 'var(--border-color)';
// Jos jo liittynyt, pitää liittyä uudelleen toisella mallilla
if (coderJoined) {
coderJoined = false;
document.getElementById('coder-status').textContent = 'Model changed — rejoin on next generate';
document.getElementById('coder-status').style.color = '#d29922';
}
});
});
const btn = document.getElementById('start-btn');
const logBox = document.getElementById('log-box');
const loadSlider = document.getElementById('gpu-load');
@@ -506,6 +702,20 @@
}
setInterval(updateMetrics, 1000);
// Laskentaverkko: status Connected (keltainen) ↔ Computing (vihreä)
let computingTimer = null;
function flashComputing() {
const el = document.getElementById('node-status');
if (!el || !window.wasm_active) return;
el.textContent = 'Computing';
el.style.color = 'var(--success-color)';
clearTimeout(computingTimer);
computingTimer = setTimeout(() => {
el.textContent = 'Connected';
el.style.color = '#d29922';
}, 3000);
}
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
const originalLog = console.log;
console.log = function(...args) {
@@ -546,6 +756,28 @@
}
});
// Käyttäjän oma tekstisyöte
const userInput = document.getElementById('user-text');
const sendBtn = document.getElementById('send-btn');
function sendUserText() {
const text = userInput.value.trim();
if (!text || !uiSocket || uiSocket.readyState !== 1) return;
const msg = JSON.stringify({
type: 'user_text',
text: text,
task_type: selectedTask,
});
uiSocket.send(msg);
userInput.value = '';
console.log(`Lähetetty: "${text}" (${selectedTask})`);
}
sendBtn?.addEventListener('click', sendUserText);
userInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendUserText();
});
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
uiSocket.onmessage = (event) => {
@@ -608,6 +840,7 @@
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
metrics.totalTimeMs += ms;
updateMetrics();
flashComputing();
// Lokiboksiin yhteenveto
console.log(`EN: ${en.token_count} tokenia (${(en.chars_per_token||0).toFixed(2)} m/t) vs FI: ${fi.token_count} tokenia (${(fi.chars_per_token||0).toFixed(2)} m/t) | ylikustannus: ${overhead}% | ${typeof ms === 'number' ? ms.toFixed(2) : ms}ms`);
@@ -688,8 +921,8 @@
<div style="font-size:13px;color:#8b949e;margin-bottom:6px">
Prompt: <span style="color:#d29922">"${data.prompt || ''}"</span>
</div>
<div style="font-size:14px;color:var(--text-color);line-height:1.5">
${data.response || '<em>tyhjä vastaus</em>'}
<div style="font-size:14px;color:var(--text-color);line-height:1.5;${(model.includes('Coder') || (data.response||'').includes('def ')) ? 'font-family:Courier New,monospace;background:#010409;padding:10px;border-radius:4px;white-space:pre-wrap;font-size:12px' : ''}">
${(data.response || '<em>tyhjä vastaus</em>').replace(/</g, '&lt;').replace(/>/g, '&gt;')}
</div>
<div style="margin-top:8px;font-size:12px;color:#8b949e">
${tokGen} tokenia generoitu | malli ladattu: ${typeof loadMs === 'number' ? loadMs.toFixed(0) : loadMs}ms
@@ -701,6 +934,7 @@
metrics.tasks++;
metrics.totalTokens += tokGen;
metrics.totalTimeMs += durMs;
flashComputing();
updateMetrics();
console.log(`[${model}] ${tokGen} tokenia | ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s | "${(data.response || '').substring(0, 60)}..."`);
@@ -812,26 +1046,210 @@
document.getElementById('initial-state').classList.add('hidden');
document.getElementById('active-state').classList.remove('hidden');
document.getElementById('user-input-box').classList.remove('hidden');
btn.style.display = 'none';
// Nappin teksti ja placeholder tehtävän mukaan
const sendBtnEl = document.getElementById('send-btn');
if (selectedTask === 'tokenize') {
sendBtnEl.textContent = 'Tokenisoi';
document.getElementById('user-text').placeholder = 'Kirjoita teksti tokenisoitavaksi...';
} else if (selectedTask === 'qwen-coder') {
sendBtnEl.textContent = 'Koodaa';
document.getElementById('user-text').placeholder = 'Kuvaile Python-ohjelmointitehtävä...';
} else {
sendBtnEl.textContent = 'Generoi';
document.getElementById('user-text').placeholder = 'Kirjoita prompti kielimallille...';
}
try {
console.log("Ladataan Burn Wasm -binääriä...");
await init();
window.wasm_active = true;
metrics.startTime = Date.now();
// Asetetaan Connected-tila (keltainen) — vihreäksi vasta kun laskentaa tapahtuu
const nodeStatusEl = document.getElementById('node-status');
nodeStatusEl.textContent = 'Connected';
nodeStatusEl.style.color = '#d29922';
// Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen
set_gpu_load(parseInt(loadSlider.value));
// WebAssembly yhdistää oikeaksi Agent Nodeksi
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
const taskIds = {'tokenize': 0, 'smollm-135m': 1, 'qwen-05b': 2, 'phi3-mini': 3};
const taskIds = {'tokenize': 0, 'smollm-135m': 1, 'qwen-05b': 2, 'phi3-mini': 3, 'qwen-coder-05b': 4, 'qwen-coder-3b': 5};
const taskId = taskIds[selectedTask] || 0;
await start_agent_node(wsUrl, hasWebGPU, JSON.stringify(deviceInfo), taskId);
} catch(e) {
console.log("Virhe GPU-käynnistyksessä: " + e);
}
});
// === Koodilaboratorio ===
const codeInput = document.getElementById('code-input');
const codeSendBtn = document.getElementById('code-send-btn');
const codeResults = document.getElementById('code-results');
const codeLoading = document.getElementById('code-loading');
let coderWsReady = false;
let coderWs = null; // Erillinen WS coder-nodelle
let pendingCodePrompt = null;
function addCodeResult(data) {
const model = data.model || 'Coder';
const tokGen = data.tokens_generated || 0;
const durMs = data.duration_ms || 0;
const tokS = data.tokens_per_sec || 0;
const response = (data.response || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
codeMetrics.tasks++;
codeMetrics.tokens += tokGen;
codeMetrics.lastSpeed = tokS;
document.getElementById('code-m-tasks').textContent = codeMetrics.tasks;
document.getElementById('code-m-tokens').textContent = codeMetrics.tokens.toLocaleString('fi-FI');
document.getElementById('code-m-speed').textContent = tokS + ' tok/s';
if (codeResults.querySelector('[data-placeholder]')) {
codeResults.innerHTML = '';
}
codeLoading.style.display = 'none';
codeSendBtn.disabled = false;
codeSendBtn.textContent = 'Generate';
const card = document.createElement('div');
card.className = 'code-task-card';
card.innerHTML = `
<div class="prompt">${data.prompt || ''}</div>
<div class="code-output">${response}</div>
<div class="meta">
${model} · ${tokGen} tokenia · ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms · ${tokS} tok/s
</div>`;
codeResults.insertBefore(card, codeResults.firstChild);
if (codeResults.children.length > 10) codeResults.removeChild(codeResults.lastChild);
}
// Kuuntele coder-tuloksia UI WebSocketista
uiSocket.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'llm_done' && (data.model || '').includes('Coder')) {
addCodeResult(data);
}
} catch(e) {}
});
// Pipeline-vaiheiden päivitys
function setStep(id, state, extra) {
const el = document.getElementById(id);
if (!el) return;
el.className = 'code-step ' + state;
const icon = el.querySelector('.step-icon');
if (state === 'active') icon.textContent = '\u25F7'; // spinning
else if (state === 'done') icon.textContent = '\u2713';
else if (state === 'error') icon.textContent = '\u2717';
if (extra) {
const pct = document.getElementById(id + '-pct');
if (pct) pct.textContent = extra;
}
}
// Kuuntele console.log-viestejä pipeline-vaiheiden seuraamiseksi
const origCodeLog = console.log;
const codeLogListener = (...args) => {
const msg = args.join(' ');
if (msg.includes('[Coder]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) {
if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active');
if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); }
if (msg.includes('[Coder]') && msg.includes('tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('[Coder]') && msg.includes('Ladataan') && msg.includes('tokenizer')) { setStep('step-tokenizer', 'active'); }
if (msg.includes('[Coder]') && msg.includes('tokenizer') && msg.includes('tallennettu')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('lataus:')) {
setStep('step-model', 'active');
const match = msg.match(/lataus: (\d+)%/);
if (match) setStep('step-model', 'active', match[1] + '%');
}
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('löytyi')) { setStep('step-model', 'done', 'cache'); }
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('tallennettu')) { setStep('step-model', 'done', '100%'); }
if (msg.includes('[Coder]') && msg.includes('Rakennetaan')) { setStep('step-build', 'active'); }
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) { setStep('step-build', 'done'); setStep('step-ready', 'done'); }
if (msg.includes('[Coder]') && msg.includes('Syöte:')) {
// Pipeline piiloon kun generointi alkaa
setTimeout(() => { document.getElementById('code-pipeline').style.display = 'none'; }, 1000);
}
}
};
// Lisätään kuuntelija alkuperäisen console.log ylikirjoituksen päälle
const _prevConsoleLog = console.log;
console.log = function(...args) { _prevConsoleLog.apply(console, args); codeLogListener(...args); };
// Käynnistä Coder-node automaattisesti ensimmäisellä kerralla
async function ensureCoderNode() {
if (coderJoined) return;
coderJoined = true;
document.getElementById('coder-status').textContent = 'Käynnistyy...';
document.getElementById('coder-status').style.color = '#d29922';
document.getElementById('code-pipeline').style.display = 'block';
setStep('step-wasm', 'active');
try {
await init();
setStep('step-wasm', 'done');
setStep('step-tokenizer', 'active');
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
const deviceInfo = {
allocated_gb: 4,
cpu_cores: navigator.hardwareConcurrency || 0,
device_memory_gb: navigator.deviceMemory || 0,
platform: navigator.platform || "",
gpu: null,
selected_task: coderSize === '3b' ? 'qwen-coder-3b' : 'qwen-coder-05b'
};
const taskId = coderSize === '3b' ? 5 : 4;
await start_agent_node(wsUrl, false, JSON.stringify(deviceInfo), taskId);
document.getElementById('coder-status').textContent = 'Connected';
document.getElementById('coder-status').style.color = '#d29922';
coderWsReady = true;
if (pendingCodePrompt) {
sendCodeToHub(pendingCodePrompt);
pendingCodePrompt = null;
}
} catch(e) {
console.log("Coder-virhe: " + e);
document.getElementById('coder-status').textContent = 'Virhe';
document.getElementById('coder-status').style.color = '#f85149';
coderJoined = false;
}
}
function sendCodeToHub(text) {
if (uiSocket && uiSocket.readyState === 1) {
uiSocket.send(JSON.stringify({ type: 'user_text', text: text, task_type: 'qwen-coder' }));
}
}
async function handleCodeSubmit() {
const text = codeInput.value.trim();
if (!text) return;
codeInput.value = '';
codeSendBtn.disabled = true;
codeSendBtn.textContent = 'Generating...';
codeLoading.style.display = 'block';
if (!coderJoined) {
pendingCodePrompt = text;
const dlSize = coderSize === '3b' ? '~6.2 GB' : '~990 MB';
codeLoading.textContent = `Loading Qwen2.5-Coder-${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
await ensureCoderNode();
} else {
codeLoading.textContent = 'Generating code...';
sendCodeToHub(text);
}
}
codeSendBtn?.addEventListener('click', handleCodeSubmit);
codeInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleCodeSubmit(); });
</script>
</body>
</html>