|
|
|
|
@@ -388,7 +388,7 @@
|
|
|
|
|
font-family: 'Courier New', Courier, monospace;
|
|
|
|
|
font-size:14px;
|
|
|
|
|
color:var(--success-color);
|
|
|
|
|
height:500px;
|
|
|
|
|
height: clamp(200px, 35vh, 500px);
|
|
|
|
|
overflow-y:auto;
|
|
|
|
|
text-align:left;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
@@ -397,56 +397,58 @@
|
|
|
|
|
.terminal-prompt { color: #d29922; }
|
|
|
|
|
.org-chart {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
perspective: 1000px;
|
|
|
|
|
padding: 25px 50px;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
.org-level {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 40px;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.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);
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 2px;
|
|
|
|
|
background: linear-gradient(to right, rgba(88, 166, 255, 0.8), rgba(88, 166, 255, 0.2));
|
|
|
|
|
align-self: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.org-branch {
|
|
|
|
|
width: 510px;
|
|
|
|
|
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);
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
.org-chart.vertical {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0;
|
|
|
|
|
}
|
|
|
|
|
.org-chart.vertical .org-connector {
|
|
|
|
|
width: 2px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
background: linear-gradient(to bottom, rgba(88, 166, 255, 0.8), rgba(88, 166, 255, 0.2));
|
|
|
|
|
}
|
|
|
|
|
.avatar-card {
|
|
|
|
|
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;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 6px 6px 4px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
width: 130px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
width: 72px;
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
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);
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
|
|
|
}
|
|
|
|
|
.avatar-card:hover {
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
transform: translateY(-4px) scale(1.02);
|
|
|
|
|
transform: translateY(-2px) scale(1.02);
|
|
|
|
|
border-color: rgba(240, 246, 252, 0.3);
|
|
|
|
|
box-shadow: 0 12px 20px rgba(0,0,0,0.4);
|
|
|
|
|
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
|
|
|
|
|
}
|
|
|
|
|
@keyframes idle-breathe {
|
|
|
|
|
0%, 100% { transform: translateY(0) scale(1); }
|
|
|
|
|
@@ -461,10 +463,10 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-card img {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
width: 50px;
|
|
|
|
|
height: 50px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
border: 2px solid rgba(240, 246, 252, 0.1);
|
|
|
|
|
transition: all 0.4s ease;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
@@ -608,7 +610,7 @@
|
|
|
|
|
background: #0d1117;
|
|
|
|
|
border: 1px solid var(--accent-color);
|
|
|
|
|
}
|
|
|
|
|
.avatar-name { font-weight: 700; font-size: 13px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; }
|
|
|
|
|
.avatar-name { font-weight: 700; font-size: 10px; color: #f0f6fc; letter-spacing: 0.3px; margin-bottom: 0; }
|
|
|
|
|
.avatar-role { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; line-height: 1.2; word-wrap: break-word; }
|
|
|
|
|
.agent-prompt-editor {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
@@ -629,7 +631,7 @@
|
|
|
|
|
font-family: 'Courier New', monospace;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
.agent-prompt-editor textarea:focus { border-color: var(--accent-color); }
|
|
|
|
|
@@ -669,17 +671,14 @@
|
|
|
|
|
#metrics-grid { grid-template-columns: 1fr 1fr !important; }
|
|
|
|
|
|
|
|
|
|
/* Org chart mobile tweaks */
|
|
|
|
|
.org-chart { padding: 20px 10px; }
|
|
|
|
|
.org-branch { display: none; }
|
|
|
|
|
.org-connector { margin-bottom: 10px; height: 20px; }
|
|
|
|
|
.org-level { flex-wrap: wrap; justify-content: center; gap: 15px !important; }
|
|
|
|
|
#avatar-observer { display: block; position: relative !important; right: auto !important; top: auto !important; margin: 0 auto; margin-bottom: 15px; }
|
|
|
|
|
.org-chart { padding: 6px; gap: 4px; }
|
|
|
|
|
.org-level { flex-wrap: wrap; gap: 4px !important; }
|
|
|
|
|
.org-connector { width: 12px; }
|
|
|
|
|
|
|
|
|
|
/* Avatar cards downscaling */
|
|
|
|
|
.avatar-card { width: 100px; padding: 8px 4px; }
|
|
|
|
|
.avatar-card img { width: 55px; height: 55px; margin-bottom: 4px; border-radius: 12px; }
|
|
|
|
|
.avatar-name { font-size: 11px; margin-bottom: 1px; }
|
|
|
|
|
.avatar-role { font-size: 8px; line-height: 1.1; }
|
|
|
|
|
.avatar-card { width: 56px; padding: 4px 3px 2px; }
|
|
|
|
|
.avatar-card img { width: 36px; height: 36px; margin-bottom: 2px; border-radius: 8px; }
|
|
|
|
|
.avatar-name { font-size: 9px; margin-bottom: 0; }
|
|
|
|
|
|
|
|
|
|
/* User Input Area */
|
|
|
|
|
#user-input-box > div { flex-direction: column; }
|
|
|
|
|
@@ -688,11 +687,26 @@
|
|
|
|
|
#code-input-container { flex-direction: column !important; }
|
|
|
|
|
#code-send-btn { width: 100%; margin-top: 5px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsiivinen korkeus */
|
|
|
|
|
@media (max-height: 900px) {
|
|
|
|
|
.terminal-panel { height: clamp(150px, 25vh, 250px); }
|
|
|
|
|
.agent-prompt-editor textarea { min-height: 30px; }
|
|
|
|
|
.container > div:first-child { margin-bottom: 6px; }
|
|
|
|
|
.container h1 { font-size: 22px; }
|
|
|
|
|
.container .sub { font-size: 12px; }
|
|
|
|
|
.avatar-card { padding: 4px 4px 2px; }
|
|
|
|
|
.avatar-card img { width: 40px; height: 40px; margin-bottom: 2px; }
|
|
|
|
|
}
|
|
|
|
|
@media (min-height: 1200px) {
|
|
|
|
|
.terminal-panel { height: clamp(350px, 40vh, 600px); }
|
|
|
|
|
.agent-prompt-editor textarea { min-height: 80px; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
|
|
|
|
|
<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">AI-ohjelmistokehitystiimi</span> · <span id="hub-version" style="color:#58a6ff">-</span></p>
|
|
|
|
|
@@ -707,8 +721,8 @@
|
|
|
|
|
<!-- Päävälilehdet -->
|
|
|
|
|
<div class="main-tabs">
|
|
|
|
|
<!-- 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" 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('guide')" data-i18n="tab_guide">Opas</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -1003,6 +1017,7 @@
|
|
|
|
|
<div style="display:flex;align-items:center;gap:16px;">
|
|
|
|
|
<span style="font-weight:600;font-size:15px;color:var(--text-color)"><span style="color:#ff6b00">Kipinä</span> 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>
|
|
|
|
|
<button id="btn-toggle-layout" onclick="toggleOrgLayout()" 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;">⇅ Pysty</button>
|
|
|
|
|
</div>
|
|
|
|
|
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -1011,64 +1026,46 @@
|
|
|
|
|
<!-- LEFT COLUMN: Org chart & Prompt Editor -->
|
|
|
|
|
<div style="flex:1; min-width:300px; overflow-x:auto;">
|
|
|
|
|
<div class="org-chart">
|
|
|
|
|
<!-- Taso 1 -->
|
|
|
|
|
<div class="org-level">
|
|
|
|
|
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client', event)">
|
|
|
|
|
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)">
|
|
|
|
|
<img src="/avatars/kettu_notext.png" alt="Asiakas">
|
|
|
|
|
<div class="avatar-name">Asiakas</div>
|
|
|
|
|
<div class="avatar-role">Tuoteomistaja</div>
|
|
|
|
|
</div>
|
|
|
|
|
</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', 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="org-level">
|
|
|
|
|
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager', event)">
|
|
|
|
|
<img src="/avatars/karhunpentu.png" alt="Manageri (Karhunpentu)">
|
|
|
|
|
<img src="/avatars/karhunpentu.png" alt="Manageri">
|
|
|
|
|
<div class="avatar-name">Manageri</div>
|
|
|
|
|
<div class="avatar-role">KPN CLI</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="org-connector"></div>
|
|
|
|
|
|
|
|
|
|
<div class="org-branch"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Taso 3 -->
|
|
|
|
|
<div class="org-level" style="gap: 20px;">
|
|
|
|
|
<div class="org-level">
|
|
|
|
|
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder', event)">
|
|
|
|
|
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
|
|
|
|
|
<img src="/avatars/kipina_notext.png" alt="Koodari">
|
|
|
|
|
<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', event)">
|
|
|
|
|
<img src="/avatars/pesukarhu_notext.png" alt="Data-Agentti (Pesukarhu)">
|
|
|
|
|
<img src="/avatars/pesukarhu_notext.png" alt="Data">
|
|
|
|
|
<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', event)">
|
|
|
|
|
<img src="/avatars/susi_notext.png" alt="QA (Pikkususi)">
|
|
|
|
|
<img src="/avatars/susi_notext.png" alt="QA">
|
|
|
|
|
<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', event)">
|
|
|
|
|
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
|
|
|
|
|
<img src="/avatars/laiskiainen_notext.png" alt="DevOps">
|
|
|
|
|
<div class="avatar-name">DevOps</div>
|
|
|
|
|
<div class="avatar-role">Käyttöönotto</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer', event)">
|
|
|
|
|
<img src="/avatars/aikuinen_susi.png" alt="Tarkkailija">
|
|
|
|
|
<div class="avatar-name">Tarkkailija</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Prompt Editor -->
|
|
|
|
|
<div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:20px;">
|
|
|
|
|
<div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:10px;">
|
|
|
|
|
<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>
|
|
|
|
|
@@ -1116,7 +1113,7 @@
|
|
|
|
|
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pipeline-steps" style="display:none;background:#0d1117;border:1px solid var(--border-color);border-top:none;padding:8px 14px;font-family:'Courier New',monospace;font-size:12px;overflow-x:auto;white-space:nowrap"></div>
|
|
|
|
|
<div id="pipeline-steps" style="display:none;background:#0d1117;border:1px solid var(--border-color);border-top:none;padding:8px 14px;font-family:'Courier New',monospace;font-size:12px;line-height:1.8;flex-wrap:wrap;display:flex;gap:2px"></div>
|
|
|
|
|
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
|
|
|
|
|
</div>
|
|
|
|
|
<div style="position:relative;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">
|
|
|
|
|
@@ -1317,6 +1314,16 @@
|
|
|
|
|
updatePromptEditor();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.toggleOrgLayout = function() {
|
|
|
|
|
const chart = document.querySelector('.org-chart');
|
|
|
|
|
const btn = document.getElementById('btn-toggle-layout');
|
|
|
|
|
if (chart.classList.toggle('vertical')) {
|
|
|
|
|
btn.textContent = '⇄ Vaaka';
|
|
|
|
|
} else {
|
|
|
|
|
btn.textContent = '⇅ Pysty';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Autosave prompti
|
|
|
|
|
document.getElementById('agent-prompt-text')?.addEventListener('input', (e) => {
|
|
|
|
|
if (selectedAgents.size === 0) return;
|
|
|
|
|
@@ -1444,12 +1451,14 @@ 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
|
|
|
|
|
- Only .py, .html and pyproject.toml files
|
|
|
|
|
- If the project has a UI, include one index.html served by FastAPI at /
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
main.py: FastAPI app with CRUD endpoints and serves index.html at /
|
|
|
|
|
index.html: simple HTML UI with forms and fetch() to call the API
|
|
|
|
|
|
|
|
|
|
Project: (käyttäjän kuvaus)` },
|
|
|
|
|
coder: { label: 'Koodaus', prompt: `Project: (managerin suunnitelma)
|
|
|
|
|
@@ -1483,7 +1492,7 @@ IMPORTANT: Check consistency
|
|
|
|
|
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` },
|
|
|
|
|
5. pyproject.toml deps: cover all imports (but NOT stdlib like sqlite3, os, sys, json)` },
|
|
|
|
|
tester: { label: 'DevOps', prompt: `docker-compose.yml: services, volumes, port mappings
|
|
|
|
|
|
|
|
|
|
EXAMPLE:
|
|
|
|
|
@@ -1499,11 +1508,11 @@ 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
|
|
|
|
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text
|
|
|
|
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
|
|
|
|
|
|
|
|
|
engine = create_engine("sqlite:///app.db")
|
|
|
|
|
Base = declarative_base()
|
|
|
|
|
class Base(DeclarativeBase): pass
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
};
|
|
|
|
|
@@ -2130,7 +2139,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
pipelineSteps.push(step);
|
|
|
|
|
}
|
|
|
|
|
renderPipelineSteps();
|
|
|
|
|
// Päivitetään agentin avatar tooltip
|
|
|
|
|
// Päivitetään agentin avatar tooltip + vilahdus
|
|
|
|
|
const avatarMap = { manager: 'avatar-kpn', coder: 'avatar-coder', tester: 'avatar-tester', qa: 'avatar-qa', data: 'avatar-data' };
|
|
|
|
|
const avatarId = avatarMap[agent];
|
|
|
|
|
if (avatarId) {
|
|
|
|
|
@@ -2138,6 +2147,19 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
if (el) {
|
|
|
|
|
const truncOut = (output || '').substring(0, 200).replace(/\n/g, ' ');
|
|
|
|
|
el.title = `${label}\n${status === 'active' ? '⏳ Käsittelee...' : '✓ Valmis'}\n\nInput: ${(input || '').substring(0, 100)}...\nOutput: ${truncOut}...`;
|
|
|
|
|
|
|
|
|
|
// Avatar-aktivointi: syttyy vuoron alussa, sammuu lopussa
|
|
|
|
|
if (status === 'active') {
|
|
|
|
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
|
|
|
|
el.classList.add('active');
|
|
|
|
|
document.querySelectorAll('.gallery-head-wrap').forEach(w => w.classList.remove('active'));
|
|
|
|
|
const gw = document.getElementById('wrap-' + agent);
|
|
|
|
|
if (gw) gw.classList.add('active');
|
|
|
|
|
} else if (status === 'done') {
|
|
|
|
|
el.classList.remove('active');
|
|
|
|
|
const gw = document.getElementById('wrap-' + agent);
|
|
|
|
|
if (gw) gw.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -2146,15 +2168,16 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
const container = document.getElementById('pipeline-steps');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
if (pipelineSteps.length === 0) { container.style.display = 'none'; return; }
|
|
|
|
|
container.style.display = 'block';
|
|
|
|
|
container.style.display = 'flex';
|
|
|
|
|
container.innerHTML = pipelineSteps.map((s, i) => {
|
|
|
|
|
const colors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
|
|
|
|
const color = colors[s.agent] || '#8b949e';
|
|
|
|
|
const icon = s.status === 'done' ? '✓' : s.status === 'active' ? '◷' : '◯';
|
|
|
|
|
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
|
|
|
|
|
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}`;
|
|
|
|
|
const stepDescs = { 'Suunnittelu': 'Pilkkoo tiedostoiksi', 'Review': 'Arvioi koodin laadun', 'Testit': 'Kirjoittaa testit', 'Dockerfile': 'Docker-image', 'Compose': 'Palvelumääritys', 'README': 'Dokumentaatio', 'Validointi': 'Tarkistaa yhteensopivuuden', 'Korjaukset': 'Korjaa löydetyt bugit' };
|
|
|
|
|
const desc = stepDescs[s.label] || s.label;
|
|
|
|
|
return `<span onclick="openPipelineStepModal(${i})" title="${desc}" 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('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2176,7 +2199,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
// Globaali storage projektikorttien tiedostoille (välttää JSON data-attribuuttien ongelmat)
|
|
|
|
|
const projectFiles = {};
|
|
|
|
|
|
|
|
|
|
function renderProjectCard(files, projectName) {
|
|
|
|
|
function renderProjectCard(files, projectName, reportUrl) {
|
|
|
|
|
const fileEntries = Object.entries(files);
|
|
|
|
|
if (fileEntries.length === 0) return;
|
|
|
|
|
|
|
|
|
|
@@ -2204,6 +2227,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
<span style="display:flex;gap:6px">
|
|
|
|
|
<button onclick="copyAllFiles('${cardId}')" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Kopioi kaikki tiedostot leikepöydälle">Kopioi kaikki</button>
|
|
|
|
|
<button onclick="downloadZip('${cardId}')" style="background:none;border:1px solid #30363d;color:#58a6ff;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer" title="Lataa projekti ZIP-tiedostona">Lataa ZIP</button>
|
|
|
|
|
${reportUrl ? `<a href="${reportUrl}" target="_blank" style="background:none;border:1px solid #a371f7;color:#a371f7;font-size:11px;padding:2px 8px;border-radius:3px;cursor:pointer;text-decoration:none">📄 Raportti</a>` : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;gap:2px;padding:6px 8px 0;background:#0d1117">${tabsHtml}</div>
|
|
|
|
|
@@ -2256,6 +2280,10 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
const card = document.getElementById(cardId);
|
|
|
|
|
if (!card) return;
|
|
|
|
|
const files = projectFiles[cardId];
|
|
|
|
|
if (!files || Object.keys(files).length === 0) {
|
|
|
|
|
alert('Tiedostot eivät ole enää muistissa — aja pipeline uudelleen.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CRC-32 laskenta ZIP-tiedostoille
|
|
|
|
|
function crc32(bytes) {
|
|
|
|
|
@@ -2276,7 +2304,7 @@ IMPORTANT: Include get_db() dependency for FastAPI` },
|
|
|
|
|
|
|
|
|
|
for (const [name, content] of entries) {
|
|
|
|
|
const nameBytes = new TextEncoder().encode(name);
|
|
|
|
|
const contentBytes = new TextEncoder().encode(content);
|
|
|
|
|
const contentBytes = new TextEncoder().encode(content || '');
|
|
|
|
|
const crc = crc32(contentBytes);
|
|
|
|
|
|
|
|
|
|
// Local file header
|
|
|
|
|
@@ -2344,14 +2372,16 @@ 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
|
|
|
|
|
- Only .py, .html and pyproject.toml files
|
|
|
|
|
- If the project has a UI, include one index.html served by FastAPI at /
|
|
|
|
|
- No directories, no paths, just filenames
|
|
|
|
|
- List dependencies first, then main app
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
main.py: FastAPI app with CRUD endpoints and serves index.html at /
|
|
|
|
|
index.html: simple HTML UI with forms and fetch() to call the API
|
|
|
|
|
|
|
|
|
|
Project: ${task}`;
|
|
|
|
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
|
|
|
|
|
@@ -2414,40 +2444,92 @@ Project: ${task}`;
|
|
|
|
|
name = "projectname"
|
|
|
|
|
version = "0.1.0"
|
|
|
|
|
requires-python = ">=3.11"
|
|
|
|
|
dependencies = ["fastapi", "uvicorn"]
|
|
|
|
|
dependencies = ["fastapi", "uvicorn", "sqlalchemy", "httpx", "pytest"]
|
|
|
|
|
|
|
|
|
|
[project.scripts]
|
|
|
|
|
start = "uvicorn main:app --reload"`;
|
|
|
|
|
start = "uvicorn main:app --reload"
|
|
|
|
|
|
|
|
|
|
IMPORTANT: Only list pip-installable packages. NEVER include Python stdlib modules like sqlite3, os, sys, json, typing, collections, etc.`;
|
|
|
|
|
} else if (file.name === 'requirements.txt') {
|
|
|
|
|
extraInstructions = '\nList one dependency per line. No version pins unless necessary.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coderExample = file.name.includes('main') || file.name.includes('app')
|
|
|
|
|
? `\nEXAMPLE output for a main.py:
|
|
|
|
|
from fastapi import FastAPI, Depends
|
|
|
|
|
? `\nEXAMPLE output for a main.py (CRUD + HTML UI):
|
|
|
|
|
from fastapi import FastAPI, Depends, HTTPException
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
from models import get_db, User
|
|
|
|
|
from models import get_db, Base, engine, User
|
|
|
|
|
|
|
|
|
|
Base.metadata.create_all(engine)
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
|
|
|
|
@app.get("/users")
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def index():
|
|
|
|
|
return FileResponse("index.html")
|
|
|
|
|
|
|
|
|
|
@app.get("/api/users")
|
|
|
|
|
def list_users(db: Session = Depends(get_db)):
|
|
|
|
|
return db.query(User).all()
|
|
|
|
|
|
|
|
|
|
@app.post("/users")
|
|
|
|
|
@app.post("/api/users")
|
|
|
|
|
def create_user(name: str, db: Session = Depends(get_db)):
|
|
|
|
|
user = User(name=name)
|
|
|
|
|
db.add(user)
|
|
|
|
|
db.commit()
|
|
|
|
|
return {"id": user.id, "name": user.name}`
|
|
|
|
|
return {"id": user.id, "name": user.name}
|
|
|
|
|
|
|
|
|
|
@app.put("/api/users/{user_id}")
|
|
|
|
|
def update_user(user_id: int, name: str, db: Session = Depends(get_db)):
|
|
|
|
|
user = db.query(User).get(user_id)
|
|
|
|
|
if not user: raise HTTPException(404)
|
|
|
|
|
user.name = name
|
|
|
|
|
db.commit()
|
|
|
|
|
return {"id": user.id, "name": user.name}
|
|
|
|
|
|
|
|
|
|
@app.delete("/api/users/{user_id}")
|
|
|
|
|
def delete_user(user_id: int, db: Session = Depends(get_db)):
|
|
|
|
|
user = db.query(User).get(user_id)
|
|
|
|
|
if not user: raise HTTPException(404)
|
|
|
|
|
db.delete(user)
|
|
|
|
|
db.commit()
|
|
|
|
|
return {"ok": True}`
|
|
|
|
|
: file.name.includes('index.html')
|
|
|
|
|
? `\nEXAMPLE output for index.html (simple CRUD UI):
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html><head><title>App</title></head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1>Users</h1>
|
|
|
|
|
<input id="name" placeholder="Name"><button onclick="add()">Add</button>
|
|
|
|
|
<ul id="list"></ul>
|
|
|
|
|
<script>
|
|
|
|
|
async function load() {
|
|
|
|
|
const r = await fetch('/api/users');
|
|
|
|
|
const users = await r.json();
|
|
|
|
|
document.getElementById('list').innerHTML = users.map(u =>
|
|
|
|
|
'<li>' + u.name + ' <button onclick="del('+u.id+')">x</button></li>'
|
|
|
|
|
).join('');
|
|
|
|
|
}
|
|
|
|
|
async function add() {
|
|
|
|
|
const name = document.getElementById('name').value;
|
|
|
|
|
await fetch('/api/users?name='+name, {method:'POST'});
|
|
|
|
|
document.getElementById('name').value = '';
|
|
|
|
|
load();
|
|
|
|
|
}
|
|
|
|
|
async function del(id) {
|
|
|
|
|
await fetch('/api/users/'+id, {method:'DELETE'});
|
|
|
|
|
load();
|
|
|
|
|
}
|
|
|
|
|
load();
|
|
|
|
|
<` + `/script></body></html>`
|
|
|
|
|
: file.name.includes('model')
|
|
|
|
|
? `\nEXAMPLE output for a models.py:
|
|
|
|
|
from sqlalchemy import create_engine, Column, Integer, String
|
|
|
|
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
|
|
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text
|
|
|
|
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
|
|
|
|
|
|
|
|
|
engine = create_engine("sqlite:///app.db")
|
|
|
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
|
|
|
Base = declarative_base()
|
|
|
|
|
class Base(DeclarativeBase): pass
|
|
|
|
|
|
|
|
|
|
class User(Base):
|
|
|
|
|
__tablename__ = "users"
|
|
|
|
|
@@ -2463,7 +2545,7 @@ def get_db():
|
|
|
|
|
: '';
|
|
|
|
|
const coderPrompt = `${context}Project: ${task}
|
|
|
|
|
Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}${coderExample}
|
|
|
|
|
IMPORTANT: Keep the code SHORT. Max ~50 lines. No comments, no docstrings. Write minimal, working code. Output ONLY code.`;
|
|
|
|
|
IMPORTANT: Keep the code SHORT. Max ~60 lines. No comments, no docstrings. Write minimal, working code. Output ONLY code. Serve index.html at / using FileResponse. Use /api/ prefix for JSON endpoints.`;
|
|
|
|
|
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
|
|
|
|
|
if (!code) {
|
|
|
|
|
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
|
|
|
|
|
@@ -2478,17 +2560,103 @@ IMPORTANT: Keep the code SHORT. Max ~50 lines. No comments, no docstrings. Write
|
|
|
|
|
.map(([name, code]) => `--- ${name} ---\n${code}`)
|
|
|
|
|
.join('\n\n');
|
|
|
|
|
|
|
|
|
|
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`);
|
|
|
|
|
// Staattinen analyysi ennen LLM-arviointia
|
|
|
|
|
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — staattinen analyysi + arviointi`);
|
|
|
|
|
pipelineStep('tester', 'Review', 'active', `${Object.keys(generatedFiles).length} tiedostoa`);
|
|
|
|
|
const reviewPrompt = `Review this project. List bugs or issues. Be brief.
|
|
|
|
|
If the code is correct, say "LGTM".
|
|
|
|
|
|
|
|
|
|
// Yksinkertainen staattinen tarkistus selaimessa
|
|
|
|
|
const staticIssues = [];
|
|
|
|
|
for (const [name, code] of Object.entries(generatedFiles)) {
|
|
|
|
|
if (!name.endsWith('.py')) continue;
|
|
|
|
|
const lines = (code || '').split('\n');
|
|
|
|
|
// Tarkista importit
|
|
|
|
|
const imports = lines.filter(l => l.match(/^(from|import)\s/));
|
|
|
|
|
const usedNames = code.replace(/^(from|import)\s.*/gm, '');
|
|
|
|
|
for (const imp of imports) {
|
|
|
|
|
const match = imp.match(/import\s+(\w+)|from\s+\S+\s+import\s+(.+)/);
|
|
|
|
|
if (match) {
|
|
|
|
|
const names = (match[1] || match[2]).split(',').map(n => n.trim().split(' as ').pop().trim());
|
|
|
|
|
for (const n of names) {
|
|
|
|
|
if (n && !usedNames.includes(n) && n !== '*') {
|
|
|
|
|
staticIssues.push(`${name}: käyttämätön import '${n}'`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Tarkista puuttuvat importit
|
|
|
|
|
const importText = imports.join(' ');
|
|
|
|
|
const needsImport = [
|
|
|
|
|
['FastAPI', 'FastAPI'], ['Session', 'Session'], ['Depends', 'Depends'],
|
|
|
|
|
['HTTPException', 'HTTPException'], ['TestClient', 'TestClient'],
|
|
|
|
|
['Boolean', 'Boolean'], ['Text', 'Text'], ['Float', 'Float'], ['DateTime', 'DateTime'],
|
|
|
|
|
['Column', 'Column'], ['create_engine', 'create_engine'],
|
|
|
|
|
['DeclarativeBase', 'DeclarativeBase'], ['sessionmaker', 'sessionmaker'],
|
|
|
|
|
];
|
|
|
|
|
for (const [symbol, name_] of needsImport) {
|
|
|
|
|
// Tarkista käytetäänkö symbolia koodissa (ei import-riveillä)
|
|
|
|
|
const codeWithoutImports = lines.filter(l => !l.match(/^(from|import)\s/)).join('\n');
|
|
|
|
|
if (codeWithoutImports.includes(symbol) && !importText.includes(symbol)) {
|
|
|
|
|
staticIssues.push(`${name}: käyttää '${symbol}' mutta ei importtaa sitä`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Tarkista tyhjät funktiot
|
|
|
|
|
const emptyFuncs = code.match(/def \w+\([^)]*\):\s*\n\s*(pass|\.\.\.)/g);
|
|
|
|
|
if (emptyFuncs) staticIssues.push(`${name}: ${emptyFuncs.length} tyhjää funktiota`);
|
|
|
|
|
// Tarkista one-liner koodi (kaikki yhdellä rivillä)
|
|
|
|
|
if (lines.length <= 2 && code.length > 200) {
|
|
|
|
|
staticIssues.push(`${name}: koodi on yhdellä rivillä (${code.length} merkkiä) — generoi uudelleen`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tarkista tiedostojen väliset importit (from db import get_db → onko get_db db.py:ssä?)
|
|
|
|
|
for (const imp of imports) {
|
|
|
|
|
const crossMatch = imp.match(/from\s+(\w+)\s+import\s+(.+)/);
|
|
|
|
|
if (crossMatch) {
|
|
|
|
|
const modName = crossMatch[1];
|
|
|
|
|
const importedNames = crossMatch[2].split(',').map(n => n.trim().split(' as ')[0].trim());
|
|
|
|
|
const targetFile = modName + '.py';
|
|
|
|
|
const targetCode = generatedFiles[targetFile];
|
|
|
|
|
if (targetCode) {
|
|
|
|
|
for (const sym of importedNames) {
|
|
|
|
|
// Tarkista onko symboli määritelty kohdetiedostossa (def, class, muuttuja)
|
|
|
|
|
const defined = targetCode.includes(`def ${sym}`) || targetCode.includes(`class ${sym}`) || targetCode.match(new RegExp(`^${sym}\\s*=`, 'm'));
|
|
|
|
|
if (!defined) {
|
|
|
|
|
staticIssues.push(`${name}: importtaa '${sym}' tiedostosta ${targetFile}, mutta sitä ei ole määritelty siellä`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (staticIssues.length > 0) {
|
|
|
|
|
termLog(` <span style="color:#d29922">Staattinen analyysi (${staticIssues.length} huomautusta):</span>`);
|
|
|
|
|
for (const issue of staticIssues) {
|
|
|
|
|
termLog(` <span style="color:#d29922">⚠</span> ${esc(issue)}`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
termLog(' <span style="color:#3fb950">Staattinen analyysi: ei huomautuksia</span>');
|
|
|
|
|
}
|
|
|
|
|
const reviewPrompt = `Review this project code. Check EVERY item and report result:
|
|
|
|
|
|
|
|
|
|
1. Imports: ✓/✗ — are all imports valid and available?
|
|
|
|
|
2. Database: ✓/✗ — is the DB setup correct (engine, session, models)?
|
|
|
|
|
3. Endpoints: ✓/✗ — do all routes have correct parameters and return types?
|
|
|
|
|
4. Error handling: ✓/✗ — are edge cases handled (404, validation)?
|
|
|
|
|
5. Security: ✓/✗ — any SQL injection, missing auth, or data exposure?
|
|
|
|
|
|
|
|
|
|
EXAMPLE output:
|
|
|
|
|
1. Imports: ✓ — all imports are valid
|
|
|
|
|
2. Database: ✗ — missing Base.metadata.create_all(engine) call
|
|
|
|
|
3. Endpoints: ✓ — GET/POST/DELETE routes are correct
|
|
|
|
|
4. Error handling: ✗ — no 404 when todo not found
|
|
|
|
|
5. Security: ✓ — using ORM, no raw SQL
|
|
|
|
|
|
|
|
|
|
${allCode}`;
|
|
|
|
|
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt, false, 200);
|
|
|
|
|
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt, false, 300);
|
|
|
|
|
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
|
|
|
|
|
|
|
|
|
|
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
|
|
|
|
|
if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) {
|
|
|
|
|
const hasIssues = review && (review.includes('✗') || staticIssues.length > 0);
|
|
|
|
|
if (hasIssues) {
|
|
|
|
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`);
|
|
|
|
|
pipelineStep('coder', 'Korjaukset', 'active', review);
|
|
|
|
|
const fixPrompt = `Fix the issues found in the review.
|
|
|
|
|
@@ -2542,7 +2710,7 @@ ${Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\
|
|
|
|
|
const mainFile = Object.keys(generatedFiles).find(f => f.includes('main') || f.includes('app')) || Object.keys(generatedFiles)[0];
|
|
|
|
|
const hasPyproject = 'pyproject.toml' in generatedFiles;
|
|
|
|
|
const hasRequirements = 'requirements.txt' in generatedFiles;
|
|
|
|
|
const pyFiles = Object.keys(generatedFiles).filter(f => f.endsWith('.py'));
|
|
|
|
|
const codeFiles = Object.keys(generatedFiles).filter(f => f.endsWith('.py') || f.endsWith('.html'));
|
|
|
|
|
// Dockerfile-templatti: ei anneta mallin keksiä omaa
|
|
|
|
|
let depLines;
|
|
|
|
|
if (hasPyproject) {
|
|
|
|
|
@@ -2556,7 +2724,7 @@ ${Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\
|
|
|
|
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
${depLines}
|
|
|
|
|
COPY ${pyFiles.join(' ')} ./
|
|
|
|
|
COPY ${codeFiles.join(' ')} ./
|
|
|
|
|
EXPOSE 8000
|
|
|
|
|
CMD ["uv", "run", "uvicorn", "${mainFile.replace('.py','')}:app", "--host", "0.0.0.0", "--port", "8000"]`;
|
|
|
|
|
// Generoidaan Dockerfile suoraan templatesta, ei mallilla
|
|
|
|
|
@@ -2568,16 +2736,21 @@ CMD ["uv", "run", "uvicorn", "${mainFile.replace('.py','')}:app", "--host", "0.0
|
|
|
|
|
const step7 = step6 + 1;
|
|
|
|
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${step7}] DevOps</span> — docker-compose.yml`);
|
|
|
|
|
pipelineStep('tester', 'Compose', 'active', 'docker-compose.yml');
|
|
|
|
|
const composePrompt = `Write a docker-compose.yml for this project. Include:
|
|
|
|
|
- app service (build from Dockerfile, port mapping, restart: unless-stopped)
|
|
|
|
|
- db service if SQLite/PostgreSQL is used (volume for data persistence)
|
|
|
|
|
- Named volumes for persistent data
|
|
|
|
|
Only output the YAML content, nothing else.
|
|
|
|
|
// docker-compose.yml templatesta (ei LLM:llä — vältetään version/postgres ongelmat)
|
|
|
|
|
const composeContent = `services:
|
|
|
|
|
app:
|
|
|
|
|
build: .
|
|
|
|
|
ports:
|
|
|
|
|
- "8000:8000"
|
|
|
|
|
volumes:
|
|
|
|
|
- app-data:/app/data
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
|
|
|
|
Files: ${Object.keys(generatedFiles).join(', ')}`;
|
|
|
|
|
const compose = await kpnRun(agentPrompts.tester.model, composePrompt, false, 256);
|
|
|
|
|
if (compose) generatedFiles['docker-compose.yml'] = compose;
|
|
|
|
|
pipelineStep('tester', 'Compose', 'done', 'docker-compose.yml', compose);
|
|
|
|
|
volumes:
|
|
|
|
|
app-data:`;
|
|
|
|
|
generatedFiles['docker-compose.yml'] = composeContent;
|
|
|
|
|
termLog(` <span style="color:#3fb950">✓</span> docker-compose.yml generoitu (template)`);
|
|
|
|
|
pipelineStep('tester', 'Compose', 'done', composeContent, composeContent);
|
|
|
|
|
|
|
|
|
|
// Vaihe 8: DevOps — README
|
|
|
|
|
const step8 = step7 + 1;
|
|
|
|
|
@@ -2585,8 +2758,8 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
|
|
|
|
|
pipelineStep('tester', 'README', 'active', 'README.md');
|
|
|
|
|
const readmePrompt = `Write a minimal README.md. Include ONLY:
|
|
|
|
|
1. One-line description
|
|
|
|
|
2. Quick start: docker compose up
|
|
|
|
|
3. Development: uv sync && uv run uvicorn main:app --reload
|
|
|
|
|
2. Quick start: docker compose up → open http://localhost:8000
|
|
|
|
|
3. Development: uv sync && uv run uvicorn main:app --reload → http://localhost:8000
|
|
|
|
|
4. API endpoints (if applicable)
|
|
|
|
|
5. Testing: uv run pytest
|
|
|
|
|
Max 20 lines.
|
|
|
|
|
@@ -2608,7 +2781,7 @@ Files: ${Object.keys(generatedFiles).join(', ')}`;
|
|
|
|
|
3. docker-compose ports: ✓ OK / ✗ problem description
|
|
|
|
|
4. README commands: ✓ OK / ✗ problem description
|
|
|
|
|
5. Test imports: ✓ OK / ✗ problem description
|
|
|
|
|
6. pyproject.toml deps: ✓ OK / ✗ problem description
|
|
|
|
|
6. pyproject.toml deps: ✓ OK / ✗ problem (NEVER list stdlib: sqlite3, os, sys, json, typing)
|
|
|
|
|
|
|
|
|
|
EXAMPLE output:
|
|
|
|
|
1. Dockerfile COPY: ✓ OK — copies main.py and models.py which both exist
|
|
|
|
|
@@ -2630,20 +2803,192 @@ ${allFiles}`;
|
|
|
|
|
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepFix}] DevOps</span> — korjaukset`);
|
|
|
|
|
pipelineStep('tester', 'Korjaukset', 'active', validation);
|
|
|
|
|
// Korjataan vain Dockerfile ja docker-compose
|
|
|
|
|
const fixPrompt = `Fix ONLY the Dockerfile based on this feedback. Output the corrected Dockerfile, nothing else.
|
|
|
|
|
// Korjataan koodatiedostot (ei Dockerfilea — se on template)
|
|
|
|
|
const fixableFiles = Object.entries(generatedFiles)
|
|
|
|
|
.filter(([n]) => n.endsWith('.py') || n === 'pyproject.toml')
|
|
|
|
|
.map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
|
|
|
|
const fixPrompt = `Fix the code files based on this feedback. Output corrected code only.
|
|
|
|
|
Do NOT output Dockerfile or docker-compose.yml — those are auto-generated.
|
|
|
|
|
|
|
|
|
|
Feedback: ${validation}
|
|
|
|
|
|
|
|
|
|
Current files: ${Object.keys(generatedFiles).join(', ')}
|
|
|
|
|
Current Dockerfile:
|
|
|
|
|
${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
const fixedDockerfile = await kpnRun(agentPrompts.tester.model, fixPrompt, false, 256);
|
|
|
|
|
if (fixedDockerfile) generatedFiles['Dockerfile'] = fixedDockerfile;
|
|
|
|
|
pipelineStep('tester', 'Korjaukset', 'done', 'Dockerfile korjattu', fixedDockerfile);
|
|
|
|
|
${fixableFiles}`;
|
|
|
|
|
const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt, false, 512);
|
|
|
|
|
// Ei ylikirjoiteta Dockerfilea — generoidaan template uudelleen
|
|
|
|
|
if (fixedCode) {
|
|
|
|
|
termLog(` <span style="color:#8b949e">Korjaukset generoitu</span>`);
|
|
|
|
|
}
|
|
|
|
|
pipelineStep('coder', 'Korjaukset', 'done', 'Korjaukset generoitu', fixedCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generoidaan projektin dokumentaatio HTML-raporttina
|
|
|
|
|
const reportHtml = generateProjectReport(task, generatedFiles, pipelineSteps, staticIssues);
|
|
|
|
|
const reportBlob = new Blob([reportHtml], { type: 'text/html' });
|
|
|
|
|
const reportUrl = URL.createObjectURL(reportBlob);
|
|
|
|
|
|
|
|
|
|
// Pipeline valmis — sammutetaan kaikki avataret
|
|
|
|
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
|
|
|
|
document.querySelectorAll('.gallery-head-wrap').forEach(w => w.classList.remove('active'));
|
|
|
|
|
|
|
|
|
|
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
|
|
|
|
|
renderProjectCard(generatedFiles, task);
|
|
|
|
|
termLog(` <a href="${reportUrl}" target="_blank" style="color:#58a6ff;text-decoration:underline;cursor:pointer">📄 Avaa projektiraportti</a>`);
|
|
|
|
|
renderProjectCard(generatedFiles, task, reportUrl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateProjectReport(task, files, steps, staticIssues) {
|
|
|
|
|
const fileEntries = Object.entries(files);
|
|
|
|
|
const agentNames = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' };
|
|
|
|
|
const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
|
|
|
|
|
|
|
|
|
// Syntaksikorostus: kevyt regex-pohjainen highlighter
|
|
|
|
|
function highlightCode(code, filename) {
|
|
|
|
|
let h = code.replace(/&/g,'&').replace(/</g,'<');
|
|
|
|
|
if (filename.endsWith('.py') || filename.endsWith('.toml') || filename.endsWith('.yml') || filename.endsWith('.yaml')) {
|
|
|
|
|
// Kommentit
|
|
|
|
|
h = h.replace(/(#[^\n]*)/g, '<span style="color:#8b949e;font-style:italic">$1</span>');
|
|
|
|
|
// Stringit (kolmois- ja yksittäiset)
|
|
|
|
|
h = h.replace(/("""[\s\S]*?"""|'''[\s\S]*?''')/g, '<span style="color:#a5d6ff">$1</span>');
|
|
|
|
|
h = h.replace(/((?<![\\])(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'))/g, '<span style="color:#a5d6ff">$1</span>');
|
|
|
|
|
// Avainsanat
|
|
|
|
|
h = h.replace(/\b(def|class|import|from|return|if|elif|else|for|in|while|try|except|finally|with|as|raise|yield|async|await|True|False|None|not|and|or|is|lambda)\b/g, '<span style="color:#ff7b72">$1</span>');
|
|
|
|
|
// Dekoraattorit
|
|
|
|
|
h = h.replace(/^(\s*@\w+)/gm, '<span style="color:#d2a8ff">$1</span>');
|
|
|
|
|
// Numerot
|
|
|
|
|
h = h.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#79c0ff">$1</span>');
|
|
|
|
|
} else if (filename.endsWith('.html')) {
|
|
|
|
|
h = h.replace(/(<\/?[\w-]+)/g, '<span style="color:#7ee787">$1</span>');
|
|
|
|
|
h = h.replace(/([\w-]+)=/g, '<span style="color:#79c0ff">$1</span>=');
|
|
|
|
|
h = h.replace(/((?<!=)"[^"]*")/g, '<span style="color:#a5d6ff">$1</span>');
|
|
|
|
|
} else if (filename === 'Dockerfile') {
|
|
|
|
|
h = h.replace(/^(FROM|RUN|COPY|WORKDIR|EXPOSE|CMD|ENV|ARG|ENTRYPOINT|ADD|VOLUME|LABEL|USER)/gm, '<span style="color:#ff7b72">$1</span>');
|
|
|
|
|
h = h.replace(/(#[^\n]*)/g, '<span style="color:#8b949e;font-style:italic">$1</span>');
|
|
|
|
|
h = h.replace(/((?<!=)"[^"]*")/g, '<span style="color:#a5d6ff">$1</span>');
|
|
|
|
|
}
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stepsHtml = steps.map((s, i) => {
|
|
|
|
|
const color = agentColors[s.agent] || '#8b949e';
|
|
|
|
|
const icon = s.status === 'done' ? '✓' : '◷';
|
|
|
|
|
const outputPreview = (s.output || '').substring(0, 500);
|
|
|
|
|
const highlighted = outputPreview ? highlightCode(outputPreview, s.label) : '';
|
|
|
|
|
return `
|
|
|
|
|
<div style="margin-bottom:12px;border:1px solid #30363d;border-radius:8px;overflow:hidden">
|
|
|
|
|
<div style="background:#161b22;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d">
|
|
|
|
|
<span><span style="color:${s.status === 'done' ? '#3fb950' : '#d29922'};font-size:14px">${icon}</span> <strong style="color:${color}">${agentNames[s.agent] || s.agent}</strong> <span style="color:#8b949e">—</span> ${s.label}</span>
|
|
|
|
|
<span style="color:#484f58;font-size:11px;background:#0d1117;padding:2px 8px;border-radius:10px">Vaihe ${i + 1}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${s.input ? `<details><summary style="padding:6px 14px;color:#8b949e;font-size:12px;cursor:pointer;border-bottom:1px solid #21262d">▸ Prompti</summary><pre style="margin:0;padding:10px 14px;background:#010409;font-size:11px;overflow-x:auto;white-space:pre-wrap;color:#8b949e;line-height:1.5">${s.input.replace(/</g,'<').substring(0, 1000)}</pre></details>` : ''}
|
|
|
|
|
${highlighted ? `<pre style="margin:0;padding:10px 14px;background:#0d1117;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9;line-height:1.6">${highlighted}</pre>` : ''}
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
const filesHtml = fileEntries.map(([name, content]) => {
|
|
|
|
|
const ext = name.split('.').pop();
|
|
|
|
|
const langLabel = {py:'Python',html:'HTML',toml:'TOML',yml:'YAML',yaml:'YAML',md:'Markdown'}[ext] || ext.toUpperCase();
|
|
|
|
|
const lines = (content || '').split('\\n').length;
|
|
|
|
|
return `
|
|
|
|
|
<div style="margin-bottom:12px;border:1px solid #30363d;border-radius:8px;overflow:hidden">
|
|
|
|
|
<div style="background:#161b22;padding:8px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d">
|
|
|
|
|
<span style="font-weight:600;color:#58a6ff">${name}</span>
|
|
|
|
|
<span style="color:#484f58;font-size:11px">${langLabel} · ${lines} riviä</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre style="margin:0;padding:12px 14px;background:#010409;font-size:12px;overflow-x:auto;white-space:pre-wrap;color:#c9d1d9;line-height:1.6">${highlightCode(content || '', name)}</pre>
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
const staticHtml = (staticIssues || []).length > 0
|
|
|
|
|
? `<div style="margin-bottom:16px;padding:12px;background:#1c1206;border:1px solid #d29922;border-radius:6px">
|
|
|
|
|
<strong style="color:#d29922">Staattinen analyysi (${staticIssues.length} huomautusta)</strong>
|
|
|
|
|
<ul style="margin:8px 0 0;padding-left:20px;color:#d29922">${staticIssues.map(i => `<li>${i}</li>`).join('')}</ul>
|
|
|
|
|
</div>`
|
|
|
|
|
: '<p style="color:#3fb950">✓ Staattinen analyysi: ei huomautuksia</p>';
|
|
|
|
|
|
|
|
|
|
return `<!DOCTYPE html>
|
|
|
|
|
<html lang="fi">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Kipinä Raportti — ${task}</title>
|
|
|
|
|
<style>
|
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; max-width: 960px; margin: 0 auto; line-height: 1.5; }
|
|
|
|
|
h1 { color: #f0f6fc; margin-bottom: 4px; font-size: 24px; }
|
|
|
|
|
h2 { color: #c9d1d9; margin: 28px 0 14px; border-bottom: 1px solid #30363d; padding-bottom: 8px; font-size: 18px; }
|
|
|
|
|
h3 { color: #8b949e; margin: 16px 0 8px; }
|
|
|
|
|
pre { font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace; tab-size: 4; }
|
|
|
|
|
a { color: #58a6ff; }
|
|
|
|
|
details summary { list-style: none; cursor: pointer; }
|
|
|
|
|
details summary::-webkit-details-marker { display: none; }
|
|
|
|
|
details[open] summary { color: #58a6ff; }
|
|
|
|
|
.sl-badge { display: inline-block; border: 1px solid; border-radius: 6px; padding: 3px 10px; margin: 2px; font-size: 11px; cursor: help; white-space: nowrap; position: relative; transition: transform 0.15s; }
|
|
|
|
|
.sl-badge:hover { transform: translateY(-1px); filter: brightness(1.3); }
|
|
|
|
|
.sl-badge[data-tip]:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #1c2028; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; font-size: 11px; color: #c9d1d9; white-space: pre-wrap; max-width: 350px; min-width: 180px; z-index: 10; box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none; line-height: 1.5; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1>🔥 Kipinä Projektiraportti</h1>
|
|
|
|
|
<p style="color:#8b949e;margin-bottom:20px">${task} — ${new Date().toLocaleString('fi-FI')} — ${fileEntries.length} tiedostoa, ${steps.length} vaihetta</p>
|
|
|
|
|
|
|
|
|
|
<h2>🔄 Agenttien workflow</h2>
|
|
|
|
|
${generateWorkflowSwimlane(steps)}
|
|
|
|
|
|
|
|
|
|
<h2>📋 Pipeline-vaiheet</h2>
|
|
|
|
|
${stepsHtml}
|
|
|
|
|
|
|
|
|
|
<h2>🔍 Staattinen analyysi</h2>
|
|
|
|
|
${staticHtml}
|
|
|
|
|
|
|
|
|
|
<h2>📁 Tiedostot</h2>
|
|
|
|
|
${filesHtml}
|
|
|
|
|
|
|
|
|
|
<hr style="border-color:#30363d;margin:24px 0">
|
|
|
|
|
<p style="color:#8b949e;font-size:12px">Generoitu Kipinä Agentic Playground v0.2.4 — <a href="https://kipina.studio">kipina.studio</a></p>
|
|
|
|
|
</body></html>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateWorkflowSwimlane(steps) {
|
|
|
|
|
const agentLabels = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' };
|
|
|
|
|
const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
|
|
|
|
|
const agentBgs = { manager: '#1c1206', coder: '#0d1a0d', tester: '#0d1520', qa: '#170d22', data: '#1a0d22' };
|
|
|
|
|
const stepDescs = { 'Suunnittelu': 'Jakaa projektin tiedostoiksi', 'Review': 'Tarkistaa koodin laadun', 'Testit': 'Kirjoittaa pytest-testit', 'Dockerfile': 'Generoi Docker-imagen', 'Compose': 'Palvelumääritys', 'README': 'Käyttöohjeet', 'Validointi': 'Tarkistaa yhteensopivuuden', 'Korjaukset': 'Korjaa löydetyt ongelmat' };
|
|
|
|
|
|
|
|
|
|
var agents = [];
|
|
|
|
|
var agentMap = {};
|
|
|
|
|
for (var si = 0; si < steps.length; si++) {
|
|
|
|
|
var s = steps[si];
|
|
|
|
|
if (!agentMap[s.agent]) { agentMap[s.agent] = []; agents.push(s.agent); }
|
|
|
|
|
agentMap[s.agent].push(s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rows = '';
|
|
|
|
|
for (var ai = 0; ai < agents.length; ai++) {
|
|
|
|
|
var agent = agents[ai];
|
|
|
|
|
var color = agentColors[agent] || '#8b949e';
|
|
|
|
|
var bg = agentBgs[agent] || '#161b22';
|
|
|
|
|
var label = agentLabels[agent] || agent;
|
|
|
|
|
var badges = '';
|
|
|
|
|
var aSteps = agentMap[agent];
|
|
|
|
|
for (var bi = 0; bi < aSteps.length; bi++) {
|
|
|
|
|
var st = aSteps[bi];
|
|
|
|
|
var desc = stepDescs[st.label] || st.label;
|
|
|
|
|
var outPrev = (st.output || '').substring(0, 150).replace(/</g, '<').replace(/"/g, '"');
|
|
|
|
|
// Rivitys tooltipiin: jokainen rivi omalle rivilleen
|
|
|
|
|
var tipLines = [desc];
|
|
|
|
|
if (outPrev) {
|
|
|
|
|
tipLines.push('');
|
|
|
|
|
var outLines = outPrev.split(/\n/).slice(0, 6);
|
|
|
|
|
for (var li = 0; li < outLines.length; li++) tipLines.push(outLines[li]);
|
|
|
|
|
if ((st.output || '').split(/\n/).length > 6) tipLines.push('...');
|
|
|
|
|
}
|
|
|
|
|
var icon = st.status === 'done' ? '✓' : '◷';
|
|
|
|
|
if (bi > 0) badges += '<span style="color:#484f58;margin:0 2px">→</span>';
|
|
|
|
|
badges += '<span class="sl-badge" style="background:' + bg + ';border-color:' + color + ';color:' + color + '" data-tip="' + tipLines.join(' ') + '">' + icon + ' ' + st.label.replace(/</g, '<') + '</span>';
|
|
|
|
|
}
|
|
|
|
|
rows += '<div style="display:flex;align-items:center;gap:10px;margin:5px 0;padding:4px 0;border-bottom:1px solid #21262d"><span style="min-width:70px;font-weight:600;font-size:12px;color:' + color + '">' + label + '</span><div style="display:flex;flex-wrap:wrap;align-items:center;gap:2px">' + badges + '</div></div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '<div style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:14px 16px">' + rows + '</div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
|
|
|
|
|
@@ -2838,7 +3183,8 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
clearInterval(spinTimer);
|
|
|
|
|
pullLine.remove();
|
|
|
|
|
if (hubData.status === 'ok') {
|
|
|
|
|
termLog(` <span style="color:#3fb950">✓</span> ${selected.name} ladattu ja aktiivinen`, '#3fb950');
|
|
|
|
|
termLog(` <span style="color:#3fb950">✓</span> ${selected.name} valittu — natiivisolmu lataa mallin`, '#3fb950');
|
|
|
|
|
termLog(` <span style="color:#8b949e">Ensimmäinen pyyntö voi kestää pidempään jos mallia ei ole ladattu</span>`);
|
|
|
|
|
ollamaModels.forEach(m => m.default = false);
|
|
|
|
|
selected.default = true;
|
|
|
|
|
} else {
|
|
|
|
|
@@ -2876,11 +3222,16 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
const btn = document.getElementById('agent-compute-btn');
|
|
|
|
|
const wasmLoaded = btn?.dataset.state === 'ready';
|
|
|
|
|
if (hw.gpu_name && hw.gpu_name !== 'ei natiivisolmua') {
|
|
|
|
|
termLog(` <span style="color:#8b949e">GPU: ${hw.gpu_name} | VRAM: ${Math.round((hw.vram_mb||0)/1024)} GB</span>`);
|
|
|
|
|
const vram = hw.vram_mb ? ` | VRAM: ${Math.round(hw.vram_mb/1024)} GB` : '';
|
|
|
|
|
termLog(` <span style="color:#8b949e">${hw.gpu_name}${vram}</span>`);
|
|
|
|
|
}
|
|
|
|
|
if (loadedNames.length > 0) {
|
|
|
|
|
termLog(` <span style="color:#3fb950">Ollama: ${loadedNames.length} mallia ladattu</span>`);
|
|
|
|
|
}
|
|
|
|
|
termLog(' Mallit <span style="color:#8b949e">(kpn load <numero>)</span>:', '#c9d1d9');
|
|
|
|
|
for (const m of allModels) {
|
|
|
|
|
const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => m.name.includes(n) || n.includes(m.name.split(':')[1]));
|
|
|
|
|
const nameBase = m.name.split(':')[1]; // "7b", "1.5b" etc
|
|
|
|
|
const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => n.includes(nameBase));
|
|
|
|
|
const status = loaded ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
|
|
|
|
|
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">${m.size} | ${m.type}</span>${status}`);
|
|
|
|
|
}
|
|
|
|
|
@@ -2956,7 +3307,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'],
|
|
|
|
|
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
|
|
|
|
|
'kpn run tester': ['"testaa login-toiminto"'],
|
|
|
|
|
'kpn project': ['"FastAPI + SQLite REST API for users"', '"Flask todo app with database"', '"CLI tool for CSV processing in Python"'],
|
|
|
|
|
'kpn project': ['"FastAPI + SQLite CRUD API for users (create, list, update, delete) with HTML form UI served at /"', '"FastAPI todo app with SQLite: CRUD (add, list, toggle done, delete) with simple HTML UI at /"', '"FastAPI bookmark manager with SQLite: CRUD (add, list, edit, delete) with HTML search UI at /"'],
|
|
|
|
|
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -3367,11 +3718,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
|
|
|
|
|
term.scrollTop = term.scrollHeight;
|
|
|
|
|
|
|
|
|
|
// Avatar-aktivointi vain oikeille käyttäjäpyynnöille
|
|
|
|
|
if (data.task_id) {
|
|
|
|
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
|
|
|
|
document.getElementById('avatar-kpn')?.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
// Avatar-aktivointi hoidetaan pipelineStep()-funktiossa
|
|
|
|
|
}
|
|
|
|
|
} else if (isCoder) {
|
|
|
|
|
// Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364)
|
|
|
|
|
@@ -3518,26 +3865,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
term.scrollTop = term.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avatar-aktivointi vain oikeille käyttäjäpyynnöille (task_id)
|
|
|
|
|
if (data.task_id) {
|
|
|
|
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
|
|
|
|
const model = data.model || '';
|
|
|
|
|
const p = data.prompt ? data.prompt.toLowerCase() : '';
|
|
|
|
|
|
|
|
|
|
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
|
|
|
|
|
document.getElementById('avatar-kpn')?.classList.add('active');
|
|
|
|
|
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
|
|
|
|
|
document.getElementById('avatar-tester')?.classList.add('active');
|
|
|
|
|
} else if (p.includes('tervehdi')) {
|
|
|
|
|
document.getElementById('avatar-client')?.classList.add('active');
|
|
|
|
|
} else if (p.includes('test')) {
|
|
|
|
|
document.getElementById('avatar-qa')?.classList.add('active');
|
|
|
|
|
} else if (model.includes('coder') || model.includes('Coder')) {
|
|
|
|
|
document.getElementById('avatar-coder')?.classList.add('active');
|
|
|
|
|
} else if (model.includes('deepseek') || model.includes('r1')) {
|
|
|
|
|
document.getElementById('avatar-observer')?.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Avatar-aktivointi hoidetaan pipelineStep()-funktiossa
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
@@ -4236,6 +4564,7 @@ ${generatedFiles['Dockerfile'] || '(puuttuu)'}`;
|
|
|
|
|
|
|
|
|
|
function inlineFormat(text) {
|
|
|
|
|
return text
|
|
|
|
|
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:8px;border:1px solid #30363d;margin:12px 0;display:block">')
|
|
|
|
|
.replace(/`([^`]+)`/g, '<code style="background:#161b22;padding:2px 6px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>')
|
|
|
|
|
.replace(/\*\*([^*]+)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
|
|
|
|
|
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
|
|
|
|