diff --git a/network-poc/frontend/src/components/AgentBar.astro b/network-poc/frontend/src/components/AgentBar.astro index c6172da..0084b95 100644 --- a/network-poc/frontend/src/components/AgentBar.astro +++ b/network-poc/frontend/src/components/AgentBar.astro @@ -1,23 +1,52 @@ - -
-
- Manageri - Manageri + +
+ +
+
-
- Koodari - Koodari -
-
- Testaaja - Testaaja -
-
- QA - QA -
-
- Data - Data + +
+
+
+ Lisää +
+
+ + + diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro index c042813..1daee28 100644 --- a/network-poc/frontend/src/pages/index.astro +++ b/network-poc/frontend/src/pages/index.astro @@ -70,10 +70,85 @@ import AgentBar from "../components/AgentBar.astro"; const initHash = window.location.hash.replace('#',''); if (['editor','guide'].includes(initHash)) switchTab(initHash); - // === Agent selection === - window.selectAgent = function(agent) { - document.querySelectorAll('.agent-avatar').forEach(el => el.classList.remove('active')); - document.querySelector(`.agent-avatar[data-agent="${agent}"]`)?.classList.add('active'); + // === Agent selection & config === + let selectedAgent = null; + + function renderAgentBar() { + const bar = document.getElementById('agent-bar'); + const sorted = Object.entries(agents).sort((a,b) => (a[1].order||0) - (b[1].order||0)); + bar.innerHTML = sorted.map(([key, a]) => + `
` + + `${esc(a.name)}` + + `${esc(a.name)}
` + ).join(''); + } + renderAgentBar(); + + window.selectAgent = function(key) { + selectedAgent = (selectedAgent === key) ? null : key; // Toggle + renderAgentBar(); + const config = document.getElementById('agent-config'); + if (!selectedAgent) { config.style.display = 'none'; return; } + + const a = agents[key]; + config.style.display = 'block'; + document.getElementById('config-avatar').src = a.avatar; + document.getElementById('config-name').value = a.name; + document.getElementById('config-role').textContent = key; + document.getElementById('config-model').value = a.model; + document.getElementById('config-prompt').value = a.prompt || ''; + + // Pipeline-järjestys + const pipeline = document.getElementById('config-pipeline'); + const sorted = Object.entries(agents).sort((a,b) => (a[1].order||0) - (b[1].order||0)); + pipeline.innerHTML = sorted.map(([k,ag]) => + `${ag.name}` + ).join(''); + + // Muutosten tallennus + document.getElementById('config-name').oninput = () => { agents[key].name = document.getElementById('config-name').value; saveAgents(); renderAgentBar(); }; + document.getElementById('config-model').onchange = () => { agents[key].model = document.getElementById('config-model').value; saveAgents(); }; + document.getElementById('config-prompt').oninput = () => { agents[key].prompt = document.getElementById('config-prompt').value; saveAgents(); }; + }; + + window.closeAgentConfig = function() { selectedAgent = null; document.getElementById('agent-config').style.display = 'none'; renderAgentBar(); }; + + // Drag & drop järjestys + let dragKey = null; + window.dragAgent = function(e, key) { dragKey = key; e.dataTransfer.effectAllowed = 'move'; }; + window.dropAgent = function(e, targetKey) { + e.preventDefault(); + if (!dragKey || dragKey === targetKey) return; + const srcOrder = agents[dragKey].order; + const tgtOrder = agents[targetKey].order; + agents[dragKey].order = tgtOrder; + agents[targetKey].order = srcOrder; + saveAgents(); renderAgentBar(); + if (selectedAgent) selectAgent(selectedAgent); // Päivitä pipeline-näkymä + }; + + // Uuden agentin luonti + window.addCustomAgent = function() { + const id = 'custom_' + Date.now(); + const avatars = ['/avatars/bear.png','/avatars/beaver.png','/avatars/gecko.png','/avatars/lion.png','/avatars/penguin.png','/avatars/spider.png','/avatars/walrus.png','/avatars/serpent.png']; + agents[id] = { + name: 'Uusi agentti', + avatar: avatars[Math.floor(Math.random() * avatars.length)], + model: 'qwen-coder', + prompt: '', + order: Object.keys(agents).length, + }; + saveAgents(); renderAgentBar(); selectAgent(id); + }; + + // Agentin poisto + window.deleteAgent = function() { + if (!selectedAgent || !agents[selectedAgent]) return; + if (!confirm(`Poistetaanko ${agents[selectedAgent].name}?`)) return; + delete agents[selectedAgent]; + saveAgents(); selectedAgent = null; + document.getElementById('agent-config').style.display = 'none'; + renderAgentBar(); }; // === WebSocket === @@ -349,7 +424,26 @@ import AgentBar from "../components/AgentBar.astro"; }; // Agenttiprompti-mapping - const agentModels = { coder: 'qwen-coder', 'coder-3b': 'qwen-coder-3b', manager: 'qwen-coder', tester: 'qwen-coder', qa: 'qwen-coder' }; + // === Agenttikonfiguraatio (localStorage-persistenssi) === + const defaultAgents = { + manager: { name: 'Manageri', avatar: '/avatars/owl.png', model: 'qwen-coder', prompt: 'You are a project manager. Break tasks into files, prioritize, coordinate.', order: 0 }, + coder: { name: 'Koodari', avatar: '/avatars/chameleon.png', model: 'qwen-coder', prompt: 'You are an expert developer. Write clean, working code. Respond with code only.', order: 1 }, + tester: { name: 'Testaaja', avatar: '/avatars/mantis.png', model: 'qwen-coder', prompt: 'You are a code reviewer. Find bugs, suggest improvements. Be brief.', order: 2 }, + qa: { name: 'QA', avatar: '/avatars/tortoise.png', model: 'qwen-coder', prompt: 'You are a QA engineer. Write tests, verify edge cases.', order: 3 }, + data: { name: 'Data', avatar: '/avatars/elephant.png', model: 'qwen-coder', prompt: 'You are a database expert. Design schemas, optimize queries.', order: 4 }, + }; + + // Ladataan tallennettu konfiguraatio tai oletukset + let agents = JSON.parse(localStorage.getItem('kpn-agents') || 'null') || JSON.parse(JSON.stringify(defaultAgents)); + + function saveAgents() { localStorage.setItem('kpn-agents', JSON.stringify(agents)); } + + // Agenttimallin haku nimellä + function getAgentModel(name) { + const a = agents[name]; + return a ? a.model : name; // fallback: käytetään nimeä mallina + } + const agentModels = new Proxy({}, { get: (_, key) => getAgentModel(key) }); function termExec(cmd) { termLog(`$ ${esc(cmd)}`); @@ -434,9 +528,14 @@ import AgentBar from "../components/AgentBar.astro"; // === Project pipeline === async function kpnProject(task) { + const mgr = agents.manager || Object.values(agents)[0]; + const cdr = agents.coder || Object.values(agents)[1]; + const tst = agents.tester || Object.values(agents)[2]; + termLog(`━━━ Projekti käynnistyy ━━━`); - termLog(`\n[1] Manageri — suunnittelu`); - const plan = await kpnRun('qwen-coder', `List the source files needed for this project. One file per line, format:\nfilename.py: what this file contains\n\nRules:\n- Max 4 files\n- Only .py, .toml, .json, .html files\n- No directories, just filenames\n- Dependencies first (models.py before main.py)\n- Use pyproject.toml for deps\n\nProject: ${task}`); + termLog(`\n[1] ${esc(mgr.name)} — suunnittelu`); + const mgrPrompt = (mgr.prompt ? mgr.prompt + '\n\n' : '') + `List the source files needed for this project. One file per line, format:\nfilename.py: what this file contains\n\nRules:\n- Max 4 files\n- Only .py, .toml, .json, .html files\n- No directories, just filenames\n- Dependencies first (models.py before main.py)\n- Use pyproject.toml for deps\n\nProject: ${task}`; + const plan = await kpnRun(mgr.model, mgrPrompt); if (!plan) { termLog(' ✗ Keskeytyi', '#f85149'); return; } const fileList = plan.split('\n').map(l => l.trim().replace(/^[\d\.\-\*\s]+/,'').replace(/\*+/g,'').replace(/`/g,'')).map(l => { @@ -446,7 +545,7 @@ import AgentBar from "../components/AgentBar.astro"; if (!fileList.length) { termLog(' Ei tiedostojakoa — generoidaan yhtenä', '#8b949e'); - await kpnRun('qwen-coder', `Project: ${task}\n\nWrite all the code.`); + await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Project: ${task}\n\nWrite all the code.`); termLog(`\n━━━ Valmis ━━━`); return; } @@ -455,24 +554,26 @@ import AgentBar from "../components/AgentBar.astro"; const files = {}; for (let i = 0; i < fileList.length; i++) { const f = fileList[i]; - termLog(`\n[${i+2}] Koodari — ${esc(f.name)}`); + termLog(`\n[${i+2}] ${esc(cdr.name)} — ${esc(f.name)}`); let ctx = ''; const prev = Object.entries(files); if (prev.length) ctx = 'Already written:\n' + prev.map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') + '\n\n'; let extra = ''; if (f.name === 'pyproject.toml') extra = '\nUse format: [project]\\nname="proj"\\nversion="0.1.0"\\nrequires-python=">=3.11"\\ndependencies=["fastapi","uvicorn"]'; - const code = await kpnRun('qwen-coder', `${ctx}Project: ${task}\nWrite ONLY "${f.name}"${f.desc ? ': '+f.desc : ''}.${extra}\nUse exact libraries from project description.`); + const coderPrompt = (cdr.prompt ? cdr.prompt+'\n\n' : '') + `${ctx}Project: ${task}\nWrite ONLY "${f.name}"${f.desc ? ': '+f.desc : ''}.${extra}\nUse exact libraries from project description.`; + const code = await kpnRun(cdr.model, coderPrompt); if (!code) { termLog(` ✗ Keskeytyi (${f.name})`, '#f85149'); return; } files[f.name] = code; } // Review const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n'); - termLog(`\n[${fileList.length+2}] Testaaja — review`); - const review = await kpnRun('qwen-coder', `Review briefly. Say LGTM if ok.\n${allCode}`); + termLog(`\n[${fileList.length+2}] ${esc(tst.name)} — review`); + const tstPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review briefly. Say LGTM if ok.\n${allCode}`; + const review = await kpnRun(tst.model, tstPrompt); if (review && !review.toLowerCase().includes('lgtm')) { - termLog(`\n[${fileList.length+3}] Korjaukset`); - await kpnRun('qwen-coder', `Fix issues:\n${review}\n\nCode:\n${allCode}`); + termLog(`\n[${fileList.length+3}] ${esc(cdr.name)} — korjaukset`); + await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix issues:\n${review}\n\nCode:\n${allCode}`); } termLog(`\n━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━`);