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 @@
+
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
-
-
-

-
KPN CLI
-
Paikallinen Ohjaus
+
+
+
+
+

+
Asiakas
+
Tuoteomistaja
+
-
-

-
Qwen-Coder
-
Koodiagentti
+
+
+
+
+
+
+
+

+
Tarkkailija
+
Laadunvalvonta
+
+
+
+

+
Manageri
+
KPN CLI
+
-
-

-
SmolLM
-
Logiikka
+
+
+
+
+
+
+

+
Koodari
+
Ohjelmistokehitys
+
+
+

+
Data
+
Tietokannat
+
+
+

+
QA
+
Testaus
+
+
+

+
DevOps
+
Käyttöönotto
+
-
-

-
Swarm
-
WebGPU Solmu
+
+
+
+
+ —
+ Tallennettu
+
+
+
+ Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
@@ -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) {}
};