Agent Builder UI: #builder -tabi, lomake, CRUD-integraatio

- Uusi välilehti: Agent Builder (navigointi #builder)
- Agenttilista: ladataan /api/v1/agents, näytetään kortteina
- Lomake: avatar-valitsin, rooli-template, malli, väri, docs, prompt, parametrit
- Tallenna → POST /api/v1/agents, Poista → DELETE /api/v1/agents/:id
- Avatar-grid: 8 valmista hahmoa valittavissa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 10:45:31 +03:00
parent 21aac49a52
commit 24a8139d3e

View File

@@ -724,6 +724,7 @@
<div class="main-tab" onclick="switchMainTab('network')" data-i18n="tab_network">Laskentaverkko</div>
<div class="main-tab" onclick="switchMainTab('codelab')" data-i18n="tab_codelab">Koodilaboratorio</div>
<div class="main-tab active" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
<div class="main-tab" onclick="switchMainTab('builder')" data-i18n="tab_builder">Agent Builder</div>
<div class="main-tab" onclick="switchMainTab('guide')" data-i18n="tab_guide">Opas</div>
</div>
@@ -1131,6 +1132,70 @@
</div><!-- /panel-agents -->
<!-- PANEELI 4: Opas -->
<div id="panel-builder" class="main-panel">
<div style="max-width:800px;margin:0 auto;padding:20px">
<h2 style="color:#f0f6fc;margin-bottom:16px">Agent Builder</h2>
<p style="color:#8b949e;margin-bottom:20px">Luo ja muokkaa agentteja. Agentit tallentuvat palvelimelle ja ovat kaikkien käytettävissä.</p>
<!-- Agenttilista -->
<div id="builder-agent-list" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:24px"></div>
<!-- Lomake -->
<div id="builder-form" style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:20px;display:none">
<div style="display:grid;grid-template-columns:auto 1fr;gap:16px;align-items:start">
<!-- Avatar-valitsin -->
<div>
<img id="builder-avatar-preview" src="/avatars/kipina_notext.png" style="width:80px;height:80px;border-radius:16px;border:2px solid #30363d;cursor:pointer;object-fit:cover" onclick="document.getElementById('builder-avatar-select').style.display=document.getElementById('builder-avatar-select').style.display==='none'?'flex':'none'">
<div id="builder-avatar-select" style="display:none;flex-wrap:wrap;gap:6px;margin-top:8px;max-width:200px"></div>
</div>
<!-- Kentät -->
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;gap:10px">
<input id="builder-id" placeholder="tunniste (esim. tofuist)" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:6px 10px;color:#c9d1d9;font-size:13px">
<input id="builder-name" placeholder="Näyttönimi" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:6px 10px;color:#c9d1d9;font-size:13px">
</div>
<div style="display:flex;gap:10px">
<select id="builder-role" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:6px 10px;color:#c9d1d9;font-size:13px">
<option value="coder">Koodari</option>
<option value="qa">QA / Testaus</option>
<option value="devops">DevOps</option>
<option value="devsecops">DevSecOps</option>
<option value="architect">Arkkitehti</option>
<option value="iac">IaC / Infra</option>
<option value="data">Data</option>
<option value="manager">Manageri</option>
<option value="writer">Kirjoittaja</option>
<option value="custom">Vapaa</option>
</select>
<input id="builder-model" placeholder="Malli (esim. qwen2.5-coder:7b)" value="qwen2.5-coder:7b" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:6px 10px;color:#c9d1d9;font-size:13px">
</div>
<div style="display:flex;gap:10px">
<input id="builder-color" type="color" value="#3fb950" style="width:40px;height:32px;border:1px solid #30363d;border-radius:4px;background:#0d1117;cursor:pointer">
<input id="builder-docs" placeholder="Docs URL (valinnainen, esim. /docs/tofu-cheatsheet.md)" style="flex:1;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:6px 10px;color:#c9d1d9;font-size:13px">
</div>
<textarea id="builder-prompt" rows="4" placeholder="System prompt..." style="background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:8px 10px;color:#c9d1d9;font-size:13px;resize:vertical;font-family:inherit"></textarea>
<div style="display:flex;gap:10px;align-items:center">
<label style="color:#8b949e;font-size:12px">Temp:</label>
<input id="builder-temp" type="number" value="0.7" min="0" max="2" step="0.1" style="width:60px;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:4px 8px;color:#c9d1d9;font-size:12px">
<label style="color:#8b949e;font-size:12px">Top-k:</label>
<input id="builder-topk" type="number" value="40" min="1" max="200" style="width:60px;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:4px 8px;color:#c9d1d9;font-size:12px">
<label style="color:#8b949e;font-size:12px">Max tok:</label>
<input id="builder-maxtokens" type="number" value="512" min="32" max="4096" step="32" style="width:70px;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:4px 8px;color:#c9d1d9;font-size:12px">
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button onclick="builderSave()" style="background:#238636;border:1px solid #2ea043;color:#fff;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:13px">Tallenna</button>
<button onclick="builderDelete()" id="builder-delete-btn" style="background:#21262d;border:1px solid #f85149;color:#f85149;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:13px;display:none">Poista</button>
<button onclick="builderCancel()" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 16px;border-radius:4px;cursor:pointer;font-size:13px">Peruuta</button>
</div>
</div>
</div>
</div>
<!-- Uusi agentti -nappi -->
<button onclick="builderNew()" id="builder-new-btn" style="margin-top:12px;background:#238636;border:1px solid #2ea043;color:#fff;padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px">+ Uusi agentti</button>
</div>
</div>
<div id="panel-guide" class="main-panel">
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:var(--text-color);line-height:1.7;font-size:15px">
<p style="color:#8b949e">Ladataan opasta...</p>
@@ -1723,7 +1788,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
// URL-hash navigointi
const initHash = window.location.hash.replace('#', '');
const hashMap = { 'laskentaverkko': 'network', 'network': 'network', 'codelab': 'codelab', 'agents': 'agents', 'guide': 'guide' };
const hashMap = { 'laskentaverkko': 'network', 'network': 'network', 'codelab': 'codelab', 'agents': 'agents', 'builder': 'builder', 'guide': 'guide' };
if (hashMap[initHash]) {
switchMainTab(hashMap[initHash]);
}
@@ -4521,6 +4586,142 @@ ${filesHtml}
}, 100);
});
// ── Agent Builder ──
const BUILDER_AVATARS = [
'/avatars/kipina_notext.png', '/avatars/karhunpentu.png', '/avatars/kettu_notext.png',
'/avatars/pesukarhu_notext.png', '/avatars/susi_notext.png', '/avatars/laiskiainen_notext.png',
'/avatars/aikuinen_susi.png', '/avatars/gecko_notext.png'
];
let builderAgents = [];
let builderEditing = null; // null = uusi, string = id
async function builderLoad() {
try {
const res = await fetch('/api/v1/agents');
if (res.ok) builderAgents = await res.json();
} catch(e) {}
builderRenderList();
}
function builderRenderList() {
const list = document.getElementById('builder-agent-list');
if (!list) return;
list.innerHTML = builderAgents.map(a => `
<div onclick="builderEdit('${a.id}')" style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:12px;cursor:pointer;transition:border-color 0.2s;display:flex;align-items:center;gap:10px" onmouseover="this.style.borderColor='${a.color}'" onmouseout="this.style.borderColor='#30363d'">
<img src="${a.avatar}" style="width:40px;height:40px;border-radius:10px;border:2px solid ${a.color};object-fit:cover">
<div>
<div style="font-weight:600;color:${a.color};font-size:14px">${esc(a.name)}</div>
<div style="color:#8b949e;font-size:11px">${esc(a.model)} · ${a.role}</div>
</div>
</div>
`).join('');
}
function builderNew() {
builderEditing = null;
document.getElementById('builder-form').style.display = 'block';
document.getElementById('builder-new-btn').style.display = 'none';
document.getElementById('builder-delete-btn').style.display = 'none';
document.getElementById('builder-id').value = '';
document.getElementById('builder-id').disabled = false;
document.getElementById('builder-name').value = '';
document.getElementById('builder-role').value = 'coder';
document.getElementById('builder-model').value = 'qwen2.5-coder:7b';
document.getElementById('builder-color').value = '#3fb950';
document.getElementById('builder-docs').value = '';
document.getElementById('builder-prompt').value = '';
document.getElementById('builder-temp').value = '0.7';
document.getElementById('builder-topk').value = '40';
document.getElementById('builder-maxtokens').value = '512';
document.getElementById('builder-avatar-preview').src = '/avatars/kipina_notext.png';
builderRenderAvatarSelect();
}
function builderEdit(id) {
const a = builderAgents.find(x => x.id === id);
if (!a) return;
builderEditing = id;
document.getElementById('builder-form').style.display = 'block';
document.getElementById('builder-new-btn').style.display = 'none';
document.getElementById('builder-delete-btn').style.display = a.is_default ? 'none' : 'inline-block';
document.getElementById('builder-id').value = a.id;
document.getElementById('builder-id').disabled = true;
document.getElementById('builder-name').value = a.name;
document.getElementById('builder-role').value = a.role;
document.getElementById('builder-model').value = a.model;
document.getElementById('builder-color').value = a.color;
document.getElementById('builder-docs').value = a.docs || '';
document.getElementById('builder-prompt').value = a.prompt;
document.getElementById('builder-temp').value = a.temperature;
document.getElementById('builder-topk').value = a.top_k;
document.getElementById('builder-maxtokens').value = a.max_tokens;
document.getElementById('builder-avatar-preview').src = a.avatar;
builderRenderAvatarSelect();
}
function builderRenderAvatarSelect() {
const container = document.getElementById('builder-avatar-select');
container.innerHTML = BUILDER_AVATARS.map(src =>
`<img src="${src}" style="width:36px;height:36px;border-radius:8px;border:2px solid #30363d;cursor:pointer;object-fit:cover" onclick="document.getElementById('builder-avatar-preview').src='${src}';document.getElementById('builder-avatar-select').style.display='none'">`
).join('');
}
async function builderSave() {
const payload = {
id: document.getElementById('builder-id').value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, ''),
name: document.getElementById('builder-name').value.trim(),
avatar: document.getElementById('builder-avatar-preview').src.replace(location.origin, ''),
role: document.getElementById('builder-role').value,
model: document.getElementById('builder-model').value.trim(),
color: document.getElementById('builder-color').value,
docs: document.getElementById('builder-docs').value.trim() || null,
prompt: document.getElementById('builder-prompt').value,
temperature: parseFloat(document.getElementById('builder-temp').value) || 0.7,
top_k: parseInt(document.getElementById('builder-topk').value) || 40,
max_tokens: parseInt(document.getElementById('builder-maxtokens').value) || 512,
repetition_penalty: 1.15,
};
if (!payload.id || !payload.name) { alert('Tunniste ja nimi vaaditaan'); return; }
try {
const res = await fetch('/api/v1/agents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
builderCancel();
await builderLoad();
} else {
alert('Virhe: ' + await res.text());
}
} catch(e) { alert('Virhe: ' + e.message); }
}
async function builderDelete() {
if (!builderEditing) return;
if (!confirm('Poistetaanko agentti "' + builderEditing + '"?')) return;
try {
const res = await fetch('/api/v1/agents/' + builderEditing, { method: 'DELETE' });
if (res.ok) {
builderCancel();
await builderLoad();
} else {
alert('Virhe: ' + await res.text());
}
} catch(e) { alert('Virhe: ' + e.message); }
}
function builderCancel() {
document.getElementById('builder-form').style.display = 'none';
document.getElementById('builder-new-btn').style.display = 'inline-block';
builderEditing = null;
}
// Ladataan agentit kun builder-tabi avataan
builderLoad();
// GUIDE.md:n lataus ja renderöinti
(async function loadGuide() {
const container = document.getElementById('guide-content');