lisätty pikku pörriäiset

This commit is contained in:
2026-04-03 08:55:07 +03:00
parent 185a40dbdf
commit 057d464fdd
17 changed files with 639 additions and 52 deletions

View File

@@ -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); }
</style>
</head>
<body>
@@ -713,33 +789,79 @@
<!-- PANEELI 3: Agents & CLI -->
<div id="panel-agents" class="main-panel" style="position: relative; border-radius: 6px;">
<div style="position: absolute; top:0; left:0; width:100%; height:100%; background: url('/avatars/forge_hero.png') no-repeat center center; background-size: cover; opacity: 0.15; z-index: 0; pointer-events: none; border-radius: 6px;"></div>
<div style="position: absolute; top:0; left:0; width:100%; height:100%; background: url('/avatars/forge_hero.svg') no-repeat center center; background-size: cover; opacity: 0.15; z-index: 0; pointer-events: none; border-radius: 6px;"></div>
<div style="background:rgba(13, 17, 23, 0.7); backdrop-filter: blur(4px); border:1px solid var(--border-color); border-radius:6px; padding:16px; margin-bottom:16px; position: relative; z-index: 1;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
<span style="font-weight:600;font-size:15px;color:var(--accent-color)">Kipinä Agent Workspace</span>
<div style="display:flex;align-items:center;gap:16px;">
<span style="font-weight:600;font-size:15px;color:var(--accent-color)">Kipinä Agent Workspace</span>
<button id="btn-toggle-all" onclick="toggleAllAgents()" style="background:rgba(33, 38, 45, 0.8);border:1px solid var(--border-color);color:#c9d1d9;font-size:11px;padding:4px 12px;border-radius:4px;cursor:pointer;">Valitse kaikki</button>
</div>
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
</div>
<div class="avatar-grid">
<div class="avatar-card active" id="avatar-kpn">
<img src="/avatars/forge_hero.png" alt="Forge">
<div class="avatar-name">KPN CLI</div>
<div class="avatar-role">Paikallinen Ohjaus</div>
<div class="org-chart">
<!-- Taso 1 -->
<div class="org-level">
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')">
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)">
<div class="avatar-name">Asiakas</div>
<div class="avatar-role">Tuoteomistaja</div>
</div>
</div>
<div class="avatar-card" id="avatar-coder">
<img src="/avatars/gecko_hero.png" alt="Gecko">
<div class="avatar-name">Qwen-Coder</div>
<div class="avatar-role">Koodiagentti</div>
<div class="org-connector"></div>
<!-- Taso 2 -->
<div class="org-level" style="position: relative;">
<!-- Tarkkailija laitetaan erilleen kauemmas sivuun jotta se näyttää itsenäiseltä valvojalta -->
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer')" style="position: absolute; right: calc(50% + 240px); top: 0;">
<img src="/avatars/aikuinen_susi.png" alt="Tarkkailija (Aikuinen Susi)">
<div class="avatar-name">Tarkkailija</div>
<div class="avatar-role">Laadunvalvonta</div>
</div>
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager')">
<img src="/avatars/pesukarhu_notext.png" alt="Manageri (Pesukarhu)">
<div class="avatar-name">Manageri</div>
<div class="avatar-role">KPN CLI</div>
</div>
</div>
<div class="avatar-card" id="avatar-smol">
<img src="/avatars/serpent_hero.png" alt="Serpent">
<div class="avatar-name">SmolLM</div>
<div class="avatar-role">Logiikka</div>
<div class="org-branch"></div>
<!-- Taso 3 -->
<div class="org-level" style="gap: 20px;">
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder')">
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
<div class="avatar-name">Koodari</div>
<div class="avatar-role">Ohjelmistokehitys</div>
</div>
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
<img src="/avatars/karhunpentu.png" alt="Data-Agentti (Karhunpentu)">
<div class="avatar-name">Data</div>
<div class="avatar-role">Tietokannat</div>
</div>
<div class="avatar-card" id="avatar-qa" data-agent="qa" onclick="selectAgent('qa')">
<img src="/avatars/susi_notext.png" alt="QA (Pikkususi)">
<div class="avatar-name">QA</div>
<div class="avatar-role">Testaus</div>
</div>
<div class="avatar-card" id="avatar-tester" data-agent="tester" onclick="selectAgent('tester')">
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
<div class="avatar-name">DevOps</div>
<div class="avatar-role">Käyttöönotto</div>
</div>
</div>
<div class="avatar-card" id="avatar-discord">
<img src="/avatars/discord_1.png" alt="Discord">
<div class="avatar-name">Swarm</div>
<div class="avatar-role">WebGPU Solmu</div>
</div>
<div class="agent-prompt-editor" id="agent-prompt-editor">
<div class="agent-prompt-label">
<strong id="agent-prompt-name"></strong>
<span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span>
</div>
<textarea id="agent-prompt-text" placeholder="Kirjoita system prompt..."></textarea>
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
</div>
</div>
@@ -747,6 +869,11 @@
<div class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div>
<div class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div>
</div>
<div style="display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
<span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span>
<input id="term-input" type="text" placeholder="kpn run coder &quot;kirjoita hello world&quot;" spellcheck="false"
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
</div>
</div>
</div><!-- /panel-agents -->
@@ -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(` → <span style="color:#58a6ff">${model}</span> 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(` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${data.model || model}</span> <span style="color:#8b949e">(${tokGen} tok)</span>`);
termLog(` ${response.replace(/</g,'&lt;').replace(/\n/g,'\n ')}`, '#c9d1d9');
} catch (e) {
termLog(`${e.message}`, '#f85149');
}
}
function termExec(cmd) {
termLog(`<span class="terminal-prompt">$</span> ${cmd.replace(/</g,'&lt;')}`);
termHistory.unshift(cmd);
termHistIdx = -1;
const parts = cmd.trim().split(/\s+/);
if (parts[0] !== 'kpn') {
termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149');
return;
}
const sub = parts[1];
if (sub === 'help' || !sub) {
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
termLog(' kpn run &lt;malli&gt; "&lt;prompti&gt;" — aja tehtävä verkossa', '#a5d6ff');
termLog(' kpn status — verkon tila', '#a5d6ff');
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff');
return;
}
if (sub === 'clear') {
termPanel.innerHTML = '';
return;
}
if (sub === 'status') {
const nodes = statNodes.textContent || '0';
const vram = statVram.textContent || '?';
termLog(` Solmuja: ${nodes} | VRAM: ${vram} | Tehtäviä: ${statTasks.textContent || '0'}`, '#a5d6ff');
return;
}
if (sub === 'models') {
termLog(' smollm-135m — SmolLM 135M (kevyt)', '#a5d6ff');
termLog(' qwen-05b — Qwen2.5 0.5B', '#a5d6ff');
termLog(' phi3-mini — Phi-3 Mini', '#a5d6ff');
termLog(' qwen-coder — Qwen2.5-Coder 0.5B', '#a5d6ff');
termLog(' qwen-coder-3b — Qwen2.5-Coder 3B', '#a5d6ff');
return;
}
if (sub === 'hello') {
kpnRun('smollm-135m', 'Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.');
return;
}
if (sub === 'run') {
const model = parts[2];
const afterModel = cmd.replace(/^kpn\s+run\s+\S+\s*/, '');
const promptMatch = afterModel.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const prompt = (promptMatch && (promptMatch[1] || promptMatch[2] || promptMatch[3] || '')).trim();
if (!model || !prompt) {
termLog(' Käyttö: kpn run &lt;malli&gt; "&lt;prompti&gt;"', '#f85149');
return;
}
kpnRun(model, prompt);
return;
}
termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149');
}
termInput?.addEventListener('keydown', (e) => {
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) {}
};