Files
agentic-studio/network-poc/static/index.html
2026-04-01 22:14:48 +03:00

636 lines
28 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; }
.compat-banner {
border-radius: 6px;
padding: 14px 18px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.6;
display: none;
}
.compat-banner.gpu {
background: #23392020;
border: 1px solid #3fb95040;
color: var(--success-color);
}
.compat-banner.cpu {
background: #d2992215;
border: 1px solid #d2992240;
color: #d29922;
}
.compat-banner code {
background: #0d1117;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
color: var(--text-color);
}
.compat-banner summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.compat-banner details[open] summary {
margin-bottom: 10px;
}
.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; }
.metric-card {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.metric-val {
font-size: 20px;
font-weight: 700;
color: var(--accent-color);
}
.metric-label {
font-size: 11px;
color: #8b949e;
margin-top: 2px;
}
</style>
</head>
<body>
<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>
<!-- 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="compat-banner" class="compat-banner"></div>
<div id="initial-state">
<button id="start-btn" class="btn">Liity laskentaverkkoon</button>
</div>
<div id="active-state" class="hidden">
<!-- Resurssipaneeli -->
<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>
</div>
<!-- Kuormitussäädin -->
<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
<span>Laskentatehon rajoitin</span>
<strong id="load-display" style="color:var(--accent-color)">50%</strong>
</div>
<input type="range" id="gpu-load" min="0" max="100" value="50" style="width:100%;accent-color:var(--accent-color)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:2px">
<span>Pysäytetty</span><span>Säästö</span><span>Tasapaino</span><span>Suorituskyky</span><span>Maksimi</span>
</div>
</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">
<div class="metric-val" id="m-tasks">0</div>
<div class="metric-label">Tehtäviä</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-avg-time">-</div>
<div class="metric-label">Ka. aika</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-tokens">0</div>
<div class="metric-label">Tokeneita</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-uptime">0s</div>
<div class="metric-label">Käynnissä</div>
</div>
</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>
<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;
// Reaaliaikaiset metriikat
const metrics = {
tasks: 0,
totalTokens: 0,
totalTimeMs: 0,
startTime: null,
};
function updateMetrics() {
document.getElementById('m-tasks').textContent = metrics.tasks;
document.getElementById('m-tokens').textContent = metrics.totalTokens.toLocaleString('fi-FI');
document.getElementById('m-avg-time').textContent = metrics.tasks > 0
? (metrics.totalTimeMs / metrics.tasks).toFixed(1) + 'ms'
: '-';
if (metrics.startTime) {
const sec = Math.floor((Date.now() - metrics.startTime) / 1000);
if (sec < 60) document.getElementById('m-uptime').textContent = sec + 's';
else if (sec < 3600) document.getElementById('m-uptime').textContent = Math.floor(sec/60) + 'min';
else document.getElementById('m-uptime').textContent = Math.floor(sec/3600) + 'h ' + (Math.floor(sec/60)%60) + 'min';
}
}
setInterval(updateMetrics, 1000);
// 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) => {
const val = parseInt(e.target.value);
loadDisplay.textContent = val + '%';
if (window.wasm_active) {
set_gpu_load(val);
}
// Tilapäivitys
const statusEl = document.getElementById('node-status');
if (val === 0) {
statusEl.textContent = 'Pysäytetty';
statusEl.style.color = '#f85149';
} else if (val <= 25) {
statusEl.textContent = 'Säästötila';
statusEl.style.color = '#d29922';
} else {
statusEl.textContent = 'Aktiivinen';
statusEl.style.color = 'var(--success-color)';
}
});
// 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;
}
if (data.version) {
document.getElementById('hub-version').textContent = 'v' + data.version;
}
} 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;
// Päivitetään metriikat
metrics.tasks++;
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
metrics.totalTimeMs += ms;
updateMetrics();
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; ');
// Yhteensopivuusbanneri
const banner = document.getElementById('compat-banner');
banner.style.display = 'block';
if (hasWebGPU) {
banner.className = 'compat-banner gpu';
banner.innerHTML = `GPU-kiihdytys aktiivinen — ${gpuStr}`;
} else {
// Tunnistetaan selain ohjeen personointia varten
const ua = navigator.userAgent;
const isFirefox = ua.includes('Firefox');
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave);
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
const isLinux = ua.includes('Linux');
let browserTip = '';
if (isFirefox) {
browserTip = `
<p><strong>Firefox</strong> ei tue WebGPU:ta oletuksena.</p>
<p>Ota käyttöön: <code>about:config</code> → <code>dom.webgpu.enabled</code> = <code>true</code> → käynnistä uudelleen.</p>
<p>Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.</p>`;
} else if ((isChrome || isBrave) && isLinux) {
const browser = isBrave ? 'brave-browser' : 'google-chrome';
browserTip = `
<p><strong>${isBrave ? 'Brave' : 'Chrome'} + Linux</strong>: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.</p>
<p>Kokeile käynnistää selain komentoriviltä:</p>
<code>${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11</code>`;
} else if (isSafari) {
browserTip = `
<p><strong>Safari</strong>: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).</p>
<p>Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.</p>`;
} else {
browserTip = `
<p>Selaimesi ei tue WebGPU:ta. Kokeile <strong>Chrome 113+</strong> tai <strong>Brave</strong>.</p>`;
}
banner.className = 'compat-banner cpu';
banner.innerHTML = `
<details>
<summary>CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita</summary>
${browserTip}
<p style="margin-top:8px;color:#8b949e;font-size:12px">Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.</p>
</details>`;
}
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;
metrics.startTime = Date.now();
// 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>