Files
agentic-studio/network-poc/static/index.html
2026-04-01 17:54:08 +03:00

457 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kipinä Agent Dashboard</title>
<style>
:root {
--bg-color: #0d1117;
--panel-bg: #161b22;
--text-color: #c9d1d9;
--accent-color: #58a6ff;
--success-color: #3fb950;
--border-color: #30363d;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
flex-direction: column;
box-sizing: border-box;
}
.container {
background-color: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
width: 100%;
max-width: 1400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
text-align: center;
margin-bottom: 20px;
}
.device-info {
background-color: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 20px;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
color: #8b949e;
text-align: left;
display: none;
}
.device-info span { color: var(--text-color); }
.dashboard-panel {
background-color: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.stat-box {
text-align: center;
flex-grow: 1;
}
.stat-box h3 {
margin: 0;
color: var(--accent-color);
font-size: 28px;
}
.stat-box p {
margin: 5px 0 0 0;
font-size: 14px;
color: #8b949e;
}
.slider-container {
margin: 20px 0;
text-align: left;
}
input[type=range] {
width: 100%;
margin-top: 10px;
accent-color: var(--accent-color);
}
h1 { margin-bottom: 5px; }
h1 span { color: var(--accent-color); }
.sub { color: #8b949e; margin-bottom: 25px; }
.status-box {
font-family: 'Courier New', Courier, monospace;
background-color: #010409;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
height: 120px;
overflow-y: auto;
text-align: left;
}
.status-box p {
margin: 0 0 5px 0;
color: var(--success-color);
font-size: 14px;
}
.btn {
background-color: #238636;
color: #ffffff;
border: 1px solid rgba(240, 246, 252, 0.1);
border-radius: 6px;
padding: 10px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn:hover { background-color: #2ea043; }
.hidden { display: none; }
.chat-box {
background-color: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
height: 500px;
overflow-y: auto;
text-align: left;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.chat-msg {
background-color: #0d1117;
padding: 12px;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
font-size: 15px;
}
.chat-prompt {
color: #8b949e;
font-size: 13px;
margin-bottom: 5px;
display: block;
}
.token-detail {
background: #010409;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px 12px;
margin-top: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.8;
display: none;
}
.token-detail.visible { display: block; }
.token-detail .tok {
background: #1c2333;
border: 1px solid #30363d;
border-radius: 3px;
padding: 2px 5px;
margin: 2px;
display: inline-block;
color: var(--text-color);
}
.token-detail .tok-en { border-color: #58a6ff44; }
.token-detail .tok-fi { border-color: #d2992244; }
.toggle-tokens {
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
color: #8b949e;
font-size: 12px;
padding: 3px 8px;
cursor: pointer;
}
.toggle-tokens:hover { color: var(--text-color); border-color: #8b949e; }
</style>
</head>
<body>
<div class="container">
<h1>Kipinä <span>Agent Dashboard</span></h1>
<p class="sub">Hajautettu WebGPU Laskentaverkko</p>
<!-- Global Cluster Statistics (UI) -->
<div class="dashboard-panel">
<div class="stat-box" style="border-right: 1px solid #30363d;">
<h3 id="stat-nodes">0</h3>
<p>Aktiivisia Nodeja</p>
</div>
<div class="stat-box" style="border-right: 1px solid #30363d;">
<h3 id="stat-tasks">0</h3>
<p>Verkossa Suoritettua Tehtävää (Globaali)</p>
</div>
<div class="stat-box">
<h3 id="stat-vram">0 GB</h3>
<p>Verkon yhteis-VRAM</p>
</div>
</div>
<div id="device-info" class="device-info"></div>
<div id="initial-state">
<button id="start-btn" class="btn">Liity laskentaverkkoon</button>
</div>
<div id="active-state" class="hidden">
<div class="slider-container">
<label for="gpu-load">Oman Laitteen Kuormitusrajoitin: <strong id="load-display" style="color:var(--accent-color);">50%</strong></label>
<input type="range" id="gpu-load" min="0" max="75" value="50">
<p style="font-size: 11px; color:#8b949e;">Hallitsee "Duty Cyclea" kuinka pitkään Wasm-ydin pakotetaan nukkumaan Tensorimatriisien laskennan välissä.</p>
</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>
<div id="log-box" class="status-box">
<p>> Odotetaan uusia tehtäviä Hubulta...</p>
</div>
</div>
</div>
<script type="module">
import init, { start_agent_node, set_gpu_load } from './pkg/node.js';
const btn = document.getElementById('start-btn');
const logBox = document.getElementById('log-box');
const loadSlider = document.getElementById('gpu-load');
const loadDisplay = document.getElementById('load-display');
const statNodes = document.getElementById('stat-nodes');
const statVram = document.getElementById('stat-vram');
const statTasks = document.getElementById('stat-tasks');
const chatBox = document.getElementById('chat-box');
let currentChatMsg = null;
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
const originalLog = console.log;
console.log = function(...args) {
originalLog.apply(console, args);
// Älä tulosta teknisiä WGPU warningeja suoraan AI:n näytölle jos niitä on
let msg = args.join(' ');
if (msg.includes("wgpu") || msg.includes("vastaanotettu")) return; // Siistitään spämmäävät lokit näkymästä, koska niitä tulee nyt sata sekunnissa
const p = document.createElement('p');
p.textContent = '> ' + msg;
logBox.appendChild(p);
// Ehkäistään selaimen jumittuminen sadoista tuhansista lokiriveistä pitkässä GPU-ajossa
if (logBox.children.length > 30) {
logBox.removeChild(logBox.firstChild);
}
logBox.scrollTop = logBox.scrollHeight;
};
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
loadSlider.addEventListener('input', (e) => {
loadDisplay.textContent = e.target.value + '%';
if (window.wasm_active) {
set_gpu_load(parseInt(e.target.value));
}
});
// 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) => {
try {
const data = JSON.parse(event.data);
if (data.type === "stats") {
statNodes.textContent = data.nodes;
statVram.textContent = data.vram_gb + " GB";
if (data.tasks !== undefined) {
statTasks.textContent = data.tasks;
}
} else if (data.type === "node_joined") {
chatBox.classList.remove('hidden');
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg';
msgDiv.style.borderLeftColor = 'var(--success-color)';
msgDiv.innerHTML = `<span style="color:var(--success-color)">[Järjestelmä] Uusi solmu (ID: ${data.node_id}) liittyi verkon työjohdon piiriin!</span>`;
chatBox.appendChild(msgDiv);
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === "pair_task") {
chatBox.classList.remove('hidden');
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
chatBox.innerHTML = '';
}
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg';
msgDiv.innerHTML = `<span class="chat-prompt">Tokenisoidaan...</span>
<div style="font-size:12px;color:#8b949e">
<div><strong style="color:#58a6ff">EN</strong> "${data.en}"</div>
<div><strong style="color:#d29922">FI</strong> "${data.fi}"</div>
</div>`;
chatBox.appendChild(msgDiv);
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === "pair_done") {
chatBox.classList.remove('hidden');
const en = data.en || {};
const fi = data.fi || {};
const overhead = data.overhead_pct || 0;
const nodeId = data.node_id || "?";
const ms = data.duration_ms || 0;
const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2));
const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2));
// Värit tehokkuudelle
const cptColor = (v) => v >= 4 ? "#3fb950" : v >= 3 ? "#d29922" : "#f85149";
// Ylikustannuksen väri
const ovColor = overhead > 20 ? "#f85149" : overhead > 0 ? "#d29922" : "#3fb950";
// Korvataan viimeisin "Tokenisoidaan..."-viesti, tai luodaan uusi
const lastMsg = chatBox.lastElementChild;
const msgDiv = (lastMsg && lastMsg.querySelector('.chat-prompt')?.textContent === 'Tokenisoidaan...')
? lastMsg : document.createElement('div');
msgDiv.className = 'chat-msg';
// Tokenilistat renderöitäväksi
const renderTokens = (tokens, cls) => (tokens || []).map(t =>
`<span class="tok ${cls}">${t.replace(/</g,'&lt;')}</span>`
).join('');
const enTokHtml = renderTokens(en.tokens, 'tok-en');
const fiTokHtml = renderTokens(fi.tokens, 'tok-fi');
const detailId = 'tok-' + Date.now();
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">${ms}ms</span>
</div>
</div>
<div style="font-size:14px;display:grid;grid-template-columns:32px 1fr auto auto auto;gap:6px 10px;align-items:baseline">
<strong style="color:#58a6ff">EN</strong>
<span style="color:#79b8ff">"${en.text || ''}"</span>
<span style="color:#8b949e">${en.char_count} m</span>
<span style="color:var(--accent-color);font-weight:600">${en.token_count} tok</span>
<span style="color:${cptColor(enCpt)};font-weight:600">${enCpt} m/t</span>
<strong style="color:#d29922">FI</strong>
<span style="color:#e3b341">"${fi.text || ''}"</span>
<span style="color:#8b949e">${fi.char_count} m</span>
<span style="color:var(--accent-color);font-weight:600">${fi.token_count} tok</span>
<span style="color:${cptColor(fiCpt)};font-weight:600">${fiCpt} m/t</span>
</div>
<div id="${detailId}" class="token-detail">
<div style="margin-bottom:6px"><strong style="color:#58a6ff;font-size:12px">EN (${en.token_count})</strong> ${enTokHtml}</div>
<div><strong style="color:#d29922;font-size:12px">FI (${fi.token_count})</strong> ${fiTokHtml}</div>
</div>
<div style="margin-top:10px;display:flex;justify-content:space-between;align-items:baseline;font-size:14px">
<span style="color:#8b949e">(<span style="color:#d29922">${fi.token_count}</span> / <span style="color:#58a6ff">${en.token_count}</span> 1) × 100 = <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
<span style="font-size:15px">FI ylikustannus: <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
</div>`;
if (!msgDiv.parentNode) chatBox.appendChild(msgDiv);
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
}
} catch(e) {}
};
btn.addEventListener('click', async () => {
// Kerätään laitteistotiedot
let hasWebGPU = false;
const deviceInfo = {
allocated_gb: 4,
cpu_cores: navigator.hardwareConcurrency || 0,
device_memory_gb: navigator.deviceMemory || 0,
platform: navigator.platform || "",
gpu: null
};
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
hasWebGPU = true;
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";
const backendStr = hasWebGPU ? "WebGPU" : "CPU (NdArray)";
const vramStr = deviceInfo.gpu?.estimated_vram_gb ? `~${deviceInfo.gpu.estimated_vram_gb} GB` : "?";
// navigator.deviceMemory on rajoitettu max 8 GB:iin — merkitään arvio
const ramNote = deviceInfo.device_memory_gb >= 8 ? "8+ GB (selaimen raja)" : `~${deviceInfo.device_memory_gb} GB`;
// Näytetään laitetiedot paneelissa
const diPanel = document.getElementById('device-info');
diPanel.style.display = 'block';
diPanel.innerHTML = [
`Backend: <span>${backendStr}</span>`,
`GPU: <span>${gpuStr}</span>`,
hasWebGPU ? `VRAM: <span>${vramStr}</span>` : null,
`CPU: <span>${deviceInfo.cpu_cores} ydintä</span>`,
`RAM: <span>${ramNote}</span>`,
`Varaus: <span>${deviceInfo.allocated_gb} GB</span>`
].filter(Boolean).join(' &middot; ');
document.getElementById('initial-state').classList.add('hidden');
document.getElementById('active-state').classList.remove('hidden');
btn.style.display = 'none';
try {
console.log("Ladataan Burn Wasm -binääriä...");
await init();
window.wasm_active = true;
// 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`;
await start_agent_node(wsUrl, hasWebGPU, JSON.stringify(deviceInfo));
} catch(e) {
console.log("Virhe GPU-käynnistyksessä: " + e);
}
});
</script>
</body>
</html>