|
|
|
|
@@ -695,7 +695,7 @@
|
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 style="margin-bottom:0;" data-i18n="main_title"><span style="color:#ff6b00">Kipinä</span> <span>Agentic Playground</span></h1>
|
|
|
|
|
<p class="sub" style="margin-bottom:0;"><span data-i18n="main_subtitle">Hajautettu WebGPU Laskentaverkko</span> · <span id="hub-version" style="color:#58a6ff">-</span></p>
|
|
|
|
|
<p class="sub" style="margin-bottom:0;"><span data-i18n="main_subtitle">AI-ohjelmistokehitystiimi</span> · <span id="hub-version" style="color:#58a6ff">-</span></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="lang-selector">
|
|
|
|
|
<button class="lang-btn active" onclick="setLanguage('fi')" data-lang="fi">FI</button>
|
|
|
|
|
@@ -706,14 +706,15 @@
|
|
|
|
|
|
|
|
|
|
<!-- Päävälilehdet -->
|
|
|
|
|
<div class="main-tabs">
|
|
|
|
|
<div class="main-tab active" 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" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
|
|
|
|
|
<!-- Laskentaverkko ja Koodilaboratorio piilotettu (koodi säilytetty) -->
|
|
|
|
|
<div class="main-tab" onclick="switchMainTab('network')" data-i18n="tab_network" style="display:none">Laskentaverkko</div>
|
|
|
|
|
<div class="main-tab" onclick="switchMainTab('codelab')" data-i18n="tab_codelab" style="display:none">Koodilaboratorio</div>
|
|
|
|
|
<div class="main-tab active" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
|
|
|
|
|
<div class="main-tab" onclick="switchMainTab('guide')" data-i18n="tab_guide">Opas</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- PANEELI 1: Laskentaverkko -->
|
|
|
|
|
<div id="panel-network" class="main-panel active">
|
|
|
|
|
<div id="panel-network" class="main-panel">
|
|
|
|
|
|
|
|
|
|
<!-- Global Cluster Statistics (UI) -->
|
|
|
|
|
<div class="dashboard-panel">
|
|
|
|
|
@@ -995,7 +996,7 @@
|
|
|
|
|
</div><!-- /panel-codelab -->
|
|
|
|
|
|
|
|
|
|
<!-- PANEELI 3: Agents & CLI -->
|
|
|
|
|
<div id="panel-agents" class="main-panel" style="position: relative; border-radius: 6px;">
|
|
|
|
|
<div id="panel-agents" class="main-panel active" style="position: relative; border-radius: 6px;">
|
|
|
|
|
<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">
|
|
|
|
|
@@ -1012,7 +1013,7 @@
|
|
|
|
|
<div class="org-chart">
|
|
|
|
|
<!-- Taso 1 -->
|
|
|
|
|
<div class="org-level">
|
|
|
|
|
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')">
|
|
|
|
|
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client', event)">
|
|
|
|
|
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)">
|
|
|
|
|
<div class="avatar-name">Asiakas</div>
|
|
|
|
|
<div class="avatar-role">Tuoteomistaja</div>
|
|
|
|
|
@@ -1024,13 +1025,13 @@
|
|
|
|
|
<!-- 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% + 350px); top: 0;">
|
|
|
|
|
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer', event)" style="position: absolute; right: calc(50% + 350px); 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')">
|
|
|
|
|
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager', event)">
|
|
|
|
|
<img src="/avatars/karhunpentu.png" alt="Manageri (Karhunpentu)">
|
|
|
|
|
<div class="avatar-name">Manageri</div>
|
|
|
|
|
<div class="avatar-role">KPN CLI</div>
|
|
|
|
|
@@ -1043,22 +1044,22 @@
|
|
|
|
|
|
|
|
|
|
<!-- Taso 3 -->
|
|
|
|
|
<div class="org-level" style="gap: 20px;">
|
|
|
|
|
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder')">
|
|
|
|
|
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder', event)">
|
|
|
|
|
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
|
|
|
|
|
<div class="avatar-name">Koodari</div>
|
|
|
|
|
<div class="avatar-role">SOFTAKEHITYS</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
|
|
|
|
|
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data', event)">
|
|
|
|
|
<img src="/avatars/pesukarhu_notext.png" alt="Data-Agentti (Pesukarhu)">
|
|
|
|
|
<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')">
|
|
|
|
|
<div class="avatar-card" id="avatar-qa" data-agent="qa" onclick="selectAgent('qa', event)">
|
|
|
|
|
<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')">
|
|
|
|
|
<div class="avatar-card" id="avatar-tester" data-agent="tester" onclick="selectAgent('tester', event)">
|
|
|
|
|
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
|
|
|
|
|
<div class="avatar-name">DevOps</div>
|
|
|
|
|
<div class="avatar-role">Käyttöönotto</div>
|
|
|
|
|
@@ -1082,8 +1083,8 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- RIGHT COLUMN: Puhuvat Päät Gallery -->
|
|
|
|
|
<div style="flex-basis:150px; flex-shrink:0;">
|
|
|
|
|
<!-- RIGHT COLUMN: Puhuvat Päät Gallery (piilotettu, koodi säilytetty) -->
|
|
|
|
|
<div style="flex-basis:150px; flex-shrink:0; display:none;">
|
|
|
|
|
<div style="background:rgba(1, 4, 9, 0.6); border:1px solid var(--border-color); border-radius:6px; padding:12px; height: 100%;">
|
|
|
|
|
<div id="all-heads-gallery" style="display:flex; flex-wrap:wrap; gap:10px; justify-content:center;">
|
|
|
|
|
<div class="gallery-head-wrap" id="wrap-client"><img src="/avatars/kettu_notext.png" id="gallery-client" class="gallery-head" alt="Asiakas"></div>
|
|
|
|
|
@@ -1255,23 +1256,44 @@
|
|
|
|
|
const lastPromptText = document.getElementById('agent-last-prompt-text');
|
|
|
|
|
if (selectedAgents.size === 1) {
|
|
|
|
|
const agent = [...selectedAgents][0];
|
|
|
|
|
const lastStep = [...pipelineSteps].reverse().find(s => s.agent === agent && s.status === 'done' && s.input);
|
|
|
|
|
lastPromptDiv.style.display = lastStep ? 'block' : 'none';
|
|
|
|
|
// Näytetään aina jos agentilla on oletusprompt tai pipeline-historia
|
|
|
|
|
const hasPrompt = defaultPipelinePrompts[agent] || pipelineSteps.some(s => s.agent === agent);
|
|
|
|
|
lastPromptDiv.style.display = hasPrompt ? 'block' : 'none';
|
|
|
|
|
} else {
|
|
|
|
|
lastPromptDiv.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.selectAgent = function(agent) {
|
|
|
|
|
const card = document.querySelector(`[data-agent="${agent}"]`);
|
|
|
|
|
window.selectAgent = function(agent, e) {
|
|
|
|
|
const isShift = e && e.shiftKey;
|
|
|
|
|
|
|
|
|
|
if (selectedAgents.has(agent)) {
|
|
|
|
|
selectedAgents.delete(agent);
|
|
|
|
|
card.classList.remove('selected');
|
|
|
|
|
card.classList.remove('active');
|
|
|
|
|
if (isShift) {
|
|
|
|
|
// Shift+klikkaus: lisää/poista multi-selectistä
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
selectedAgents.add(agent);
|
|
|
|
|
card.classList.add('selected');
|
|
|
|
|
// Normaali klikkaus: valitse yksi (poista muut)
|
|
|
|
|
const wasSelected = selectedAgents.has(agent) && selectedAgents.size === 1;
|
|
|
|
|
// Poistetaan kaikki valinnat
|
|
|
|
|
document.querySelectorAll('.avatar-card').forEach(c => {
|
|
|
|
|
c.classList.remove('selected');
|
|
|
|
|
c.classList.remove('active');
|
|
|
|
|
});
|
|
|
|
|
selectedAgents.clear();
|
|
|
|
|
|
|
|
|
|
if (!wasSelected) {
|
|
|
|
|
// Valitaan klikattu
|
|
|
|
|
selectedAgents.add(agent);
|
|
|
|
|
const card = document.querySelector(`[data-agent="${agent}"]`);
|
|
|
|
|
card?.classList.add('selected');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updatePromptEditor();
|
|
|
|
|
@@ -1376,6 +1398,21 @@
|
|
|
|
|
window.openPromptModal = openPromptModal;
|
|
|
|
|
|
|
|
|
|
function closePromptModal() {
|
|
|
|
|
// Tallennetaan muokattu prompti localStorageen
|
|
|
|
|
if (modalAgent) {
|
|
|
|
|
const fields = document.getElementById('prompt-modal-fields');
|
|
|
|
|
const textareas = fields.querySelectorAll('textarea');
|
|
|
|
|
const parts = [];
|
|
|
|
|
textareas.forEach((ta, i) => {
|
|
|
|
|
const key = modalPromptParts[i]?.key || '';
|
|
|
|
|
parts.push(`${key}: ${ta.value.trim()}`);
|
|
|
|
|
});
|
|
|
|
|
const assembled = parts.join('\n\n');
|
|
|
|
|
if (defaultPipelinePrompts[modalAgent]) {
|
|
|
|
|
defaultPipelinePrompts[modalAgent].prompt = assembled;
|
|
|
|
|
localStorage.setItem('kpn-pipeline-prompt-' + modalAgent, assembled);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.getElementById('prompt-modal').style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
window.closePromptModal = closePromptModal;
|
|
|
|
|
@@ -1399,11 +1436,93 @@
|
|
|
|
|
window.rerunFromModal = rerunFromModal;
|
|
|
|
|
|
|
|
|
|
// "Näytä prompti" -nappi avaa modalin
|
|
|
|
|
// Oletuspromptit jokaiselle agentille — näkyvät aina, muokattavissa
|
|
|
|
|
const defaultPipelinePrompts = {
|
|
|
|
|
manager: { label: 'Suunnittelu', prompt: `List the source files needed for this project. One file per line, format:
|
|
|
|
|
filename.py: one-line description
|
|
|
|
|
|
|
|
|
|
CONSTRAINTS: the coder can only generate ~400 tokens per file
|
|
|
|
|
- Max 3 files (keep it minimal)
|
|
|
|
|
- Each file must be SHORT: one clear responsibility, no boilerplate
|
|
|
|
|
- Only .py and pyproject.toml files
|
|
|
|
|
|
|
|
|
|
EXAMPLE: for "FastAPI todo app with SQLite"
|
|
|
|
|
pyproject.toml: project metadata and dependencies
|
|
|
|
|
models.py: SQLAlchemy models and database setup
|
|
|
|
|
main.py: FastAPI app with CRUD endpoints
|
|
|
|
|
|
|
|
|
|
Project: (käyttäjän kuvaus)` },
|
|
|
|
|
coder: { label: 'Koodaus', prompt: `Project: (managerin suunnitelma)
|
|
|
|
|
Write ONLY the file "filename.py": description
|
|
|
|
|
|
|
|
|
|
EXAMPLE: output for a main.py
|
|
|
|
|
from fastapi import FastAPI, Depends
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
from models import get_db, User
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
|
|
|
|
@app.get("/users")
|
|
|
|
|
def list_users(db: Session = Depends(get_db)):
|
|
|
|
|
return db.query(User).all()
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Keep the code SHORT. Max ~50 lines. No comments, no docstrings. Output ONLY code.` },
|
|
|
|
|
qa: { label: 'Testit + Validointi', prompt: `Write test_app.py using pytest and FastAPI TestClient. Max 3 tests. Output ONLY code.
|
|
|
|
|
|
|
|
|
|
EXAMPLE:
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
from main import app
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
|
|
|
|
def test_create():
|
|
|
|
|
r = client.post("/users", params={"name": "Test"})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Check consistency
|
|
|
|
|
1. Dockerfile COPY: references files that exist
|
|
|
|
|
2. Dockerfile deps vs imports: all imports covered
|
|
|
|
|
3. docker-compose ports: match EXPOSE
|
|
|
|
|
4. Test imports: match actual module names
|
|
|
|
|
5. pyproject.toml deps: cover all imports` },
|
|
|
|
|
tester: { label: 'DevOps', prompt: `docker-compose.yml: services, volumes, port mappings
|
|
|
|
|
|
|
|
|
|
EXAMPLE:
|
|
|
|
|
services:
|
|
|
|
|
app:
|
|
|
|
|
build: .
|
|
|
|
|
ports:
|
|
|
|
|
- "8000:8000"
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
README.md: Quick start, development, API endpoints, testing
|
|
|
|
|
IMPORTANT: Use uv for package management (uv sync, uv run)` },
|
|
|
|
|
data: { label: 'Data', prompt: `SQLAlchemy models and database setup.
|
|
|
|
|
|
|
|
|
|
EXAMPLE:
|
|
|
|
|
from sqlalchemy import create_engine, Column, Integer, String
|
|
|
|
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
|
|
|
|
|
|
|
|
engine = create_engine("sqlite:///app.db")
|
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Ladataan muokatut promptit localStoragesta
|
|
|
|
|
for (const [agent, def] of Object.entries(defaultPipelinePrompts)) {
|
|
|
|
|
const saved = localStorage.getItem('kpn-pipeline-prompt-' + agent);
|
|
|
|
|
if (saved) def.prompt = saved;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('agent-open-modal-btn')?.addEventListener('click', () => {
|
|
|
|
|
if (selectedAgents.size !== 1) return;
|
|
|
|
|
const agent = [...selectedAgents][0];
|
|
|
|
|
// Viimeisin pipeline-prompti tai oletusprompt
|
|
|
|
|
const lastStep = [...pipelineSteps].reverse().find(s => s.agent === agent && s.status === 'done' && s.input);
|
|
|
|
|
if (lastStep) openPromptModal(agent, lastStep.label, lastStep.input);
|
|
|
|
|
const def = defaultPipelinePrompts[agent];
|
|
|
|
|
const prompt = lastStep?.input || def?.prompt || agentPrompts[agent]?.prompt || '';
|
|
|
|
|
const label = lastStep?.label || def?.label || 'Prompti';
|
|
|
|
|
openPromptModal(agent, label, prompt);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function checkAgentConfusion() {
|
|
|
|
|
@@ -2034,11 +2153,18 @@
|
|
|
|
|
const iconColor = s.status === 'done' ? '#3fb950' : s.status === 'active' ? '#d29922' : '#8b949e';
|
|
|
|
|
const arrow = i < pipelineSteps.length - 1 ? ' <span style="color:#30363d">→</span> ' : '';
|
|
|
|
|
// Tooltip: input/output esikatselu
|
|
|
|
|
const tip = esc(`${s.label}\nInput: ${(s.input || '').substring(0, 150)}\nOutput: ${(s.output || '').substring(0, 150)}`).replace(/\n/g, ' ');
|
|
|
|
|
return `<span title="${tip}" style="cursor:help"><span style="color:${iconColor}">${icon}</span> <span style="color:${color}">${esc(s.label)}</span></span>${arrow}`;
|
|
|
|
|
return `<span onclick="openPipelineStepModal(${i})" style="cursor:pointer;padding:2px 4px;border-radius:3px;transition:background 0.2s" onmouseenter="this.style.background='#21262d'" onmouseleave="this.style.background='transparent'"><span style="color:${iconColor}">${icon}</span> <span style="color:${color}">${esc(s.label)}</span></span>${arrow}`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.openPipelineStepModal = function(idx) {
|
|
|
|
|
const s = pipelineSteps[idx];
|
|
|
|
|
if (!s) return;
|
|
|
|
|
// Näytetään modal promptilla (input) ja tuloksella (output)
|
|
|
|
|
const combined = (s.input ? s.input : '') + (s.output ? '\n\n--- Tulos ---\n' + s.output : '');
|
|
|
|
|
openPromptModal(s.agent, s.label, combined);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function pipelineClear() {
|
|
|
|
|
pipelineSteps.length = 0;
|
|
|
|
|
const container = document.getElementById('pipeline-steps');
|
|
|
|
|
@@ -3927,7 +4053,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
const translations = {
|
|
|
|
|
fi: {
|
|
|
|
|
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
|
|
|
|
|
main_subtitle: "Hajautettu WebGPU Laskentaverkko",
|
|
|
|
|
main_subtitle: "AI-ohjelmistokehitystiimi",
|
|
|
|
|
tab_network: "Laskentaverkko",
|
|
|
|
|
tab_codelab: "Koodilaboratorio",
|
|
|
|
|
tab_agents: "Kipinä Agentic Playground",
|
|
|
|
|
@@ -3953,7 +4079,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
},
|
|
|
|
|
se: {
|
|
|
|
|
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
|
|
|
|
|
main_subtitle: "Decentraliserat WebGPU Beräkningsnätverk",
|
|
|
|
|
main_subtitle: "AI-programvaruutvecklingsteam",
|
|
|
|
|
tab_network: "Kalkylnätverk",
|
|
|
|
|
tab_codelab: "Kodlaboratorium",
|
|
|
|
|
tab_agents: "Kipinä Agentic Playground",
|
|
|
|
|
@@ -3979,7 +4105,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
},
|
|
|
|
|
en: {
|
|
|
|
|
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
|
|
|
|
|
main_subtitle: "Decentralized WebGPU Compute Network",
|
|
|
|
|
main_subtitle: "AI Software Development Team",
|
|
|
|
|
tab_network: "Compute Network",
|
|
|
|
|
tab_codelab: "Code Laboratory",
|
|
|
|
|
tab_agents: "Kipinä Agentic Playground",
|
|
|
|
|
|