Pipeline-rajoitteet kevennetty ja näkyville Asetukset-sivulle

- maxTokens: client/manager/devops/observer 512→1024
- Client: 200→400 sanaa, 3-5→3-8 ominaisuutta, MVP-rajoitus poistettu
- Manager: 4-5→8 tiedostoa, vapaa tila 6→8
- Terminaali: 100→300 riviä, CrewAI prompt truncation 20→50 riviä
- Uusi pipelineConfig-objekti (localStorage-persistenssi)
- Asetukset-sivulle Pipeline-rajoitteet -osio sliderien kanssa
- AGENTS_VERSION 3→4
This commit is contained in:
Jaakko Vanhala
2026-04-12 15:47:46 +03:00
parent 74a2045def
commit 1f85c03624
2 changed files with 132 additions and 68 deletions

View File

@@ -58,6 +58,49 @@
</select> </select>
</div> </div>
<!-- Pipeline-rajoitteet -->
<div class="settings-section">
<h3 class="settings-title">Pipeline-rajoitteet</h3>
<p class="settings-desc">Projektin generoinnin rajat. Suuremmat arvot = rikkaampi output, hitaampi suoritus.</p>
<div class="settings-grid">
<div>
<label class="settings-label">Client: max sanat <span id="set-plc-words-val" class="settings-val">400</span></label>
<input type="range" id="set-plc-words" min="100" max="800" step="50" value="400" class="settings-slider">
<div class="settings-hint">Vaatimusmäärittelyn maksimipituus sanoina</div>
</div>
<div>
<label class="settings-label">Client: max ominaisuudet <span id="set-plc-feats-val" class="settings-val">8</span></label>
<input type="range" id="set-plc-feats" min="3" max="15" step="1" value="8" class="settings-slider">
<div class="settings-hint">Montako ominaisuutta vaatimuksiin</div>
</div>
<div>
<label class="settings-label">Manager: max tiedostot <span id="set-plc-mfiles-val" class="settings-val">8</span></label>
<input type="range" id="set-plc-mfiles" min="3" max="15" step="1" value="8" class="settings-slider">
<div class="settings-hint">Managerin suunnittelemien tiedostojen yläraja</div>
</div>
<div>
<label class="settings-label">Vapaa tila: max tiedostot <span id="set-plc-ffiles-val" class="settings-val">8</span></label>
<input type="range" id="set-plc-ffiles" min="3" max="15" step="1" value="8" class="settings-slider">
<div class="settings-hint">Tiedostoraja kun ei mallipohjaa</div>
</div>
<div>
<label class="settings-label">Review-kierrokset <span id="set-plc-review-val" class="settings-val">3</span></label>
<input type="range" id="set-plc-review" min="1" max="5" step="1" value="3" class="settings-slider">
<div class="settings-hint">Katselmointi-korjaus-syklien max määrä</div>
</div>
<div>
<label class="settings-label">Terminaali: max rivit <span id="set-plc-term-val" class="settings-val">300</span></label>
<input type="range" id="set-plc-term" min="50" max="1000" step="50" value="300" class="settings-slider">
<div class="settings-hint">Terminaalin näyttämien rivien yläraja</div>
</div>
<div>
<label class="settings-label">CrewAI: prompt-rivit <span id="set-plc-crew-val" class="settings-val">50</span></label>
<input type="range" id="set-plc-crew" min="10" max="200" step="10" value="50" class="settings-slider">
<div class="settings-hint">tasks.yaml:n promptin max rivimäärä</div>
</div>
</div>
</div>
<!-- Reset --> <!-- Reset -->
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)"> <div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button> <button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button>

View File

@@ -42,7 +42,7 @@ import Settings from "../components/Settings.astro";
<p class="hero-desc"> <p class="hero-desc">
Seuraa reaaliajassa miten kuusi erikoistunutta AI-agenttia suunnittelee, koodaa, testaa ja katselmoi ohjelmistoprojektin — askel askeleelta. Seuraa reaaliajassa miten kuusi erikoistunutta AI-agenttia suunnittelee, koodaa, testaa ja katselmoi ohjelmistoprojektin — askel askeleelta.
</p> </p>
<p class="hero-notice"> <p class="hero-notice" style="border-left-color:#ff6b00;color:#ff6b00">
Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopuksi saat toistettavan CrewAI-projektin. Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopuksi saat toistettavan CrewAI-projektin.
</p> </p>
@@ -212,33 +212,32 @@ import Settings from "../components/Settings.astro";
// === Globaalit tilat === // === Globaalit tilat ===
const defaultAgents = { const defaultAgents = {
client: { name: 'Asiakas', avatar: '/avatars/kettu_notext.webp', model: 'qwen-coder', order: 0, client: { name: 'Client', avatar: '/avatars/kettu_notext.webp', model: 'qwen-coder', order: 0,
temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512, temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a product owner who turns vague ideas into clear, actionable software requirements. prompt: `You are a product owner who turns vague ideas into clear, actionable software requirements.
GIVEN a short project description from the user, produce a structured brief: GIVEN a short project description from the user, produce a structured brief:
1. PROJECT NAME: a short, descriptive name 1. PROJECT NAME: a short, descriptive name
2. GOAL: one sentence explaining what the software does and who it's for 2. GOAL: one sentence explaining what the software does and who it's for
3. CORE FEATURES: numbered list of 3-5 concrete features (not vague wishes) 3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes)
4. DATA MODEL: list the main entities and their key fields 4. DATA MODEL: list the main entities and their key fields (include field types)
5. API ENDPOINTS: list the essential REST endpoints (method + path + purpose) 5. API ENDPOINTS: list the REST endpoints (method + path + purpose)
6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed for MVP") 6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed")
RULES: RULES:
- Be specific: "User can filter todos by status" not "todo management" - Be specific: "User can filter todos by status" not "todo management"
- Keep scope small — MVP only, no nice-to-haves
- Use plain English, no code - Use plain English, no code
- Maximum 200 words total` }, - Maximum 400 words total` },
manager: { name: 'Manageri', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 1, manager: { name: 'Manager', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 1,
temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 512, temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a senior project manager and software architect. Your job is to plan the file structure of a software project. prompt: `You are a senior project manager and software architect. Your job is to plan the file structure of a software project.
RULES: RULES:
- List each source file on its own line in format: filename.py: one-line description - List each source file on its own line in format: filename.py: one-line description
- List dependency files FIRST (models.py, schemas.py before main.py) - List dependency files FIRST (models.py, schemas.py before main.py)
- Use pyproject.toml for Python dependencies (never requirements.txt) - Use pyproject.toml for Python dependencies (never requirements.txt)
- Maximum 4-5 files per project - Maximum 8 files per project
- Only filenames, no directory paths - Only filenames, no directory paths
- Common patterns: models.py → schemas.py → main.py → pyproject.toml - Common patterns: models.py → schemas.py → main.py → pyproject.toml
@@ -247,7 +246,7 @@ models.py: SQLAlchemy database models and engine setup
schemas.py: Pydantic request/response schemas schemas.py: Pydantic request/response schemas
main.py: FastAPI application with CRUD endpoints main.py: FastAPI application with CRUD endpoints
pyproject.toml: project dependencies` }, pyproject.toml: project dependencies` },
coder: { name: 'Koodari', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 2, coder: { name: 'Coder', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 2,
temperature: 0.7, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, temperature: 0.7, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are an expert Python developer. Write complete, production-ready code. prompt: `You are an expert Python developer. Write complete, production-ready code.
@@ -268,7 +267,7 @@ NEVER:
- Forget to import from other project files - Forget to import from other project files
- Use requirements.txt or Poetry — always use pyproject.toml with [project] format (PEP 621) - Use requirements.txt or Poetry — always use pyproject.toml with [project] format (PEP 621)
- Use pip install — use uv (e.g. uv run uvicorn main:app --reload)` }, - Use pip install — use uv (e.g. uv run uvicorn main:app --reload)` },
data: { name: 'Data', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3, data: { name: 'Data Engineer', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3,
temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024, temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are a database architect specializing in SQLAlchemy and relational databases. prompt: `You are a database architect specializing in SQLAlchemy and relational databases.
@@ -303,7 +302,7 @@ TEST STRUCTURE:
ALWAYS: from fastapi.testclient import TestClient` }, ALWAYS: from fastapi.testclient import TestClient` },
tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5, tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 512, temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line. prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line.
STATIC ANALYSIS CHECKLIST: STATIC ANALYSIS CHECKLIST:
@@ -321,8 +320,8 @@ RESPOND:
- If all checks pass: "LGTM" - If all checks pass: "LGTM"
- If issues found: list each as "ISSUE: filename.py: description" - If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague` }, - Be specific and actionable, not vague` },
observer: { name: 'Tarkkailija', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6, observer: { name: 'Observer', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6,
temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512, temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
prompt: `You are an independent technical observer and risk analyst. prompt: `You are an independent technical observer and risk analyst.
EVALUATE THE PROJECT FOR: EVALUATE THE PROJECT FOR:
@@ -337,7 +336,7 @@ OUTPUT FORMAT:
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` }, - End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` },
}; };
// Versio: kasvata kun oletuspromptit muuttuvat // Versio: kasvata kun oletuspromptit muuttuvat
const AGENTS_VERSION = 3; const AGENTS_VERSION = 4;
let agents; let agents;
const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0'); const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0');
if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) { if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) {
@@ -369,6 +368,19 @@ OUTPUT FORMAT:
let settings = JSON.parse(localStorage.getItem('kpn-settings') || 'null') || { ...defaultSettings }; let settings = JSON.parse(localStorage.getItem('kpn-settings') || 'null') || { ...defaultSettings };
function saveSettings() { localStorage.setItem('kpn-settings', JSON.stringify(settings)); } function saveSettings() { localStorage.setItem('kpn-settings', JSON.stringify(settings)); }
// Pipeline-rajoitteet (näkyvillä Asetukset-sivulla)
const defaultPipelineConfig = {
clientMaxWords: 400,
clientMaxFeatures: 8,
managerMaxFiles: 8,
freeMaxFiles: 8,
maxReviewRounds: 3,
terminalMaxLines: 300,
crewaiPromptLines: 50,
};
let pipelineConfig = JSON.parse(localStorage.getItem('kpn-pipeline') || 'null') || { ...defaultPipelineConfig };
function savePipelineConfig() { localStorage.setItem('kpn-pipeline', JSON.stringify(pipelineConfig)); }
// === Tab switching === // === Tab switching ===
window.switchTab = function(tab) { window.switchTab = function(tab) {
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
@@ -561,7 +573,7 @@ OUTPUT FORMAT:
if (color) div.style.color = color; if (color) div.style.color = color;
div.innerHTML = html; div.innerHTML = html;
termPanel.appendChild(div); termPanel.appendChild(div);
while (termPanel.children.length > 100 && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild); while (termPanel.children.length > pipelineConfig.terminalMaxLines && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild);
termPanel.scrollTop = termPanel.scrollHeight; termPanel.scrollTop = termPanel.scrollHeight;
} }
@@ -658,21 +670,12 @@ OUTPUT FORMAT:
const statusDiv = document.createElement('div'); const statusDiv = document.createElement('div');
statusDiv.className = 'terminal-line'; statusDiv.className = 'terminal-line';
statusDiv.id = 'status-' + taskId; statusDiv.id = 'status-' + taskId;
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> käsittelee...</span>`; statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> processing...</span>`;
termPanel.appendChild(statusDiv); termPanel.appendChild(statusDiv);
termPanel.scrollTop = termPanel.scrollHeight; termPanel.scrollTop = termPanel.scrollHeight;
try { try {
if (!silent) { statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> processing...</span>`;
const streamDiv = document.createElement('div');
streamDiv.className = 'terminal-line';
streamDiv.innerHTML = ' <span class="stream-content"></span><span style="color:#8b949e;animation:blink 1s infinite">▌</span>';
termPanel.appendChild(streamDiv);
termPanel.scrollTop = termPanel.scrollHeight;
activeStreams[taskId] = streamDiv;
}
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> käsittelee...</span>`;
// Rakennetaan pyyntö: agentin asetukset tai globaalit oletukset // Rakennetaan pyyntö: agentin asetukset tai globaalit oletukset
const opts = agentOpts || {}; const opts = agentOpts || {};
@@ -716,14 +719,7 @@ OUTPUT FORMAT:
statusDiv.innerHTML = ` <span style="color:#3fb950">✓</span> <span style="color:var(--accent)">${esc(data.model || model)}</span> <span style="color:#8b949e">${tokGen} tok · ${durS} · ${tokS}</span>`; statusDiv.innerHTML = ` <span style="color:#3fb950">✓</span> <span style="color:var(--accent)">${esc(data.model || model)}</span> <span style="color:#8b949e">${tokGen} tok · ${durS} · ${tokS}</span>`;
if (!silent && response) { if (!silent && response) {
const firstLine = response.split('\n').find(l => l.trim()) || response;
const lineCount = response.split('\n').filter(l => l.trim()).length;
const uid = 'code-' + Date.now();
termLog(
` <span style="color:#3fb950;cursor:pointer" onclick="document.getElementById('${uid}').style.display=document.getElementById('${uid}').style.display==='none'?'block':'none'">`
+ `<span style="color:#8b949e">▶</span> ${esc(firstLine.trim())} <span style="color:#8b949e">${lineCount > 1 ? '(+' + (lineCount-1) + ' riviä)' : ''}</span></span>`
+ `<pre id="${uid}" style="display:none;margin:4px 0 0 16px;font:inherit;white-space:pre-wrap;border-left:2px solid var(--border);padding-left:10px">${highlightCode(response)}</pre>`
);
} }
return response; return response;
} catch(e) { } catch(e) {
@@ -975,11 +971,11 @@ OUTPUT FORMAT:
const name = `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`; const name = `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`;
ty += `\n${name}:\n`; ty += `\n${name}:\n`;
ty += ` description: |\n`; ty += ` description: |\n`;
const descLines = e.userPrompt.split('\n').slice(0, 20); const descLines = e.userPrompt.split('\n').slice(0, pipelineConfig.crewaiPromptLines);
for (const line of descLines) { for (const line of descLines) {
ty += ` ${line}\n`; ty += ` ${line}\n`;
} }
if (e.userPrompt.split('\n').length > 20) ty += ` # ... (truncated)\n`; if (e.userPrompt.split('\n').length > pipelineConfig.crewaiPromptLines) ty += ` # ... (truncated)\n`;
ty += ` expected_output: >-\n ${e.label}\n`; ty += ` expected_output: >-\n ${e.label}\n`;
ty += ` agent: ${e.agentKey}\n`; ty += ` agent: ${e.agentKey}\n`;
} }
@@ -1027,14 +1023,14 @@ OUTPUT FORMAT:
const cdr = agents.coder || Object.values(agents)[2]; const cdr = agents.coder || Object.values(agents)[2];
// Asiakas: jalostaa vaatimukset // Asiakas: jalostaa vaatimukset
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti — ${esc(task)} ━━━</span>`); termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Project — ${esc(task)} ━━━</span>`);
termLog(`\n<span style="color:#f0883e;font-weight:bold">[0] ${esc(cli.name)}</span> — vaatimusmäärittely`); termLog(`\n<span style="color:#f0883e;font-weight:bold">[0] ${esc(cli.name)}</span> — Requirements Definition`);
highlightAgent('client'); highlightAgent('client');
explainStep('Vaatimusmäärittely', `${cli.name} muotoilee idean selkeiksi vaatimuksiksi: ominaisuudet, datamallit, rajapinnat.`); explainStep('Requirements Definition', `${cli.name} forms clear requirements: features, models, endpoints.`);
const brief = await kpnRun(cli.model, `${task}`, false, cli); const brief = await kpnRun(cli.model, `${task}`, false, cli);
if (!brief) { termLog(' ✗ Vaatimusmäärittely epäonnistui', '#f85149'); return; } if (!brief) { termLog(' ✗ Requirements definition failed', '#f85149'); return; }
promptLog.push({ step: 0, agentKey: 'client', agentName: cli.name, model: cli.model, label: 'vaatimusmäärittely', systemPrompt: cli.prompt || '', userPrompt: task, response: brief }); promptLog.push({ step: 0, agentKey: 'client', agentName: cli.name, model: cli.model, label: 'requirements', systemPrompt: cli.prompt || '', userPrompt: task, response: brief });
termLog(` <span style="color:#8b949e">Vaatimukset valmiit → Manageri</span>`); termLog(` <span style="color:#8b949e">Requirements ready → Manager</span>`);
// Valitaan mallipohja automaattisesti briefin perusteella // Valitaan mallipohja automaattisesti briefin perusteella
const template = selectTemplate(brief); const template = selectTemplate(brief);
@@ -1047,15 +1043,15 @@ OUTPUT FORMAT:
// Mallipohja löytyi — käytetään sen rakennetta // Mallipohja löytyi — käytetään sen rakennetta
fileOrder = template.order; fileOrder = template.order;
fileDefs = template.files; fileDefs = template.files;
explainStep('Mallipohja', `Tunnistettiin "${template.name}" — ${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}.`); explainStep('Template', `Detected "${template.name}" — ${fileOrder.length} files: ${fileOrder.join(', ')}.`);
} else { } else {
// Vapaa tila — Manageri päättää tiedostorakenteen // Vapaa tila — Manageri päättää tiedostorakenteen
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — tiedostorakenne`); termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — File structure`);
highlightAgent('manager'); highlightAgent('manager');
explainStep('Vapaa tila', 'Sopivaa mallipohjaa ei löytynyt. Manageri suunnittelee tiedostorakenteen vaatimusten perusteella.'); explainStep('Free mode', 'No suitable template found. Manager plans the architecture.');
const planPrompt = `PROJECT REQUIREMENTS:\n${brief}\n\nPlan the file structure for this project. List each file on its own line:\nfilename.ext: one-line description\n\nMaximum 6 files. List dependency files first.`; const planPrompt = `PROJECT REQUIREMENTS:\n${brief}\n\nPlan the file structure for this project. List each file on its own line:\nfilename.ext: one-line description\n\nMaximum ${pipelineConfig.freeMaxFiles} files. List dependency files first.`;
const plan = await kpnRun(mgr.model, planPrompt, false, mgr); const plan = await kpnRun(mgr.model, planPrompt, false, mgr);
if (!plan) { termLog(' ✗ Suunnittelu epäonnistui', '#f85149'); return; } if (!plan) { termLog(' ✗ Planning failed', '#f85149'); return; }
// Parsitaan managerin tuottama tiedostolista // Parsitaan managerin tuottama tiedostolista
for (const line of plan.split('\n')) { for (const line of plan.split('\n')) {
@@ -1067,11 +1063,11 @@ OUTPUT FORMAT:
} }
} }
if (fileOrder.length === 0) { if (fileOrder.length === 0) {
termLog(' ✗ Manageri ei tuottanut tiedostolistaa', '#f85149'); termLog(' ✗ Manager produced no file list', '#f85149');
return; return;
} }
explainStep('Suunnitelma', `${fileOrder.length} tiedostoa: ${fileOrder.join(', ')}`); explainStep('Plan', `${fileOrder.length} files: ${fileOrder.join(', ')}`);
promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'tiedostorakenne', systemPrompt: mgr.prompt || '', userPrompt: planPrompt, response: plan }); promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'file structure', systemPrompt: mgr.prompt || '', userPrompt: planPrompt, response: plan });
} }
const files = {}; const files = {};
@@ -1135,16 +1131,16 @@ OUTPUT FORMAT:
// Review-korjausluuppi: max 2 kierrosta // Review-korjausluuppi: max 2 kierrosta
const tst = agents.tester || Object.values(agents)[5]; const tst = agents.tester || Object.values(agents)[5];
const MAX_REVIEW_ROUNDS = 3; const MAX_REVIEW_ROUNDS = pipelineConfig.maxReviewRounds;
for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) { for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n'); const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
// DevOps review // DevOps review
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — koodikatselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`); termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — code review${round > 0 ? ' (round '+(round+1)+')' : ''}`);
highlightAgent('tester'); highlightAgent('tester');
if (round === 0) explainStep('Koodikatselmointi', `${tst.name} analysoi koodin rivi riviltä: importit, nimeämiset, virheenkäsittely, tietoturva.`); if (round === 0) explainStep('Code Review', `${tst.name} reviews code line by line: imports, naming, security.`);
else explainStep('Uudelleentarkistus', `${tst.name} tarkistaa korjaukset.`); else explainStep('Re-evaluation', `${tst.name} checks the applied fixes.`);
const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`; const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`;
const review = await kpnRun(tst.model, reviewPrompt, false, tst); const review = await kpnRun(tst.model, reviewPrompt, false, tst);
@@ -1158,16 +1154,16 @@ OUTPUT FORMAT:
} }
// Korjaukset // Korjaukset
termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`); termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — fixes${round > 0 ? ' (round '+(round+1)+')' : ''}`);
highlightAgent('coder'); highlightAgent('coder');
explainStep('Korjaus', `${tst.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa.`); explainStep('Fixing', `${tst.name} found issues. ${cdr.name} will attempt to fix them.`);
const fixPrompt = `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${currentCode}\n\nWrite ALL corrected files. Start each file with: --- filename.py ---`; const fixPrompt = `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${currentCode}\n\nWrite ALL corrected files. Start each file with: --- filename.py ---`;
const fixedCode = await kpnRun(cdr.model, fixPrompt, false, cdr); const fixedCode = await kpnRun(cdr.model, fixPrompt, false, cdr);
// Parsitaan korjatut tiedostot takaisin files-objektiin // Parsitaan korjatut tiedostot takaisin files-objektiin
if (fixedCode) { if (fixedCode) {
promptLog.push({ step: promptLog.length, agentKey: 'coder', agentName: cdr.name, model: cdr.model, label: 'korjaus' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: cdr.prompt || '', userPrompt: fixPrompt, response: fixedCode }); promptLog.push({ step: promptLog.length, agentKey: 'coder', agentName: cdr.name, model: cdr.model, label: 'fix' + (round > 0 ? '_r'+(round+1) : ''), systemPrompt: cdr.prompt || '', userPrompt: fixPrompt, response: fixedCode });
const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m); const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
for (let j = 1; j < fixedParts.length; j += 2) { for (let j = 1; j < fixedParts.length; j += 2) {
const fname = fixedParts[j].trim(); const fname = fixedParts[j].trim();
@@ -1293,23 +1289,23 @@ OUTPUT FORMAT:
pipelineAbort = new AbortController(); pipelineAbort = new AbortController();
const cli = agents.client || Object.values(agents)[0]; const cli = agents.client || Object.values(agents)[0];
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Pipeline ━━━</span>`); termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Pipeline ━━━</span>`);
termLog(`\n<span style="color:#f0883e;font-weight:bold">[1/4] ${esc(cli.name)}</span> — vaatimukset`); termLog(`\n<span style="color:#f0883e;font-weight:bold">[1/4] ${esc(cli.name)}</span> — Requirements`);
highlightAgent('client'); highlightAgent('client');
const brief = await kpnRun(cli.model, `${task}`, false, cli); const brief = await kpnRun(cli.model, `${task}`, false, cli);
if (!brief) return; if (!brief) return;
termLog(`\n<span style="color:#d29922;font-weight:bold">[2/4] Manageri</span>`); termLog(`\n<span style="color:#d29922;font-weight:bold">[2/4] Manager</span>`);
highlightAgent('manager'); highlightAgent('manager');
const plan = await kpnRun('qwen-coder', `Requirements:\n${brief}\n\nAnalyse briefly, write a spec:\n${task}`); const plan = await kpnRun('qwen-coder', `Requirements:\n${brief}\n\nAnalyse briefly, write a spec:\n${task}`);
if (!plan) return; if (!plan) return;
termLog(`\n<span style="color:#3fb950;font-weight:bold">[3/4] Koodari</span>`); termLog(`\n<span style="color:#3fb950;font-weight:bold">[3/4] Coder</span>`);
highlightAgent('coder'); highlightAgent('coder');
const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`); const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`);
if (!code) return; if (!code) return;
termLog(`\n<span style="color:var(--accent);font-weight:bold">[4/4] Testaaja</span>`); termLog(`\n<span style="color:var(--accent);font-weight:bold">[4/4] DevOps</span>`);
highlightAgent('tester'); highlightAgent('tester');
await kpnRun('qwen-coder', `Review briefly:\n${code}`); await kpnRun('qwen-coder', `Review briefly:\n${code}`);
highlightAgent(null); highlightAgent(null);
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`); termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Done ━━━</span>`);
} }
// === Project card === // === Project card ===
@@ -1403,14 +1399,14 @@ OUTPUT FORMAT:
else window._editorModels[name] = m.editor.createModel(code, langMap[ext]||'plaintext'); else window._editorModels[name] = m.editor.createModel(code, langMap[ext]||'plaintext');
} }
document.getElementById('editor-file-list').innerHTML = Object.keys(files).map(n => `<div class="dd-item" onclick="openFile('${n}')">${n}</div>`).join(''); document.getElementById('editor-file-list').innerHTML = Object.keys(files).map(n => `<div class="dd-item" onclick="openFile('${n}')">${n}</div>`).join('');
document.getElementById('editor-tabs').innerHTML = Object.keys(files).map(n => `<div class="project-tab" onclick="openFile('${n}')">${n}</div>`).join('');
openFile(Object.keys(files)[0]); openFile(Object.keys(files)[0]);
}; };
window.openFile = function(name) { window.openFile = function(name) {
if (!window._editorModels[name] || !window._monaco) return; if (!window._editorModels[name] || !window._monaco) return;
window._monaco.setModel(window._editorModels[name]); window._monaco.setModel(window._editorModels[name]);
document.querySelectorAll('#editor-file-list .dd-item').forEach(el => el.style.background = el.textContent===name ? 'var(--border)' : ''); document.querySelectorAll('#editor-file-list .dd-item').forEach(el => el.style.background = el.textContent===name ? 'var(--border)' : '');
document.querySelectorAll('#editor-tabs .project-tab').forEach(el => el.classList.toggle('active', el.textContent===name)); document.getElementById('editor-tabs').innerHTML = `<div class="project-tab active" style="cursor:default">${name}</div>`;
}; };
// === Guide loader === // === Guide loader ===
@@ -1543,6 +1539,29 @@ OUTPUT FORMAT:
els.maxTokens.oninput = () => { settings.maxTokens = +els.maxTokens.value; els.maxtokVal.textContent = settings.maxTokens; saveSettings(); }; els.maxTokens.oninput = () => { settings.maxTokens = +els.maxTokens.value; els.maxtokVal.textContent = settings.maxTokens; saveSettings(); };
els.stopSeq.oninput = () => { settings.stopSequences = els.stopSeq.value.replace(/\n/g, '\\n'); saveSettings(); }; els.stopSeq.oninput = () => { settings.stopSequences = els.stopSeq.value.replace(/\n/g, '\\n'); saveSettings(); };
els.model.onchange = () => { settings.model = els.model.value; saveSettings(); }; els.model.onchange = () => { settings.model = els.model.value; saveSettings(); };
// Pipeline-rajoitteet
const plcMap = [
['plc-words', 'clientMaxWords'],
['plc-feats', 'clientMaxFeatures'],
['plc-mfiles', 'managerMaxFiles'],
['plc-ffiles', 'freeMaxFiles'],
['plc-review', 'maxReviewRounds'],
['plc-term', 'terminalMaxLines'],
['plc-crew', 'crewaiPromptLines'],
];
for (const [id, key] of plcMap) {
const slider = document.getElementById('set-' + id);
const valEl = document.getElementById('set-' + id + '-val');
if (!slider || !valEl) continue;
slider.value = pipelineConfig[key];
valEl.textContent = pipelineConfig[key];
slider.oninput = () => {
pipelineConfig[key] = +slider.value;
valEl.textContent = pipelineConfig[key];
savePipelineConfig();
};
}
} }
// Alustetaan kun settings-tab avataan // Alustetaan kun settings-tab avataan
const origSwitchTab = window.switchTab; const origSwitchTab = window.switchTab;
@@ -1554,7 +1573,9 @@ OUTPUT FORMAT:
window.resetSettings = function() { window.resetSettings = function() {
if (!confirm('Palautetaanko kaikki asetukset oletuksiin?')) return; if (!confirm('Palautetaanko kaikki asetukset oletuksiin?')) return;
settings = { ...defaultSettings }; settings = { ...defaultSettings };
pipelineConfig = { ...defaultPipelineConfig };
saveSettings(); saveSettings();
savePipelineConfig();
initSettings(); initSettings();
}; };
// === Raportti-modal === // === Raportti-modal ===