Agenttien konfigurointi: promptit, mallit, järjestys, omat agentit
Klikatessa avataria avautuu konfigurointipaneeli: - Nimen muokkaus - Mallinvalinta (0.5B/1.5B/3B/7B) - System promptin muokkaus - Pipeline-järjestys (drag & drop avatarit ja pipeline-tagit) - Agentin poisto "+" -nappi luo uuden agentin satunnaisella avatarilla. Konfiguraatio tallennetaan localStorageen (kpn-agents). Pipeline (kpn project) käyttää agenttien prompteja ja malleja. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,52 @@
|
||||
<!-- Agenttigalleria: roolit + avatarit -->
|
||||
<div id="agent-bar" style="display:flex;gap:8px;padding:10px 0;overflow-x:auto;align-items:flex-end">
|
||||
<div class="agent-avatar" data-agent="manager" onclick="selectAgent('manager')" title="Manageri — suunnittelee ja pilkkoo tehtävät">
|
||||
<img src="/avatars/owl.png" alt="Manageri" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Manageri</span>
|
||||
<!-- Agenttigalleria + konfigurointipaneeli -->
|
||||
<div style="display:flex;gap:16px;padding:10px 0">
|
||||
<!-- Agenttilista (drag & drop) -->
|
||||
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
|
||||
<!-- Renderöidään JS:stä -->
|
||||
</div>
|
||||
<div class="agent-avatar" data-agent="coder" onclick="selectAgent('coder')" title="Koodari — kirjoittaa koodin">
|
||||
<img src="/avatars/chameleon.png" alt="Koodari" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Koodari</span>
|
||||
</div>
|
||||
<div class="agent-avatar" data-agent="tester" onclick="selectAgent('tester')" title="Testaaja — arvioi ja etsii bugeja">
|
||||
<img src="/avatars/mantis.png" alt="Testaaja" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Testaaja</span>
|
||||
</div>
|
||||
<div class="agent-avatar" data-agent="qa" onclick="selectAgent('qa')" title="QA — laadunvarmistus ja testit">
|
||||
<img src="/avatars/tortoise.png" alt="QA" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">QA</span>
|
||||
</div>
|
||||
<div class="agent-avatar" data-agent="data" onclick="selectAgent('data')" title="Data — tietokanta-asiantuntija">
|
||||
<img src="/avatars/elephant.png" alt="Data" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Data</span>
|
||||
<!-- + Lisää agentti -->
|
||||
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
|
||||
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
|
||||
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Lisää</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
|
||||
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
|
||||
<div>
|
||||
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agentin nimi">
|
||||
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
|
||||
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Malli -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</label>
|
||||
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
|
||||
<option value="qwen-coder">Qwen2.5-Coder:0.5B (selain)</option>
|
||||
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
|
||||
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- System prompt -->
|
||||
<div style="margin-bottom:10px">
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">System prompt</label>
|
||||
<textarea id="config-prompt" rows="4" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:inherit;resize:vertical" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline-järjestys -->
|
||||
<div>
|
||||
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
|
||||
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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]) =>
|
||||
`<div class="agent-avatar${selectedAgent===key?' active':''}" data-agent="${key}" onclick="selectAgent('${key}')" draggable="true" ondragstart="dragAgent(event,'${key}')" ondragover="event.preventDefault()" ondrop="dropAgent(event,'${key}')" title="${esc(a.name)} — ${esc(a.prompt?.substring(0,60))}...">` +
|
||||
`<img src="${a.avatar}" alt="${esc(a.name)}" style="width:48px;height:48px;border-radius:50%;border:2px solid transparent">` +
|
||||
`<span style="font-size:10px;color:#8b949e;text-align:center;display:block">${esc(a.name)}</span></div>`
|
||||
).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]) =>
|
||||
`<span style="padding:3px 8px;border-radius:4px;font-size:11px;border:1px solid ${k===key?'var(--accent)':'var(--border)'};color:${k===key?'var(--accent)':'#8b949e'};cursor:grab" draggable="true" ondragstart="dragAgent(event,'${k}')" ondragover="event.preventDefault()" ondrop="dropAgent(event,'${k}')">${ag.name}</span>`
|
||||
).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(`<span class="terminal-prompt">$</span> ${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(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti käynnistyy ━━━</span>`);
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — 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<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — 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<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
|
||||
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<span style="color:#3fb950;font-weight:bold">[${i+2}] Koodari</span> — ${esc(f.name)}`);
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}] ${esc(cdr.name)}</span> — ${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<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] Testaaja</span> — review`);
|
||||
const review = await kpnRun('qwen-coder', `Review briefly. Say LGTM if ok.\n${allCode}`);
|
||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] ${esc(tst.name)}</span> — 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<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] Korjaukset</span>`);
|
||||
await kpnRun('qwen-coder', `Fix issues:\n${review}\n\nCode:\n${allCode}`);
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] ${esc(cdr.name)}</span> — korjaukset`);
|
||||
await kpnRun(cdr.model, `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix issues:\n${review}\n\nCode:\n${allCode}`);
|
||||
}
|
||||
|
||||
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`);
|
||||
|
||||
Reference in New Issue
Block a user