hyvä siitä tulee

This commit is contained in:
2026-04-02 18:16:41 +03:00
parent 6cdd695a3b
commit 31995fb278
8 changed files with 272 additions and 89 deletions

View File

@@ -119,6 +119,8 @@
.main-panel { display: none; }
.main-panel.active { display: block; }
@keyframes spin { to { transform: rotate(360deg); } }
.code-output {
font-family: 'Courier New', Courier, monospace;
background: #010409;
@@ -500,6 +502,15 @@
</div>
</div>
<!-- Automaattiset tehtävät -->
<div style="margin-bottom:14px;display:flex;align-items:center;gap:10px">
<label style="font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer">
<input type="checkbox" id="auto-tasks-toggle" checked style="accent-color:var(--accent-color)">
Vastaanota automaattisia tehtäviä hubilta
</label>
<span style="font-size:11px;color:#8b949e">(10s välein)</span>
</div>
<!-- Reaaliaikaiset metriikat -->
<div id="metrics-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px">
<div class="metric-card">
@@ -655,7 +666,7 @@
</div>
<script type="module">
import init, { start_agent_node, set_gpu_load } from './pkg/node.js';
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
// Päävälilehtien vaihto
window.switchMainTab = function(tab) {
@@ -664,6 +675,21 @@
document.getElementById('panel-' + tab).classList.add('active');
document.querySelector(`.main-tab[onclick*="${tab}"]`).classList.add('active');
window.location.hash = tab;
// Päivitetään admin-sessio vastaamaan nykyistä välilehteä
if (window._uiSocket && window._uiSocket.readyState === 1) {
const viewTask = tab === 'codelab' ? 'codelab-viewer' : 'viewer';
window._uiSocket.send(JSON.stringify({
type: 'auth',
status: 'viewer',
node_type: 'browser',
platform: navigator.platform || '',
cpu_cores: navigator.hardwareConcurrency || 0,
device_memory_gb: navigator.deviceMemory || 0,
allocated_gb: 0,
selected_task: viewTask,
}));
}
};
// URL-hash navigointi: #codelab tai #network
@@ -671,9 +697,24 @@
switchMainTab('codelab');
}
// Synkronoi coder-status kun WS on jo auki (suora #codelab navigointi)
setTimeout(() => {
if (uiSocket && uiSocket.readyState === 1) {
const coderEl = document.getElementById('coder-status');
if (coderEl && coderEl.textContent === 'Ei yhdistetty') {
coderEl.textContent = 'Connected';
coderEl.style.color = '#d29922';
}
}
}, 1000);
// Koodilaboratorion tila
const codeMetrics = { tasks: 0, tokens: 0, lastSpeed: 0 };
let coderJoined = false;
// Globaali WebGPU-tila — tunnistetaan kerran viewer-authissa, käytetään kaikkialla
let detectedWebGPU = false;
let detectedGpuInfo = null;
let wasmInitialized = false;
let coderSize = '05b'; // '05b' tai '3b'
@@ -791,6 +832,13 @@
}
});
// Automaattisten tehtävien toggle
document.getElementById('auto-tasks-toggle')?.addEventListener('change', (e) => {
if (window.wasm_active) {
set_auto_tasks(e.target.checked);
}
});
// Käyttäjän oma tekstisyöte
const userInput = document.getElementById('user-text');
const sendBtn = document.getElementById('send-btn');
@@ -815,13 +863,41 @@
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
uiSocket.onopen = () => {
window._uiSocket = uiSocket;
uiSocket.onopen = async () => {
// Päivitetään molemmat statukset
const el = document.getElementById('node-status');
el.textContent = 'Connected';
el.style.color = '#d29922';
const coderEl = document.getElementById('coder-status');
if (coderEl && !coderJoined) {
coderEl.textContent = 'Connected';
coderEl.style.color = '#d29922';
}
// Tunnistetaan WebGPU kunnolla (adapter + info) — tallennetaan globaalisti
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
detectedWebGPU = true;
const info = adapter.info || {};
const maxBuf = Number(adapter.limits.maxBufferSize || 0);
detectedGpuInfo = {
vendor: info.vendor || '',
description: info.description || '',
architecture: info.architecture || '',
device: info.device || '',
estimated_vram_gb: maxBuf > 0 ? Math.round(maxBuf / 1024 / 1024 / 1024 * 4) : 0,
max_buffer_size: maxBuf,
max_compute_workgroups: adapter.limits.maxComputeWorkgroupsPerDimension || 0,
};
}
} catch(e) {}
}
const hasGPU = detectedWebGPU;
const gpuInfo = detectedGpuInfo;
// Lähetetään kevyt auth heti — admin näkee kävijän välittömästi
const hasGPU = !!navigator.gpu;
uiSocket.send(JSON.stringify({
type: 'auth',
status: 'viewer',
@@ -832,12 +908,18 @@
allocated_gb: 0,
selected_task: 'viewer',
has_webgpu: hasGPU,
gpu: gpuInfo,
}));
};
uiSocket.onclose = () => {
const el = document.getElementById('node-status');
el.textContent = 'Disconnected';
el.style.color = '#f85149';
const coderEl = document.getElementById('coder-status');
if (coderEl) {
coderEl.textContent = 'Disconnected';
coderEl.style.color = '#f85149';
}
};
uiSocket.onmessage = (event) => {
try {
@@ -871,6 +953,43 @@
} else {
dlBar.style.display = 'none';
}
} else if (data.type === "single_tokenize_done") {
chatBox.classList.remove('hidden');
const r = data.result || {};
const ms = data.duration_ms || 0;
const nodeId = data.node_id || '?';
const cpt = parseFloat((r.chars_per_token || 0).toFixed(2));
const cptColor = cpt >= 4 ? "#3fb950" : cpt >= 3 ? "#d29922" : "#f85149";
const renderTokens = (tokens) => (tokens || []).map(t =>
`<span class="tok tok-en">${t.replace(/</g,'&lt;')}</span>`
).join('');
const tokHtml = renderTokens(r.tokens);
const detailId = 'stok-' + Date.now();
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg';
msgDiv.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="color:var(--accent-color);font-weight:600;font-size:15px">Solmu #${nodeId}</span>
<div style="display:flex;gap:8px;align-items:center">
<button class="toggle-tokens" onclick="document.getElementById('${detailId}').classList.toggle('visible')">Tokenit</button>
<span style="color:#8b949e;font-size:13px">${typeof ms === 'number' ? ms.toFixed(2) : ms}ms</span>
</div>
</div>
<div style="font-size:14px;color:#79b8ff;margin-bottom:6px">"${r.text || ''}"</div>
<div style="font-size:14px;display:flex;gap:16px">
<span style="color:#8b949e">${r.char_count || 0} merkkiä</span>
<span style="color:#8b949e">${r.word_count || 0} sanaa</span>
<span style="color:var(--accent-color);font-weight:600">${r.token_count || 0} tokenia</span>
<span style="color:${cptColor};font-weight:600">${cpt} merkkiä/token</span>
</div>
<div id="${detailId}" class="token-detail">
<strong style="color:#58a6ff;font-size:12px">(${r.token_count || 0})</strong> ${tokHtml}
</div>`;
chatBox.appendChild(msgDiv);
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
flashComputing();
} else if (data.type === "pair_task") {
chatBox.classList.remove('hidden');
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
@@ -960,7 +1079,8 @@
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === "llm_done") {
// SmolLM / LLM-inferenssin tulos
// Poistetaan streaming-kortti
chatBox.querySelector('.streaming-card')?.remove();
chatBox.classList.remove('hidden');
const nodeId = data.node_id || "?";
const model = data.model || "LLM";
@@ -998,53 +1118,56 @@
console.log(`[${model}] ${tokGen} tokenia | ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s | "${(data.response || '').substring(0, 60)}..."`);
} else if (data.type === "llm_chunk") {
// Streaming-token LLM:ltä — näytetään lokissa
// (chat-ikkunan streaming tulisi toteuttaa samalla logiikalla kuin pair_task/pair_done)
// Streaming: näytetään generointi reaaliaikaisesti
const model = data.model || '';
const isCoder = model.includes('Coder');
const targetBox = isCoder ? codeResults : chatBox;
if (targetBox) {
let streamEl = targetBox.querySelector('.streaming-card');
if (!streamEl) {
streamEl = document.createElement('div');
streamEl.className = isCoder ? 'code-task-card streaming-card' : 'chat-msg streaming-card';
streamEl.style.borderLeftColor = '#a371f7';
streamEl.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span style="color:#a371f7;font-weight:600">${model}</span>
<span class="stream-counter" style="color:var(--accent-color);font-size:12px">0 tok</span>
</div>
<div style="font-size:13px;color:#8b949e;margin-bottom:4px">Prompt: "${data.prompt || ''}"</div>
<div class="stream-text" style="font-size:14px;color:var(--text-color);line-height:1.5;${isCoder ? 'font-family:Courier New,monospace;background:#010409;padding:8px;border-radius:4px;white-space:pre-wrap;font-size:12px;color:#3fb950' : ''}"></div>
<div style="margin-top:6px;font-size:11px;color:#d29922">
<span class="spinner" style="display:inline-block;animation:spin 1s linear infinite">&#9696;</span> Generating...
</div>`;
if (isCoder) {
targetBox.insertBefore(streamEl, targetBox.firstChild);
} else {
targetBox.appendChild(streamEl);
}
}
const textEl = streamEl.querySelector('.stream-text');
const counterEl = streamEl.querySelector('.stream-counter');
if (textEl) textEl.textContent += data.token || '';
const tokCount = (textEl.textContent || '').split('').length;
if (counterEl) counterEl.textContent = tokCount + ' tok';
targetBox.scrollTop = targetBox.scrollHeight;
}
}
} catch(e) {}
};
btn.addEventListener('click', async () => {
// Kerätään laitteistotiedot
let hasWebGPU = false;
// Käytetään viewer-authissa jo tunnistettua WebGPU-tilaa
let hasWebGPU = detectedWebGPU;
const deviceInfo = {
allocated_gb: 4,
cpu_cores: navigator.hardwareConcurrency || 0,
device_memory_gb: navigator.deviceMemory || 0,
platform: navigator.platform || "",
gpu: null,
gpu: detectedGpuInfo,
selected_task: selectedTask
};
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
try {
const testDevice = await adapter.requestDevice({ requiredLimits: { maxInterStageShaderComponents: 60 } });
hasWebGPU = true;
testDevice.destroy();
} catch(e) {
console.log("[WebGPU] Legacy-tuki rajoittaa WebGPU:n: " + e.message);
hasWebGPU = false; // Fallback to CPU NdArray avoiding WASM panic
}
const info = adapter.info || {};
const maxBuf = Number(adapter.limits.maxBufferSize || 0);
// maxBufferSize antaa arvion VRAM:sta — tyypillisesti ~25% todellisesta
const estimatedVramGb = maxBuf > 0 ? Math.round(maxBuf / 1024 / 1024 / 1024 * 4) : 0;
deviceInfo.gpu = {
vendor: info.vendor || "",
architecture: info.architecture || "",
device: info.device || "",
description: info.description || "",
max_buffer_size: maxBuf,
max_compute_workgroups: adapter.limits.maxComputeWorkgroupsPerDimension || 0,
estimated_vram_gb: estimatedVramGb
};
}
} catch (e) {}
}
const gpuStr = hasWebGPU ? (deviceInfo.gpu?.description || deviceInfo.gpu?.vendor || "WebGPU") : "ei GPU:ta";
// Laskenta käyttää aina CPU:ta (Candle), WebGPU on vain tensorilaskennassa (Burn)
const computeBackend = (selectedTask === 'tokenize')
@@ -1184,6 +1307,9 @@
}
function addCodeResult(data) {
// Poistetaan streaming-kortti
codeResults.querySelector('.streaming-card')?.remove();
const model = data.model || 'Coder';
const tokGen = data.tokens_generated || 0;
const durMs = data.duration_ms || 0;