diff --git a/network-poc/hub/nodes.db b/network-poc/hub/nodes.db index b30b85c..237a775 100644 Binary files a/network-poc/hub/nodes.db and b/network-poc/hub/nodes.db differ diff --git a/network-poc/hub/src/main.rs b/network-poc/hub/src/main.rs index bd5fd9c..e98fe7f 100644 --- a/network-poc/hub/src/main.rs +++ b/network-poc/hub/src/main.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; use std::sync::{Arc, Mutex}; use tokio::sync::broadcast; -use tower_http::services::ServeDir; +use tower_http::services::{ServeDir, ServeFile}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod db; @@ -341,7 +341,10 @@ async fn main() { .route("/api/stats", get(api_stats)) .route("/api/v1/chat/completions", axum::routing::post(api_chat_completions)) .route("/admin", get(admin_page)) - .nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string()))) + .nest_service("/", { + let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string()); + ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir))) + }) .with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); diff --git a/network-poc/nodes.db b/network-poc/nodes.db new file mode 100644 index 0000000..d71206e Binary files /dev/null and b/network-poc/nodes.db differ diff --git a/network-poc/static/avatars/aikuinen_susi.png b/network-poc/static/avatars/aikuinen_susi.png new file mode 100644 index 0000000..15f0b8c Binary files /dev/null and b/network-poc/static/avatars/aikuinen_susi.png differ diff --git a/network-poc/static/avatars/forge_hero.svg b/network-poc/static/avatars/forge_hero.svg new file mode 100644 index 0000000..6bba7a3 --- /dev/null +++ b/network-poc/static/avatars/forge_hero.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/network-poc/static/avatars/karhunpentu.png b/network-poc/static/avatars/karhunpentu.png new file mode 100644 index 0000000..17a2044 Binary files /dev/null and b/network-poc/static/avatars/karhunpentu.png differ diff --git a/network-poc/static/avatars/kettu_notext.png b/network-poc/static/avatars/kettu_notext.png new file mode 100644 index 0000000..81e2c18 Binary files /dev/null and b/network-poc/static/avatars/kettu_notext.png differ diff --git a/network-poc/static/avatars/kipina_notext.png b/network-poc/static/avatars/kipina_notext.png new file mode 100644 index 0000000..63077ca Binary files /dev/null and b/network-poc/static/avatars/kipina_notext.png differ diff --git a/network-poc/static/avatars/laiskiainen.png b/network-poc/static/avatars/laiskiainen.png new file mode 100644 index 0000000..d1dbf2c Binary files /dev/null and b/network-poc/static/avatars/laiskiainen.png differ diff --git a/network-poc/static/avatars/laiskiainen_notext.png b/network-poc/static/avatars/laiskiainen_notext.png new file mode 100644 index 0000000..df61301 Binary files /dev/null and b/network-poc/static/avatars/laiskiainen_notext.png differ diff --git a/network-poc/static/avatars/lizard_ai.svg b/network-poc/static/avatars/lizard_ai.svg new file mode 100644 index 0000000..43d1cba --- /dev/null +++ b/network-poc/static/avatars/lizard_ai.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + } + </> + + diff --git a/network-poc/static/avatars/pesukarhu.png b/network-poc/static/avatars/pesukarhu.png new file mode 100644 index 0000000..99d406e Binary files /dev/null and b/network-poc/static/avatars/pesukarhu.png differ diff --git a/network-poc/static/avatars/pesukarhu_notext.png b/network-poc/static/avatars/pesukarhu_notext.png new file mode 100644 index 0000000..faaa36b Binary files /dev/null and b/network-poc/static/avatars/pesukarhu_notext.png differ diff --git a/network-poc/static/avatars/raccoon_ai.svg b/network-poc/static/avatars/raccoon_ai.svg new file mode 100644 index 0000000..5e9d292 --- /dev/null +++ b/network-poc/static/avatars/raccoon_ai.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/network-poc/static/avatars/sloth_ai.svg b/network-poc/static/avatars/sloth_ai.svg new file mode 100644 index 0000000..1194b47 --- /dev/null +++ b/network-poc/static/avatars/sloth_ai.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Z + z + z + + + + + + + + + diff --git a/network-poc/static/avatars/susi_notext.png b/network-poc/static/avatars/susi_notext.png new file mode 100644 index 0000000..ca18894 Binary files /dev/null and b/network-poc/static/avatars/susi_notext.png differ diff --git a/network-poc/static/index.html b/network-poc/static/index.html index 5384df5..53405d9 100644 --- a/network-poc/static/index.html +++ b/network-poc/static/index.html @@ -382,39 +382,115 @@ } .terminal-line { margin: 4px 0; } .terminal-prompt { color: #d29922; } - .avatar-grid { - display:flex; - gap:15px; - justify-content:center; - margin-bottom:20px; + .org-chart { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40px; + perspective: 1000px; + } + .org-level { + display: flex; + justify-content: center; + gap: 40px; + position: relative; + z-index: 2; + } + .org-connector { + width: 2px; + height: 40px; + background: linear-gradient(to bottom, rgba(88, 166, 255, 0.8), rgba(88, 166, 255, 0.2)); + margin: 0px auto; + box-shadow: 0 0 10px rgba(88, 166, 255, 0.5); + } + .org-branch { + width: 420px; + height: 40px; + border-top: 2px solid rgba(88, 166, 255, 0.5); + border-left: 2px solid rgba(88, 166, 255, 0.5); + border-right: 2px solid rgba(88, 166, 255, 0.5); + border-top-left-radius: 12px; + border-top-right-radius: 12px; + margin-top: 0; + margin-bottom: -2px; + box-shadow: inset 0 3px 6px -3px rgba(88, 166, 255, 0.4); } .avatar-card { - background:var(--panel-bg); - border:1px solid var(--border-color); - border-radius:8px; - padding:10px; - text-align:center; - width:120px; - opacity: 0.6; - transition: all 0.3s; + background: linear-gradient(145deg, rgba(33, 38, 45, 0.4) 0%, rgba(13, 17, 23, 0.8) 100%); + backdrop-filter: blur(12px); + border: 1px solid rgba(240, 246, 252, 0.1); + border-radius: 16px; + padding: 12px 10px; + text-align: center; + width: 120px; + opacity: 0.5; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + box-shadow: 0 8px 16px rgba(0,0,0,0.3); + } + .avatar-card:hover { + opacity: 0.85; + transform: translateY(-4px) scale(1.02); + border-color: rgba(240, 246, 252, 0.3); + box-shadow: 0 12px 20px rgba(0,0,0,0.4); } .avatar-card img { - width:80px; - height:80px; - border-radius:50%; - margin-bottom:10px; - border:2px solid var(--border-color); + width: 80px; + height: 80px; + border-radius: 18px; + margin-bottom: 8px; + border: 2px solid rgba(240, 246, 252, 0.1); + transition: all 0.4s ease; + object-fit: cover; + background: #010409; } - .avatar-card.active { + .avatar-card.active, .avatar-card.selected { opacity: 1; - transform: translateY(-5px); + transform: translateY(-8px) scale(1.05); + border-color: var(--accent-color); + background: linear-gradient(145deg, rgba(88, 166, 255, 0.15) 0%, rgba(13, 17, 23, 0.9) 100%); + box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88, 166, 255, 0.3); + z-index: 2; } - .avatar-card.active img { - border-color:var(--accent-color); - box-shadow: 0 0 15px var(--accent-color); + .avatar-card.active img, .avatar-card.selected img { + border-color: var(--accent-color); + box-shadow: 0 0 25px rgba(88, 166, 255, 0.5); + transform: scale(1.05); } - .avatar-name { font-weight: bold; font-size: 13px; color: var(--text-color); } - .avatar-role { font-size: 11px; color: #8b949e; margin-top: 2px; } + .avatar-name { font-weight: 700; font-size: 14px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; } + .avatar-role { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; } + .agent-prompt-editor { + margin-top: 12px; + background: var(--panel-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 12px; + display: none; + } + .agent-prompt-editor.visible { display: block; } + .agent-prompt-editor textarea { + width: 100%; + background: #010409; + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-color); + font-size: 13px; + font-family: 'Courier New', monospace; + padding: 8px; + resize: vertical; + min-height: 60px; + outline: none; + } + .agent-prompt-editor textarea:focus { border-color: var(--accent-color); } + .agent-prompt-label { + font-size: 12px; + color: #8b949e; + margin-bottom: 6px; + display: flex; + justify-content: space-between; + align-items: center; + } + .agent-prompt-label strong { color: var(--text-color); } @@ -713,33 +789,79 @@
-
+
- Kipinä Agent Workspace +
+ Kipinä Agent Workspace + +
Monitoring Active
-
-
- Forge -
KPN CLI
-
Paikallinen Ohjaus
+
+ +
+
+ Asiakas (Kettu) +
Asiakas
+
Tuoteomistaja
+
-
- Gecko -
Qwen-Coder
-
Koodiagentti
+ +
+ + +
+ +
+ Tarkkailija (Aikuinen Susi) +
Tarkkailija
+
Laadunvalvonta
+
+ +
+ Manageri (Pesukarhu) +
Manageri
+
KPN CLI
+
-
- Serpent -
SmolLM
-
Logiikka
+ +
+ + +
+
+ Koodari (Salamanteri) +
Koodari
+
Ohjelmistokehitys
+
+
+ Data-Agentti (Karhunpentu) +
Data
+
Tietokannat
+
+
+ QA (Pikkususi) +
QA
+
Testaus
+
+
+ DevOps (Laiskiainen) +
DevOps
+
Käyttöönotto
+
-
- Discord -
Swarm
-
WebGPU Solmu
+
+ +
+
+ + Tallennettu +
+ +
@@ -747,6 +869,11 @@
$ kpn hub connect wss://localhost
✓ Yhdistetty Kipinä Hubiin
+
+ $ + +
@@ -756,6 +883,114 @@ import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js'; // Päävälilehtien vaihto + // Agenttien system promptit + const agentPrompts = { + client: { name: 'Asiakas — Projektin vaatimukset', model: 'user-input', default: 'Kirjoita tähän asiakkaan toiveet ja projektin vaatimukset. Orkestraattori (Manageri) purkaa ja delegoi nämä työt asiantuntijoille.' }, + observer: { name: 'Tarkkailija — System Prompt', model: 'deepseek-r1', default: 'Olet ohjelmistoprojektin riippumaton valvoja. Sinulla on täysi pääsy kaikkiin projektin tietoihin ja muiden agenttien keskusteluihin. Valvo tiimin (Manageri, Koodari, Data, QA, DevOps) toimintaa asiantuntijana kokonaisuutena ja huomauta välittömästi visio- tai turvallisuusriskeistä.' }, + manager: { name: 'Manageri — System Prompt', model: 'qwen-coder', default: 'Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.' }, + coder: { name: 'Koodari — System Prompt', model: 'qwen-coder', default: 'Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.' }, + data: { name: 'Data-Agentti — System Prompt', model: 'qwen-coder', default: 'Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.' }, + qa: { name: 'QA — System Prompt', model: 'smollm-135m', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' }, + tester: { name: 'DevOps — System Prompt', model: 'smollm-135m', default: 'Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.' }, + }; + const selectedAgents = new Set(); + let sharedPrompt = localStorage.getItem('kpn-shared-prompt') || ''; + + // Ladataan tallennetut promptit localStoragesta + for (const key of Object.keys(agentPrompts)) { + const saved = localStorage.getItem('kpn-agent-prompt-' + key); + if (saved) agentPrompts[key].prompt = saved; + else agentPrompts[key].prompt = agentPrompts[key].default; + } + + function updatePromptEditor() { + const editor = document.getElementById('agent-prompt-editor'); + const nameEl = document.getElementById('agent-prompt-name'); + const textEl = document.getElementById('agent-prompt-text'); + const sharedEl = document.getElementById('shared-prompt-section'); + const btnAll = document.getElementById('btn-toggle-all'); + + if (btnAll) { + if (selectedAgents.size === Object.keys(agentPrompts).length) { + btnAll.textContent = 'Tyhjennä valinnat'; + } else { + btnAll.textContent = 'Valitse kaikki'; + } + } + + if (selectedAgents.size === 0) { + editor.classList.remove('visible'); + return; + } + + editor.classList.add('visible'); + + if (selectedAgents.size === 1) { + const agent = [...selectedAgents][0]; + const cfg = agentPrompts[agent]; + nameEl.textContent = cfg.name; + textEl.value = cfg.prompt; + sharedEl.style.display = 'none'; + } else { + const names = [...selectedAgents].map(a => agentPrompts[a].name.split(' — ')[0]); + nameEl.textContent = names.join(' + ') + ' — Yhteinen konteksti'; + textEl.value = sharedPrompt; + sharedEl.style.display = 'block'; + } + } + + window.selectAgent = function(agent) { + const card = document.querySelector(`[data-agent="${agent}"]`); + + if (selectedAgents.has(agent)) { + selectedAgents.delete(agent); + card.classList.remove('selected'); + card.classList.remove('active'); + } else { + selectedAgents.add(agent); + card.classList.add('selected'); + } + + updatePromptEditor(); + if (selectedAgents.size > 0) { + document.getElementById('agent-prompt-text')?.focus(); + } + }; + + window.toggleAllAgents = function() { + const allAgents = Object.keys(agentPrompts); + if (selectedAgents.size === allAgents.length) { + selectedAgents.clear(); + document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('selected', 'active')); + } else { + allAgents.forEach(a => { + selectedAgents.add(a); + const card = document.querySelector(`[data-agent="${a}"]`); + if (card) card.classList.add('selected'); + }); + } + updatePromptEditor(); + }; + + // Autosave prompti + document.getElementById('agent-prompt-text')?.addEventListener('input', (e) => { + if (selectedAgents.size === 0) return; + const saved = document.getElementById('agent-prompt-saved'); + + if (selectedAgents.size === 1) { + const agent = [...selectedAgents][0]; + agentPrompts[agent].prompt = e.target.value; + localStorage.setItem('kpn-agent-prompt-' + agent, e.target.value); + } else { + sharedPrompt = e.target.value; + localStorage.setItem('kpn-shared-prompt', e.target.value); + } + + saved.style.opacity = '1'; + clearTimeout(saved._t); + saved._t = setTimeout(() => saved.style.opacity = '0', 1500); + }); + window.switchMainTab = function(tab) { document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active')); @@ -779,9 +1014,10 @@ } }; - // URL-hash navigointi: #codelab tai #network - if (window.location.hash === '#codelab') { - switchMainTab('codelab'); + // URL-hash navigointi + const initHash = window.location.hash.replace('#', ''); + if (['codelab', 'agents'].includes(initHash)) { + switchMainTab(initHash); } // Synkronoi coder-status kun WS on jo auki (suora #codelab navigointi) @@ -1020,6 +1256,142 @@ coderEl.style.color = '#f85149'; } }; + // Terminaalin komentorivi + const termInput = document.getElementById('term-input'); + const termPanel = document.getElementById('agent-terminal'); + const termHistory = []; + let termHistIdx = -1; + + function termLog(html, color) { + const div = document.createElement('div'); + div.className = 'terminal-line'; + if (color) div.style.color = color; + div.innerHTML = html; + termPanel.appendChild(div); + while (termPanel.children.length > 100) termPanel.removeChild(termPanel.firstChild); + termPanel.scrollTop = termPanel.scrollHeight; + } + + async function kpnRun(model, prompt) { + termLog(` → ${model} käsittelee...`, '#8b949e'); + try { + const taskId = crypto.randomUUID(); + // Liitetään yhteinen konteksti + agentin oma system prompt + const agent = Object.values(agentPrompts).find(a => a.model === model); + const parts = []; + if (sharedPrompt) parts.push(sharedPrompt); + if (agent && agent.prompt) parts.push(agent.prompt); + parts.push(prompt); + const fullPrompt = parts.join('\n\n'); + const res = await fetch('/api/v1/chat/completions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId }), + }); + if (!res.ok) { + termLog(` ✗ Virhe: ${res.status} ${res.statusText}`, '#f85149'); + return; + } + const data = await res.json(); + const response = (data.response || '').trim(); + const tokGen = data.tokens_generated || 0; + termLog(` ${data.model || model} (${tokGen} tok)`); + termLog(` ${response.replace(/$ ${cmd.replace(/ { + if (e.key === 'Enter') { + const cmd = termInput.value.trim(); + if (cmd) termExec(cmd); + termInput.value = ''; + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (termHistIdx < termHistory.length - 1) { + termHistIdx++; + termInput.value = termHistory[termHistIdx]; + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (termHistIdx > 0) { + termHistIdx--; + termInput.value = termHistory[termHistIdx]; + } else { + termHistIdx = -1; + termInput.value = ''; + } + } + }); + + // Klikkaa terminaalipaneelia → fokusoi input + termPanel?.addEventListener('click', () => termInput?.focus()); + uiSocket.onmessage = (event) => { try { const raw = event.data; @@ -1281,9 +1653,8 @@ } document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active')); const model = data.model || ''; - if (model.includes('coder')) document.getElementById('avatar-coder')?.classList.add('active'); - else document.getElementById('avatar-smol')?.classList.add('active'); - document.getElementById('avatar-discord')?.classList.add('active'); + if (model.includes('coder') || model.includes('Coder')) document.getElementById('avatar-coder')?.classList.add('active'); + else document.getElementById('avatar-tester')?.classList.add('active'); } } catch(e) {} };