hyvä siitä tulee
This commit is contained in:
@@ -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,'<')}</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">◠</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;
|
||||
|
||||
Reference in New Issue
Block a user