---
import "../styles/global.css";
import StatusBar from "../components/StatusBar.astro";
import Terminal from "../components/Terminal.astro";
import Editor from "../components/Editor.astro";
import Guide from "../components/Guide.astro";
import AgentBar from "../components/AgentBar.astro";
import Settings from "../components/Settings.astro";
---
<!DOCTYPE html>
<html lang="fi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kipinä Agentic Studio — Näe miten AI-agentit rakentavat projektin</title>
    <meta name="description" content="Seuraa reaaliajassa miten AI-agenttitiimi suunnittelee, koodaa ja testaa ohjelmistoprojektin. Ilmainen oppimistyökalu.">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
    <script is:inline type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.14.0/dist/mermaid.esm.min.mjs';
        mermaid.initialize({ startOnLoad: false, theme: 'dark' });
        window.mermaid = mermaid;
    </script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css">
</head>
<body>
    <!-- ===== LANDING ===== -->
    <div id="landing">
        <div class="bg-mesh"></div>
        <header class="landing-nav">
            <a href="/" class="landing-logo"><span class="logo-accent">KIPINÄ</span> <span class="logo-sub">/ agentic studio</span></a>
            <button id="theme-cycle-btn" class="theme-cycle-btn" title="Vaihda teemaa"></button>
        </header>

        <section class="hero">
            <div class="hero-container">
                <div class="hero-content">
                    <h1 class="hero-title">Näe miten AI-agenttitiimi rakentaa projektin.</h1>
                    <div class="hero-divider"></div>
                    <p class="hero-desc">
                        Seuraa reaaliajassa miten ohjelmistokehitykseen erikoistuneet AI-agentit suunnittelevat, koodaavat, testaavat ja katselmoivat ohjelmistoprojektin askel askeleelta. Nämä veijarit ovat erityisen hyviä Python-ohjelmoinnissa.
                    </p>
                    <p class="hero-notice" style="border-left-color:#ff6b00;color:#ff6b00">
                        Jokaisen agentin prompti, syöte ja tulos tallennetaan. Lopputuloksena syntyy CrewAI-projekti, jonka parissa voit jatkaa eteenpäin.
                    </p>

                    <div class="hero-input-group">
                        <input type="text" id="landing-input" class="hero-input" placeholder="Kuvaile projektisi, esim. &quot;REST API käyttäjähallinnalle&quot;" autocomplete="off">
                        <button id="landing-start" class="hero-btn">Käynnistä</button>
                    </div>

                    <div class="hero-examples">
                        <span class="hero-examples-label">Tai kokeile valmista:</span>
                        <button class="example-btn" data-prompt="REST API käyttäjähallinnalle SQLite-tietokannalla">Käyttäjähallinta-API</button>
                        <button class="example-btn" data-prompt="UWB-paikannusdatan ETL-pipeline CSV:stä MariaDB:hen ja visualisointi">UWB-data-analyysi</button>
                        <button class="example-btn" data-prompt="Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit">Todo-sovellus</button>
                    </div>
                </div>
                <div class="hero-orb-wrapper">
                    <div class="hero-orb">
                        <img src="/gecko_hero.webp" alt="Kipinä" class="hero-orb-img">
                    </div>
                </div>
            </div>
        </section>

        <!-- Miten se toimii -->
        <section class="how-section">
            <div class="how-container">
                <h2 class="how-title">Miten se toimii?</h2>
                <div class="how-steps">
                    <div class="how-step">
                        <div class="how-step-num">1</div>
                        <h3>Kuvaile projekti</h3>
                        <p>Kirjoita lyhyt kuvaus. Asiakas-agentti jalostaa sen vaatimuksiksi.</p>
                    </div>
                    <div class="how-step">
                        <div class="how-step-num">2</div>
                        <h3>Agentit työskentelevät</h3>
                        <p>Manageri suunnittelee, koodari kirjoittaa, testaaja katselmoi ja korjaa.</p>
                    </div>
                    <div class="how-step">
                        <div class="how-step-num">3</div>
                        <h3>Tutki tulokset</h3>
                        <p>Näe valmis koodi, jokaisen agentin promptit ja CrewAI-projektirakenne.</p>
                    </div>
                </div>
            </div>
        </section>

        <footer class="landing-footer">
            <p>&copy; 2026 Kipinä Technologies — <a href="https://kipina.tech">kipina.tech</a></p>
        </footer>
    </div>

    <!-- ===== APP (piilotettu kunnes pipeline käynnistyy) ===== -->
    <div id="app" class="container">
        <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px">
            <div>
                <h1 style="margin-bottom:0"><span style="color:#ff6b00">Kipinä</span> Agentic Studio</h1>
                <p style="color:#8b949e;margin:0">AI-ohjelmistokehitystiimi · <span id="hub-version">-</span></p>
            </div>
            <button class="btn btn-muted" onclick="showLanding()" style="font-size:14px">← Takaisin</button>
        </div>

        <!-- Välilehdet -->
        <div class="tabs">
            <div class="tab active" onclick="switchTab('agents')">Agentit</div>
            <div class="tab" onclick="switchTab('learn')">Oppimispolku</div>
            <div class="tab" onclick="switchTab('editor')">Editor</div>
            <div class="tab" onclick="switchTab('guide')">Opas</div>
            <div class="tab" onclick="switchTab('settings')">Asetukset</div>
        </div>

        <!-- Agents-paneeli -->
        <div id="panel-agents" class="panel active">
            <AgentBar />
            <StatusBar />
            <Terminal />
        </div>

        <!-- Oppimispolku-paneeli -->
        <div id="panel-learn" class="panel">
            <div id="learn-empty" style="padding:40px;text-align:center;color:#8b949e">
                <p style="font-size:18px;margin-bottom:8px">Oppimispolku</p>
                <p>Käynnistä projekti nähdäksesi agenttien promptit ja tulokset askel askeleelta.</p>
            </div>
            <div id="learn-steps"></div>
        </div>

        <Editor />
        <Guide />
        <Settings />
    </div>

    <script is:inline>
    // === Teemajärjestelmä (gecko/forge/serpent) — oletus forge ===
    const KS_THEMES = [
        { id: 'gecko', icon: '\u{1F98E}' },
        { id: 'forge', icon: '\u{2699}\u{FE0F}' },
        { id: 'serpent', icon: '\u{1F40D}' }
    ];
    const KS_DEFAULT = 'forge';
    (function() {
        let saved = KS_DEFAULT;
        try { saved = localStorage.getItem('kipina-studio-theme') || KS_DEFAULT; } catch(_){}
        if (!KS_THEMES.find(t => t.id === saved)) saved = KS_DEFAULT;
        document.documentElement.setAttribute('data-theme', saved);
    })();

    // === Helpers ===
    window.showJoinDialog = function() {
        const d = document.getElementById('join-dialog');
        d.style.display = d.style.display === 'none' ? 'block' : 'none';
    };

    // === Teemapainike: kierrätys + hero-kuvan vaihto ===
    (function() {
        const btn = document.getElementById('theme-cycle-btn');
        const heroImg = document.querySelector('.hero-orb-img');
        function currentTheme() { return document.documentElement.getAttribute('data-theme') || KS_DEFAULT; }
        function applyTheme(themeId) {
            document.documentElement.setAttribute('data-theme', themeId);
            const t = KS_THEMES.find(x => x.id === themeId) || KS_THEMES[1];
            if (btn) btn.textContent = t.icon;
            if (heroImg) {
                heroImg.style.opacity = '0';
                setTimeout(() => { heroImg.src = '/' + themeId + '_hero.webp'; heroImg.style.opacity = '1'; }, 200);
            }
            try { localStorage.setItem('kipina-studio-theme', themeId); } catch(_){}
        }
        // Alkuasetus
        applyTheme(currentTheme());
        // Klikkaus kiertää seuraavaan
        if (btn) btn.addEventListener('click', () => {
            const cur = currentTheme();
            const idx = KS_THEMES.findIndex(t => t.id === cur);
            const next = KS_THEMES[(idx + 1) % KS_THEMES.length];
            applyTheme(next.id);
        });
    })();

    function esc(str) {
        if (!str) return '';
        return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
    }
    function highlightCode(code) {
        if (typeof hljs !== 'undefined') {
            try { return hljs.highlightAuto(code).value; } catch(e) {}
        }
        return esc(code);
    }

    // === Mekaaninen koodivalidointi (QA-stepin tueksi) ===
    function validateProjectCode(files) {
        const issues = [];
        const fileNames = Object.keys(files);

        for (const [fname, code] of Object.entries(files)) {
            if (!fname.endsWith('.py')) continue;
            const lines = code.split('\n');

            // 1. Relatiiviset importit
            for (const line of lines) {
                const m = line.match(/^from\s+\.(\w*)\s+import/);
                if (m) issues.push(`ISSUE: ${fname}: relatiivinen import "from .${m[1]}" — käytä absoluuttista: from ${m[1]} import ...`);
            }

            // 2. Projektin sisäiset importit — tarkista että importatut nimet löytyvät
            for (const line of lines) {
                const m = line.match(/^from\s+(models|schemas|main)\s+import\s+(.+)/);
                if (!m) continue;
                const srcFile = m[1] + '.py';
                const srcCode = files[srcFile];
                if (!srcCode) { issues.push(`ISSUE: ${fname}: importtaa "${m[1]}" mutta ${srcFile} puuttuu`); continue; }
                const names = m[2].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim());
                for (const name of names) {
                    if (name && !srcCode.includes(name)) {
                        issues.push(`ISSUE: ${fname}: importtaa "${name}" moduulista ${m[1]} mutta sitä ei löydy ${srcFile}:stä`);
                    }
                }
            }

            // 3. models.py: SQLite connect_args
            if (fname === 'models.py') {
                if (/sqlite/i.test(code) && !code.includes('check_same_thread'))
                    issues.push('ISSUE: models.py: SQLite create_engine puuttuu connect_args={"check_same_thread": False}');
            }

            // 4. schemas.py: date/datetime importit
            if (fname === 'schemas.py') {
                if (/:\s*date\b/.test(code) && !/from datetime import/.test(code))
                    issues.push('ISSUE: schemas.py: käyttää date-tyyppiä mutta "from datetime import date" puuttuu');
                if (/:\s*datetime\b/.test(code) && !/from datetime import/.test(code))
                    issues.push('ISSUE: schemas.py: käyttää datetime-tyyppiä mutta "from datetime import datetime" puuttuu');
            }

            // 4b. Python-syntaksi: JS-booleanit (false/true ilman isoa alkukirjainta)
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                if (/^\s*#/.test(line) || /^\s*$/.test(line)) continue;
                // Etsi false/true joita ei ole merkkijonon sisällä ja jotka eivät ole osa isompaa sanaa
                if (/(?<!["\w])false(?![\w"])/.test(line))
                    issues.push(`ISSUE: ${fname}:${i+1}: "false" ei ole Python — pitäisi olla "False"`);
                if (/(?<!["\w])true(?![\w"])/.test(line))
                    issues.push(`ISSUE: ${fname}:${i+1}: "true" ei ole Python — pitäisi olla "True"`);
            }

            // 5. test_main.py: ei saa uudelleenmääritellä appia tai modeleita
            if (fname === 'test_main.py') {
                if (/^app\s*=\s*FastAPI\s*\(/m.test(code))
                    issues.push('ISSUE: test_main.py: luo oman FastAPI()-instanssin — pitäisi importata main.py:stä');
                if (/^class\s+(Todo|User|Base)\b/m.test(code))
                    issues.push('ISSUE: test_main.py: uudelleenmäärittelee model-luokan — pitäisi importata models.py:stä');
            }

            // 6. main.py: FastAPI query param reitissä
            if (fname === 'main.py') {
                const routeMatches = code.matchAll(/@app\.\w+\(\s*["']([^"']+)["']/g);
                for (const rm of routeMatches) {
                    if (rm[1].includes('?') && rm[1].includes('{'))
                        issues.push(`ISSUE: main.py: reitti "${rm[1]}" sisältää query parametrin — FastAPI:ssa query params tulevat funktion parametreina`);
                }
            }
        }

        // 7. pyproject.toml: poetry
        if (files['pyproject.toml']) {
            const toml = files['pyproject.toml'];
            if (/\[tool\.poetry\]/.test(toml)) issues.push('ISSUE: pyproject.toml: sisältää [tool.poetry] — käytä vain [project] (PEP 621)');
            if (!/fastapi/i.test(toml)) issues.push('ISSUE: pyproject.toml: puuttuu fastapi riippuvuuksista');
            if (!/sqlalchemy/i.test(toml)) issues.push('ISSUE: pyproject.toml: puuttuu sqlalchemy riippuvuuksista');
        }

        // 8. Dockerfile: poetry
        if (files['Dockerfile']) {
            const df = files['Dockerfile'];
            if (/poetry/i.test(df)) issues.push('ISSUE: Dockerfile: käyttää Poetryä — pitäisi käyttää uv:tä');
        }

        return issues;
    }

    // === Landing → App siirtymä ===
    function startProject(task, skipValidation) {
        if (!task || !task.trim()) return;
        const trimmed = task.trim();
        if (!skipValidation && (trimmed.length < 10 || trimmed.split(/\s+/).length < 2)) {
            const input = document.getElementById('landing-input');
            if (input) {
                input.classList.add('shake');
                input.setAttribute('placeholder', 'Kuvaile tarkemmin, esim. "REST API käyttäjähallinnalle"');
                setTimeout(() => input.classList.remove('shake'), 500);
            }
            return;
        }
        document.getElementById('landing').classList.add('hidden');
        document.getElementById('app').classList.add('active');
        setTimeout(() => {
            if (typeof termExec === 'function') termExec(`kpn project "${trimmed}"`);
        }, 200);
    }

    window.showLanding = function() {
        document.getElementById('app').classList.remove('active');
        document.getElementById('landing').classList.remove('hidden');
    };

    // Landing-napit
    document.getElementById('landing-start')?.addEventListener('click', () => {
        startProject(document.getElementById('landing-input')?.value);
    });
    document.getElementById('landing-input')?.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') startProject(e.target.value);
    });
    document.querySelectorAll('.example-btn').forEach(btn => {
        btn.addEventListener('click', () => startProject(btn.dataset.prompt, true));
    });

    // === Oppimispolku — renderöinti ===
    window._lastPromptLog = null;

    function renderLearnView(promptLog) {
        window._lastPromptLog = promptLog;
        const container = document.getElementById('learn-steps');
        const empty = document.getElementById('learn-empty');
        if (!container) return;
        if (!promptLog || !promptLog.length) { container.innerHTML = ''; if (empty) empty.style.display = ''; return; }
        if (empty) empty.style.display = 'none';

        container.innerHTML = promptLog.map((e, i) => `
            <div class="learn-step">
                <div class="learn-step-header" onclick="this.nextElementSibling.classList.toggle('open')">
                    <div class="learn-step-num">${i}</div>
                    <span class="learn-step-agent">${esc(e.agentName)}</span>
                    <span class="learn-step-label">${esc(e.label)} · ${esc(e.model)}</span>
                </div>
                <div class="learn-step-body">
                    <div class="learn-section-title">System Prompt</div>
                    <pre class="learn-code">${esc(e.systemPrompt || '(ei system promptia)')}</pre>
                    <div class="learn-section-title">Syöte</div>
                    <pre class="learn-code">${esc(e.userPrompt)}</pre>
                    <div class="learn-section-title">Tulos</div>
                    <pre class="learn-code">${esc(e.response || '(ei tulosta)')}</pre>
                </div>
            </div>
        `).join('');
    }

    // === Globaalit tilat ===
    const defaultAgents = {
        client: { name: 'Client', avatar: '/avatars/kettu_notext.webp', model: 'qwen-coder', order: 0,
            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.

GIVEN a short project description from the user, produce a structured brief:

1. PROJECT NAME: a short, descriptive name
2. GOAL: one sentence explaining what the software does and who it's for
3. CORE FEATURES: numbered list of 3-8 concrete features (not vague wishes)
4. DATA MODEL: list the main entities and their key fields (include field types)
5. API ENDPOINTS: list the REST endpoints (method + path + purpose)
6. CONSTRAINTS: any technical constraints (e.g. "must use SQLite", "no auth needed")

RULES:
- Be specific: "User can filter todos by status" not "todo management"
- Use plain English, no code
- Maximum 400 words total` },
        manager: { name: 'Manager', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 1,
            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.

RULES:
- 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)
- Use pyproject.toml for Python dependencies (never requirements.txt)
- Maximum 8 files per project
- Only filenames, no directory paths
- Common patterns: models.py → schemas.py → main.py → pyproject.toml

EXAMPLE OUTPUT:
models.py: SQLAlchemy database models and engine setup
schemas.py: Pydantic request/response schemas
main.py: FastAPI application with CRUD endpoints
pyproject.toml: project dependencies` },
        coder: { name: 'Coder', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 2,
            temperature: 0.7, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
            prompt: `You are an expert Python developer. Write complete, production-ready code.

CRITICAL RULES:
1. Include ALL imports at the top of every file — including stdlib (from datetime import date, etc.)
2. Import from other project files: from models import Todo, SessionLocal
3. NEVER use relative imports (from .models) — ALWAYS absolute: from models import ...
4. Pydantic schemas use different names than SQLAlchemy models: TodoCreate, TodoResponse (not Todo)
5. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False})
6. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine)
7. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close()
8. Pydantic v2: use model_dump() not dict(), class Config: from_attributes = True
9. All CRUD endpoints: POST (201), GET list, GET by id, PUT, DELETE (204)

NEVER:
- Leave out any import (EVERY type you use must be imported)
- Use relative imports (from .models)
- Add explanations or comments
- Leave placeholder code or TODO comments
- Use Flask syntax (app.run) in FastAPI projects
- 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)` },
        data: { name: 'Data Engineer', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 3,
            temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
            prompt: `You are a database architect specializing in SQLAlchemy and relational databases.

YOUR RESPONSIBILITIES:
1. Design normalized database schemas with proper column types and constraints
2. Define SQLAlchemy models with __tablename__, primary keys, indexes, and relationships
3. Set up engine, SessionLocal, and Base in the same file (models.py)
4. Use String(length) not bare String for SQLite compatibility
5. Add nullable=False for required fields, unique=True where appropriate
6. Use Column(Integer, primary_key=True, index=True) for IDs
7. SQLite: create_engine(url, connect_args={"check_same_thread": False})

ENUM HANDLING (IMPORTANT):
- For status fields, use Column(String(20)) with a default value — simpler and SQLite-compatible
- Do NOT define Python Enum classes — use plain strings instead
- Example: status = Column(String(20), default="pending")

ALWAYS INCLUDE:
- from sqlalchemy import create_engine, Column, Integer, String
- from sqlalchemy.ext.declarative import declarative_base
- from sqlalchemy.orm import sessionmaker
- DATABASE_URL, engine, SessionLocal, Base
- create_engine with connect_args={"check_same_thread": False}` },
        qa: { name: 'QA', avatar: '/avatars/susi_notext.webp', model: 'qwen-coder', order: 4,
            temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
            prompt: `You are a QA engineer responsible for code review and automated testing.

CODE REVIEW CHECKLIST:
1. IMPORTS: Every "from X import Y" must match an actual export in file X
2. NAMES: Pydantic schemas (UserCreate) must not shadow SQLAlchemy models (User)
3. TYPES: All function parameters have type hints, return types specified
4. ERRORS: Every db query that can return None has a 404 check
5. RESOURCES: Database session uses yield+finally pattern (no leaks)
6. SECURITY: No raw SQL, no hardcoded secrets, inputs validated via Pydantic
7. ENDPOINTS: All CRUD operations exist (POST/GET/GET-by-id/PUT/DELETE)
8. MODELS: Pydantic Config has from_attributes=True, uses model_dump() not dict()
9. COMPLETENESS: No placeholder comments, no "TODO", no "pass" in handlers

WHEN REVIEWING:
- If all checks pass: respond "LGTM"
- If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague

WHEN WRITING TESTS:
- ALWAYS import app from main.py: from main import app, get_db
- ALWAYS import Base from models.py: from models import Base
- NEVER redefine the app, models, or routes in the test file
- Use file-based SQLite for test isolation: sqlite:///./test.db
- Override the get_db dependency to use test database
- Use TestClient from fastapi.testclient
- Test all CRUD: create (201), list (200), get by id (200/404), update (200), delete (204)
- Each test should create its own data, not depend on other tests` },
        tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 5,
            temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 1024,
            prompt: `You are a DevOps engineer specializing in containerization and deployment.

DOCKERFILE RULES:
- Use python:3.12-slim as base
- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
- ENV UV_CACHE_DIR=/tmp/uv-cache (MUST set before uv sync)
- Copy pyproject.toml first, then RUN uv sync, then COPY source files
- Set USER AFTER installing dependencies (uv sync needs write access)
- RUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache
- NEVER use pip, poetry, or requirements.txt
- Expose port 8000
- CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Write ONLY the Dockerfile, no explanations.` },
        observer: { name: 'Observer', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 6,
            temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
            prompt: `You are an independent technical observer and risk analyst.

EVALUATE THE PROJECT FOR:
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
2. SECURITY: SQL injection risks? Input validation? Authentication?
3. RELIABILITY: Error handling? Database connection management? Edge cases?
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?

OUTPUT FORMAT:
- RISK: [critical/high/medium/low] Description
- List max 3-5 most important findings
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"` },
    };
    // Versio: kasvata kun oletuspromptit muuttuvat
    const AGENTS_VERSION = 5;
    let agents;
    const savedVersion = parseInt(localStorage.getItem('kpn-agents-version') || '0');
    if (savedVersion < AGENTS_VERSION && localStorage.getItem('kpn-agents')) {
        // Uudet oletukset saatavilla — kysytään käyttäjältä
        if (confirm('Agenttien oletuspromptit on päivitetty. Haluatko ottaa uudet käyttöön?\n\n(OK = päivitä oletuksiin, Peruuta = säilytä omat muokkauksesi)')) {
            agents = JSON.parse(JSON.stringify(defaultAgents));
            localStorage.setItem('kpn-agents', JSON.stringify(agents));
        } else {
            agents = JSON.parse(localStorage.getItem('kpn-agents'));
        }
        localStorage.setItem('kpn-agents-version', String(AGENTS_VERSION));
    } else {
        agents = JSON.parse(localStorage.getItem('kpn-agents') || 'null') || JSON.parse(JSON.stringify(defaultAgents));
        localStorage.setItem('kpn-agents-version', String(AGENTS_VERSION));
    }
    function saveAgents() { localStorage.setItem('kpn-agents', JSON.stringify(agents)); }
    function getAgentModel(name) { const a = agents[name]; return a ? a.model : name; }

    // LLM-asetukset (localStorage-persistenssi)
    const defaultSettings = {
        systemPrompt: "You are a coding assistant. Respond with ONLY code. No explanations, no markdown fences, no 'Please note' text. Only working code with proper imports.",
        temperature: 0.7,
        topK: 40,
        repeatPenalty: 1.15,
        maxTokens: 1024,
        stopSequences: "\\n###\\n\\nExplanation\\nNote:\\nPlease note\\nThis is a basic\\n```\\n\\n\\n// Example\\n# Example",
        model: "qwen2.5-coder:3b",
    };
    let settings = JSON.parse(localStorage.getItem('kpn-settings') || 'null') || { ...defaultSettings };
    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 ===
    window.switchTab = function(tab) {
        document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        document.getElementById('panel-' + tab)?.classList.add('active');
        document.querySelector(`.tab[onclick*="${tab}"]`)?.classList.add('active');
        window.location.hash = tab;
        if (tab === 'editor') initMonaco();
    };
    setTimeout(() => {
        const initHash = window.location.hash.replace('#','');
        if (['editor','guide','learn'].includes(initHash)) switchTab(initHash);
    }, 0);

    // === Agent selection & config ===
    let selectedAgent = null;

    // Highlightaa aktiivinen agentti pipeline-aikana
    function highlightAgent(agentKey) {
        document.querySelectorAll('.agent-avatar').forEach(el => el.classList.remove('active'));
        if (agentKey) document.querySelector(`.agent-avatar[data-agent="${agentKey}"]`)?.classList.add('active');
    }

    function renderAgentBar() {
        const bar = document.getElementById('agent-bar');
        const sorted = Object.entries(agents).sort((a,b) => (a[1].order||0) - (b[1].order||0));
        bar.innerHTML = sorted.map(([key, a]) =>
            `<div class="agent-avatar${selectedAgent===key?' active':''}" data-agent="${key}" onclick="selectAgent('${key}')" draggable="true" ondragstart="dragAgent(event,'${key}')" ondragover="event.preventDefault()" ondrop="dropAgent(event,'${key}')" title="${esc(a.name)}">` +
            `<img src="${a.avatar}" alt="${esc(a.name)}">` +
            `<div class="avatar-name">${esc(a.name)}</div></div>`
        ).join('');
    }
    renderAgentBar();

    window.selectAgent = function(key) {
        selectedAgent = (selectedAgent === key) ? null : key; // Toggle
        renderAgentBar();
        const config = document.getElementById('agent-config');
        if (!selectedAgent) { config.style.display = 'none'; return; }

        // Tarkkailija-klikkaus: avaa raportti modalina jos se on generoitu
        if (key === 'observer' && window._lastReport) {
            config.style.display = 'none';
            showReportModal(window._lastReport);
            selectedAgent = null;
            renderAgentBar();
            return;
        }

        const a = agents[key];
        config.style.display = 'block';
        document.getElementById('config-avatar').src = a.avatar;
        document.getElementById('config-name').value = a.name;
        document.getElementById('config-role').textContent = key;
        document.getElementById('config-model').value = a.model;
        const promptEl = document.getElementById('config-prompt');
        promptEl.value = a.prompt || '';
        // Auto-resize: textarea kasvaa sisällön mukaan
        promptEl.style.height = 'auto';
        promptEl.style.height = promptEl.scrollHeight + 'px';

        // Sampling-parametrit
        const tempEl = document.getElementById('config-temperature');
        const tempValEl = document.getElementById('config-temp-val');
        const maxtokEl = document.getElementById('config-maxtokens');
        const maxtokValEl = document.getElementById('config-maxtok-val');
        const topkEl = document.getElementById('config-topk');
        const topkValEl = document.getElementById('config-topk-val');
        const repEl = document.getElementById('config-repeat');
        const repValEl = document.getElementById('config-rep-val');

        tempEl.value = a.temperature ?? 0.7; tempValEl.textContent = tempEl.value;
        maxtokEl.value = a.maxTokens ?? 1024; maxtokValEl.textContent = maxtokEl.value;
        topkEl.value = a.topK ?? 40; topkValEl.textContent = topkEl.value;
        repEl.value = a.repeatPenalty ?? 1.15; repValEl.textContent = repEl.value;

        // Pipeline-järjestys
        const pipeline = document.getElementById('config-pipeline');
        const sorted = Object.entries(agents).sort((a,b) => (a[1].order||0) - (b[1].order||0));
        pipeline.innerHTML = sorted.map(([k,ag]) =>
            `<span style="padding:3px 8px;border-radius:4px;font-size:11px;border:1px solid ${k===key?'var(--accent)':'var(--border)'};color:${k===key?'var(--accent)':'#8b949e'};cursor:grab" draggable="true" ondragstart="dragAgent(event,'${k}')" ondragover="event.preventDefault()" ondrop="dropAgent(event,'${k}')">${ag.name}</span>`
        ).join('');

        // Muutosten tallennus
        document.getElementById('config-name').oninput = () => { agents[key].name = document.getElementById('config-name').value; saveAgents(); renderAgentBar(); };
        document.getElementById('config-model').onchange = () => { agents[key].model = document.getElementById('config-model').value; saveAgents(); };
        promptEl.oninput = () => { agents[key].prompt = promptEl.value; saveAgents(); promptEl.style.height = 'auto'; promptEl.style.height = promptEl.scrollHeight + 'px'; };
        tempEl.oninput = () => { agents[key].temperature = +tempEl.value; tempValEl.textContent = tempEl.value; saveAgents(); };
        maxtokEl.oninput = () => { agents[key].maxTokens = +maxtokEl.value; maxtokValEl.textContent = maxtokEl.value; saveAgents(); };
        topkEl.oninput = () => { agents[key].topK = +topkEl.value; topkValEl.textContent = topkEl.value; saveAgents(); };
        repEl.oninput = () => { agents[key].repeatPenalty = +repEl.value; repValEl.textContent = repEl.value; saveAgents(); };
    };

    window.closeAgentConfig = function() { selectedAgent = null; document.getElementById('agent-config').style.display = 'none'; renderAgentBar(); };

    // Drag & drop järjestys
    let dragKey = null;
    window.dragAgent = function(e, key) { dragKey = key; e.dataTransfer.effectAllowed = 'move'; };
    window.dropAgent = function(e, targetKey) {
        e.preventDefault();
        if (!dragKey || dragKey === targetKey) return;
        const srcOrder = agents[dragKey].order;
        const tgtOrder = agents[targetKey].order;
        agents[dragKey].order = tgtOrder;
        agents[targetKey].order = srcOrder;
        saveAgents(); renderAgentBar();
        if (selectedAgent) selectAgent(selectedAgent); // Päivitä pipeline-näkymä
    };

    // Uuden agentin luonti
    window.addCustomAgent = function() {
        const id = 'custom_' + Date.now();
        const avatars = ['/avatars/bear.webp','/avatars/beaver.webp','/avatars/gecko.webp','/avatars/lion.webp','/avatars/penguin.webp','/avatars/spider.webp','/avatars/walrus.webp','/avatars/serpent.webp'];
        agents[id] = {
            name: 'Uusi agentti',
            avatar: avatars[Math.floor(Math.random() * avatars.length)],
            model: 'qwen-coder',
            prompt: '',
            order: Object.keys(agents).length,
        };
        saveAgents(); renderAgentBar(); selectAgent(id);
    };

    // Agentin poisto
    window.deleteAgent = function() {
        if (!selectedAgent || !agents[selectedAgent]) return;
        if (!confirm(`Poistetaanko ${agents[selectedAgent].name}?`)) return;
        delete agents[selectedAgent];
        saveAgents(); selectedAgent = null;
        document.getElementById('agent-config').style.display = 'none';
        renderAgentBar();
    };

    // === WebSocket ===
    const wsUrl = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`;
    const uiSocket = new WebSocket(wsUrl);
    window._uiSocket = uiSocket;

    uiSocket.onopen = async () => {
        document.getElementById('hub-dot').style.background = '#3fb950';
        document.getElementById('hub-label').textContent = 'Yhdistetty';
        document.getElementById('hub-label').style.color = '#3fb950';
        // Rekisteröidy viewerina
        uiSocket.send(JSON.stringify({
            type: 'auth', status: 'viewer', node_type: 'browser',
            platform: navigator.platform || '', cpu_cores: navigator.hardwareConcurrency || 0,
            device_memory_gb: navigator.deviceMemory || 0, allocated_gb: 0, selected_task: 'viewer',
        }));
        // Tarkistetaan onko natiivisolmu jo hubissa
        try {
            const res = await fetch('/api/v1/chat/completions', {
                method: 'POST', headers: {'Content-Type':'application/json'},
                body: JSON.stringify({ model: 'qwen-coder', prompt: 'ping', task_id: 'status-check' }),
                signal: AbortSignal.timeout(3000),
            });
            if (res.status !== 503) {
                // Solmu löytyi (natiivi tai Wasm)
                document.getElementById('compute-dot').style.background = '#3fb950';
                document.getElementById('compute-label').textContent = 'Valmis (natiivi)';
                document.getElementById('compute-label').style.color = '#3fb950';
                document.getElementById('compute-btn').textContent = '✓ Valmis';
                document.getElementById('compute-btn').className = 'btn btn-green';
                llmReady = true;
            }
        } catch(e) {
            // Timeout = solmu on olemassa mutta laskee — se on ok
            document.getElementById('compute-dot').style.background = '#3fb950';
            document.getElementById('compute-label').textContent = 'Valmis (natiivi)';
            document.getElementById('compute-label').style.color = '#3fb950';
            document.getElementById('compute-btn').textContent = '✓ Valmis';
            document.getElementById('compute-btn').className = 'btn btn-green';
            llmReady = true;
        }
    };
    uiSocket.onclose = () => {
        document.getElementById('hub-dot').style.background = '#f85149';
        document.getElementById('hub-label').textContent = 'Yhteys katkennut';
        document.getElementById('hub-label').style.color = '#f85149';
    };

    // === Terminal ===
    const termPanel = document.getElementById('terminal');
    const termInput = document.getElementById('term-input');
    const termHistory = [];
    let termHistIdx = -1;

    function termLog(html, color) {
        const div = document.createElement('div');
        div.className = 'terminal-line';
        if (color) div.style.color = color;
        div.innerHTML = html;
        termPanel.appendChild(div);
        while (termPanel.children.length > pipelineConfig.terminalMaxLines && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild);
        termPanel.scrollTop = termPanel.scrollHeight;
    }

    // === Wasm inference (main thread) ===
    let wasmReady = false;
    let wasmNodeStarted = false;
    let llmReady = false;

    async function ensureWasm() {
        if (wasmReady) return;
        const { default: init, start_agent_node, set_gpu_load } = await import('/pkg/node.js');
        window._wasmExports = { init, start_agent_node, set_gpu_load };
        termLog('  Ladataan WebAssembly...', '#d29922');
        await init();
        wasmReady = true;
        termLog('  <span style="color:#3fb950">✓</span> WebAssembly valmis');
    }

    async function ensureNode() {
        if (wasmNodeStarted) return;
        await ensureWasm();
        const { start_agent_node } = window._wasmExports;
        const deviceInfo = JSON.stringify({
            allocated_gb: 4,
            cpu_cores: navigator.hardwareConcurrency || 0,
            device_memory_gb: navigator.deviceMemory || 0,
            platform: navigator.platform || '',
            gpu: null,
            selected_task: 'qwen-coder-05b'
        });
        termLog('  Yhdistetään laskentasolmuna...', '#d29922');
        await start_agent_node(wsUrl, false, deviceInfo, 4);
        wasmNodeStarted = true;

        // Odotetaan WS-yhteyden avautumista (kuunnellaan console.log)
        await new Promise(resolve => {
            const origLog = console.log;
            const check = (...args) => {
                const msg = args.join(' ');
                if (msg.includes('Yhteys Hubiin avattu')) {
                    console.log = origLog;
                    resolve();
                }
            };
            console.log = function(...args) { origLog.apply(console, args); check(...args); };
            // Timeout 15s
            setTimeout(() => { console.log = origLog; resolve(); }, 15000);
        });

        document.getElementById('compute-dot').style.background = '#d29922';
        document.getElementById('compute-label').textContent = 'Yhdistetty';
        document.getElementById('compute-label').style.color = '#d29922';
        termLog('  <span style="color:#3fb950">✓</span> Laskentasolmu yhdistetty hubiin');
    }

    // Kuunnellaan console.log mallin latauksen etenemiselle
    const _origLog = console.log;
    console.log = function(...args) {
        _origLog.apply(console, args);
        const msg = args.join(' ');
        if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
            llmReady = true;
            document.getElementById('compute-dot').style.background = '#3fb950';
            document.getElementById('compute-label').textContent = 'Qwen2.5-Coder:0.5B';
            document.getElementById('compute-label').style.color = '#3fb950';
            const btn = document.getElementById('compute-btn');
            if (btn) { btn.textContent = '✓ Valmis'; btn.className = 'btn btn-green'; }
            localStorage.setItem('kpn-coder-loaded', 'true');
        }
    };

    // Compute-nappi
    document.getElementById('compute-btn')?.addEventListener('click', () => {
        const btn = document.getElementById('compute-btn');
        if (btn.textContent.includes('Valmis')) return;
        btn.textContent = 'Ladataan...';
        btn.className = 'btn btn-muted';
        ensureNode();
    });

    // Wasm-autostart vain jos natiivisolmua ei löydy (tarkistetaan onopen:ssa)

    // === Pipeline-keskeytys ===
    let pipelineAbort = null; // AbortController tai null

    // === kpnRun: lähettää promptin mallille ===
    const activeStreams = {};

    async function kpnRun(model, prompt, silent, agentOpts) {
        // Tarkistetaan keskeytys
        if (pipelineAbort?.signal?.aborted) return null;

        const taskId = crypto.randomUUID();
        const statusDiv = document.createElement('div');
        statusDiv.className = 'terminal-line';
        statusDiv.id = 'status-' + taskId;
        statusDiv.innerHTML = `  <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> processing...</span>`;
        termPanel.appendChild(statusDiv);
        termPanel.scrollTop = termPanel.scrollHeight;

        try {
            statusDiv.innerHTML = `  <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> processing...</span>`;

            // Rakennetaan pyyntö: agentin asetukset tai globaalit oletukset
            const opts = agentOpts || {};
            const payload = {
                model,
                prompt,
                task_id: taskId,
                system_prompt: opts.prompt || settings.systemPrompt || undefined,
                temperature: opts.temperature ?? settings.temperature ?? undefined,
                top_k: opts.topK ?? settings.topK ?? undefined,
                max_tokens: opts.maxTokens ?? settings.maxTokens ?? undefined,
                repeat_penalty: opts.repeatPenalty ?? settings.repeatPenalty ?? undefined,
                capability: opts.capability || undefined, // "heavy" → isoin malli
            };

            const res = await fetch('/api/v1/chat/completions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload),
                signal: pipelineAbort?.signal,
            });

            if (res.status === 503 && !wasmNodeStarted) {
                // Ei natiivisolmua — käynnistetään Wasm-fallback
                statusDiv.innerHTML = '  <span style="color:#d29922">→ Ei natiivisolmua — käynnistetään selainlaskenta...</span>';
                await ensureNode();
                for (let i = 0; i < 120 && !llmReady; i++) await new Promise(r => setTimeout(r, 500));
                if (!llmReady) { statusDiv.innerHTML = '  <span style="color:#f85149">✗ Mallin lataus aikakatkaistiin</span>'; return null; }
                // Yritetään uudelleen
                return kpnRun(model, prompt, silent);
            }
            if (!res.ok) {
                const err = await res.text().catch(() => res.statusText);
                statusDiv.innerHTML = `  <span style="color:#f85149">✗ ${esc(err)}</span>`;
                return null;
            }
            const data = await res.json();
            const response = (data.response || '').trim();
            const tokGen = data.tokens_generated || 0;
            const durS = data.duration_ms ? (data.duration_ms / 1000).toFixed(1) + 's' : '';
            const tokS = data.tokens_per_sec ? data.tokens_per_sec.toFixed(1) + ' tok/s' : '';
            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) {

            }
            return response;
        } catch(e) {
            statusDiv.innerHTML = `  <span style="color:#f85149">✗ ${esc(e.message)}</span>`;
            return null;
        } finally {
            if (activeStreams[taskId]) { activeStreams[taskId].remove(); delete activeStreams[taskId]; }
        }
    }

    // === WebSocket message handler ===
    uiSocket.onmessage = (event) => {
        try {
            const data = JSON.parse(event.data);
            if (data.type === 'stats') {
                document.getElementById('hub-version').textContent = 'v' + (data.version || '?');
            } else if (data.type === 'task_routed') {
                const statusDiv = document.getElementById('status-' + data.task_id);
                if (statusDiv) {
                    const color = data.status === 'queued' ? '#d29922' : '#8b949e';
                    statusDiv.innerHTML = `  <span style="color:${color}">${data.status === 'queued' ? '⏳' : '→'} ${esc(data.message)}</span>`;
                }
            } else if (data.type === 'llm_chunk' && data.task_id && activeStreams[data.task_id]) {
                const el = activeStreams[data.task_id].querySelector('.stream-content');
                if (el) { el.textContent += data.token || ''; termPanel.scrollTop = termPanel.scrollHeight; }
            }
        } catch(e) {}
    };

    // === Terminal commands ===
    const kpnCommands = {
        'kpn': ['help','run','project','pipeline','stop','load','status','models','clear'],
        'kpn run': ['coder','coder-3b','manager','tester','qa','qwen-coder'],
        'kpn load': ['1','2'],
        'kpn project': ['"'],
        'kpn pipeline': ['"'],
    };
    const kpnExamples = {
        'kpn run coder': ['"hello world in python"','"fibonacci in rust"','"quicksort in javascript"'],
        'kpn run coder-3b': ['"REST API with Flask"','"binary search tree"'],
        'kpn project': ['"FastAPI + SQLite REST API"','"UWB indoor positioning analytics — CSV cart tracking data, heatmaps, statistics, Docker (MariaDB + Jupyter)"','"CLI tool for CSV processing"'],
        'kpn pipeline': ['"todo-sovellus"','"laskin pythonilla"'],
    };

    // Tab completion
    function getCompletions(val) {
        const words = val.trimEnd().split(/\s+/);
        for (let d = words.length; d >= 1; d--) {
            const prefix = words.slice(0,d).join(' ');
            const partial = words[d] || '';
            if (kpnExamples[prefix] && !partial) return { items: kpnExamples[prefix], prefix: prefix + ' ' };
            const cands = kpnCommands[prefix];
            if (cands) {
                const m = partial ? cands.filter(c => c.startsWith(partial)) : cands;
                if (m.length > 0) return { items: m, prefix: prefix + ' ' };
            }
        }
        if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' };
        return { items: [], prefix: val };
    }

    // Dropdown
    const dropdown = document.getElementById('term-dropdown');
    let ddItems = [], ddIdx = -1, ddPrefix = '';

    function showDD(items, prefix) {
        if (!items.length) { hideDD(); return; }
        ddItems = items; ddPrefix = prefix; ddIdx = -1;
        dropdown.innerHTML = items.map((item,i) =>
            `<div class="dd-item" data-i="${i}" onclick="selectDD()">${esc(item)}</div>`
        ).join('');
        dropdown.style.display = 'block';
        dropdown.querySelectorAll('.dd-item').forEach(el => {
            el.addEventListener('mouseenter', () => highlightDD(+el.dataset.i));
        });
    }
    function hideDD() { dropdown.style.display = 'none'; ddItems = []; ddIdx = -1; }
    function highlightDD(i) {
        ddIdx = i;
        dropdown.querySelectorAll('.dd-item').forEach((el,j) => el.classList.toggle('active', j===i));
        dropdown.children[i]?.scrollIntoView({ block: 'nearest' });
    }
    window.selectDD = function() {
        if (ddIdx >= 0 && ddIdx < ddItems.length)
            termInput.value = ddPrefix + ddItems[ddIdx] + (ddItems[ddIdx].startsWith('"') ? '' : ' ');
        hideDD(); termInput.focus();
    };

    // Agenttiprompti-mapping
    const agentModels = new Proxy({}, { get: (_, key) => getAgentModel(key) });

    function termExec(cmd) {
        termLog(`<span class="terminal-prompt">$</span> ${esc(cmd)}`);
        termHistory.unshift(cmd); termHistIdx = -1;
        const parts = cmd.trim().split(/\s+/);
        if (parts[0] !== 'kpn') { termLog('  Tuntematon komento. Kokeile: kpn help', '#f85149'); return; }
        const sub = parts[1];

        if (sub === 'help' || !sub) {
            termLog('  kpn run &lt;malli&gt; "prompti"  — aja tehtävä', '#a5d6ff');
            termLog('  kpn project "kuvaus"        — monivaiheinen projekti', '#a5d6ff');
            termLog('  kpn pipeline "tehtävä"      — nopea: asiakas→manageri→koodari→testaaja', '#a5d6ff');
            termLog('  kpn stop                    — keskeytä pipeline', '#a5d6ff');
            termLog('  kpn load                    — lataa kielimalli', '#a5d6ff');
            termLog('  kpn models                  — mallit', '#a5d6ff');
            termLog('  kpn status                  — verkon tila', '#a5d6ff');
            termLog('  kpn clear                   — tyhjennä', '#a5d6ff');
        } else if (sub === 'stop') {
            if (pipelineAbort) {
                pipelineAbort.abort();
                pipelineAbort = null;
                termLog('  ✋ Pipeline keskeytetty', '#d29922');
                highlightAgent(null);
            } else {
                termLog('  Ei käynnissä olevaa pipelinea', '#8b949e');
            }
        } else if (sub === 'clear') { termPanel.innerHTML = '';
        } else if (sub === 'load') {
            const btn = document.getElementById('compute-btn');
            if (btn && btn.textContent.includes('Valmis')) { termLog('  ✓ Malli jo ladattu', '#3fb950'); }
            else { btn?.click(); }
        } else if (sub === 'models') {
            termLog('  <span style="color:var(--accent)">1</span>  qwen-coder     Qwen2.5-Coder:0.5B  <span style="color:#8b949e">~990 MB (selain)</span>');
            termLog('  <span style="color:var(--accent)">2</span>  qwen-coder-3b  Qwen2.5-Coder:3B    <span style="color:#8b949e">~6.2 GB (Ollama)</span>');
        } else if (sub === 'status') {
            termLog(`  Hub: ${document.getElementById('hub-label').textContent} | Laskenta: ${document.getElementById('compute-label').textContent}`, '#a5d6ff');
        } else if (sub === 'run') {
            let model = parts[2];
            const after = cmd.replace(/^kpn\s+run\s+\S+\s*/, '');
            const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
            const prompt = (m && (m[1]||m[2]||m[3]||'')).trim();
            if (!model || !prompt) { termLog('  Käyttö: kpn run &lt;malli&gt; "prompti"', '#f85149'); return; }
            if (agentModels[model]) model = agentModels[model];
            kpnRun(model, prompt);
        } else if (sub === 'project') {
            const after = cmd.replace(/^kpn\s+project\s*/, '');
            const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
            const task = (m && (m[1]||m[2]||m[3]||'')).trim();
            if (!task) { termLog('  Käyttö: kpn project "kuvaus"', '#f85149'); return; }
            kpnProject(task);
        } else if (sub === 'pipeline') {
            const after = cmd.replace(/^kpn\s+pipeline\s*/, '');
            const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
            const task = (m && (m[1]||m[2]||m[3]||'')).trim();
            if (!task) { termLog('  Käyttö: kpn pipeline "tehtävä"', '#f85149'); return; }
            kpnPipelineSimple(task);
        } else { termLog(`  Tuntematon: ${sub}. Kokeile: kpn help`, '#f85149'); }
    }

    // Input handler
    termInput?.addEventListener('keydown', (e) => {
        if (dropdown.style.display === 'block') {
            if (e.key === 'ArrowDown') { e.preventDefault(); highlightDD(Math.min(ddIdx+1, ddItems.length-1)); return; }
            if (e.key === 'ArrowUp') { e.preventDefault(); highlightDD(Math.max(ddIdx-1, 0)); return; }
            if ((e.key === 'Enter' || e.key === 'Tab') && ddIdx >= 0) { e.preventDefault(); selectDD(); return; }
            if (e.key === 'Escape') { e.preventDefault(); hideDD(); return; }
        }
        if (e.key === 'Tab' && e.shiftKey) {
            e.preventDefault(); hideDD();
            const val = termInput.value.trimEnd();
            if (!val) return;
            const qm = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/);
            if (qm) termInput.value = (qm[1]||qm[2]).trimEnd() + ' ';
            else { const ls = val.lastIndexOf(' '); termInput.value = ls > 0 ? val.substring(0, ls+1) : ''; }
        } else if (e.key === 'Tab') {
            e.preventDefault();
            const { items, prefix } = getCompletions(termInput.value);
            if (items.length === 1) { termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' '); hideDD(); }
            else if (items.length > 1) showDD(items, prefix);
        } else if (e.key === 'Enter') {
            hideDD();
            const cmd = termInput.value.trim();
            if (cmd) termExec(cmd);
            termInput.value = '';
        } else if (e.key === 'ArrowUp') { e.preventDefault(); if (termHistIdx < termHistory.length-1) { termHistIdx++; termInput.value = termHistory[termHistIdx]; }
        } else if (e.key === 'ArrowDown') { e.preventDefault(); if (termHistIdx > 0) { termHistIdx--; termInput.value = termHistory[termHistIdx]; } else { termHistIdx=-1; termInput.value=''; }
        }
    });
    termPanel?.addEventListener('click', () => termInput?.focus());
    document.addEventListener('click', (e) => { if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDD(); });

    // === Template-pohjainen projektipipeline ===
    let templates = {};
    const TEMPLATE_FILES = ['fastapi-crud.json', 'data-analytics.json'];

    // Ladataan mallipohjat
    (async () => {
        for (const file of TEMPLATE_FILES) {
            try {
                const res = await fetch(`/templates/${file}`);
                if (res.ok) { const t = await res.json(); templates[t.name] = t; }
            } catch(e) {}
        }
    })();

    // Valitaan mallipohja Asiakkaan briefin perusteella (keywords-match)
    function selectTemplate(brief) {
        const lower = brief.toLowerCase();
        let best = null, bestScore = 0;
        for (const t of Object.values(templates)) {
            const keywords = t.keywords || [];
            const score = keywords.filter(k => lower.includes(k)).length;
            if (score > bestScore) { bestScore = score; best = t; }
        }
        return best; // null = vapaa tila
    }

    function explainStep(title, explanation) {
        termLog(`\n  <span style="color:#a371f7;font-size:12px">💡 ${esc(title)}</span>`);
        termLog(`  <span style="color:#8b949e;font-size:12px">${esc(explanation)}</span>`);
    }

    // --- CrewAI-yhteensopiva output ---
    function generateCrewAIFiles(promptLog, task) {
        const crewFiles = {};

        // Kerätään uniikit agentit
        const agentMap = {};
        for (const entry of promptLog) {
            if (!agentMap[entry.agentKey]) {
                const firstSentence = (entry.systemPrompt || '').split(/\.\s/)[0].trim();
                agentMap[entry.agentKey] = {
                    name: entry.agentName,
                    model: entry.model,
                    systemPrompt: entry.systemPrompt || '',
                    goal: firstSentence.replace(/^You are (a |an )?/i, '').trim() || entry.label,
                };
            }
        }

        // agents.yaml
        let ay = '# Agents — Kipinä Agentic Studio → CrewAI\n';
        for (const [key, a] of Object.entries(agentMap)) {
            ay += `\n${key}:\n`;
            ay += `  role: >-\n    ${a.name}\n`;
            ay += `  goal: >-\n    ${a.goal}\n`;
            ay += `  backstory: |\n`;
            for (const line of a.systemPrompt.split('\n')) {
                ay += `    ${line}\n`;
            }
            ay += `  llm: ${a.model}\n`;
        }
        crewFiles['agents.yaml'] = ay;

        // tasks.yaml
        let ty = '# Tasks — Kipinä Agentic Studio → CrewAI\n';
        for (let i = 0; i < promptLog.length; i++) {
            const e = promptLog[i];
            const name = `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`;
            ty += `\n${name}:\n`;
            ty += `  description: |\n`;
            const descLines = e.userPrompt.split('\n').slice(0, pipelineConfig.crewaiPromptLines);
            for (const line of descLines) {
                ty += `    ${line}\n`;
            }
            if (e.userPrompt.split('\n').length > pipelineConfig.crewaiPromptLines) ty += `    # ... (truncated)\n`;
            ty += `  expected_output: >-\n    ${e.label}\n`;
            ty += `  agent: ${e.agentKey}\n`;
        }
        crewFiles['tasks.yaml'] = ty;

        // crew.py
        const agentKeys = Object.keys(agentMap);
        const taskNames = promptLog.map((e, i) => `step_${i}_${e.label.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`);
        let py = `"""${task}\n\nCrewAI crew — generated by Kipinä Agentic Studio.\nRun: crewai run\n"""\n\n`;
        py += `from crewai import Agent, Crew, Process, Task\n`;
        py += `from crewai.project import CrewBase, agent, crew, task\n\n\n`;
        py += `@CrewBase\nclass ProjectCrew:\n    """${task}"""\n\n`;
        py += `    agents_config = "agents.yaml"\n    tasks_config = "tasks.yaml"\n\n`;
        for (const key of agentKeys) {
            py += `    @agent\n    def ${key}(self) -> Agent:\n        return Agent(config=self.agents_config["${key}"])\n\n`;
        }
        for (const name of taskNames) {
            py += `    @task\n    def ${name}(self) -> Task:\n        return Task(config=self.tasks_config["${name}"])\n\n`;
        }
        py += `    @crew\n    def crew(self) -> Crew:\n        return Crew(\n`;
        py += `            agents=self.agents,\n            tasks=self.tasks,\n`;
        py += `            process=Process.sequential,\n            verbose=True,\n        )\n`;
        crewFiles['crew.py'] = py;

        // prompts/*.md — jokaisen vaiheen system prompt + syöte + tulos
        for (let i = 0; i < promptLog.length; i++) {
            const e = promptLog[i];
            let md = `# ${i} — ${e.agentName} (${e.agentKey}) — ${e.label}\n\n`;
            md += `**Malli:** \`${e.model}\`\n\n`;
            md += `## System Prompt\n\n\`\`\`\n${e.systemPrompt || '(ei system promptia)'}\n\`\`\`\n\n`;
            md += `## Syöte\n\n\`\`\`\n${e.userPrompt}\n\`\`\`\n\n`;
            md += `## Tulos\n\n\`\`\`\n${e.response || '(ei tulosta)'}\n\`\`\`\n`;
            const safe = e.label.replace(/[^a-z0-9._-]/gi, '_').toLowerCase();
            crewFiles[`prompts/${i}_${e.agentKey}_${safe}.md`] = md;
        }

        return crewFiles;
    }

    // === Template Pipeline — rakennuspalaset ===

    // JS-arvo → Python-literaali: false→False, true→True, "teksti"→'"teksti"'
    function pyLiteral(val) {
        if (val === true) return 'True';
        if (val === false) return 'False';
        if (val === null || val === undefined) return 'None';
        if (typeof val === 'string') return `"${val}"`;
        return String(val);
    }

    // JSON-merkkijono jossa booleanit on Python-muodossa: {"a": True, "b": False}
    function pyJsonLiteral(obj) {
        const parts = Object.entries(obj).map(([k, v]) => {
            let pyVal;
            if (v === true) pyVal = 'True';
            else if (v === false) pyVal = 'False';
            else if (v === null) pyVal = 'None';
            else if (typeof v === 'string') pyVal = `"${v}"`;
            else pyVal = String(v);
            return `"${k}":${pyVal}`;
        });
        return '{' + parts.join(',') + '}';
    }

    const SPEC_SYSTEM = `You are a software architect who designs database schemas for Python web applications.

THINK STEP BY STEP before outputting JSON:
1. What are the main ENTITIES (nouns) in this project?
2. What FIELDS does each entity need? (name, type, required?)
3. Which entities REFERENCE each other? (e.g. "a Book belongs to an Author" → Book has author_id)
4. Are there Date/DateTime fields? → add extra_imports

Then output ONLY valid JSON (no explanations before or after).

SCHEMA:
{"project_name":"short-name","description":"One sentence","entities":[{"name":"EntityName","table_name":"entity_names","fields":[{"name":"field_name","sa_type":"String(255)","py_type":"str","nullable":false,"default":null}]}],"relationships":[{"from":"ChildEntity","field":"parent_id","to":"ParentEntity","type":"many-to-one"}],"extra_imports":[]}

FIELD RULES:
- sa_type: String(N), Text, Integer, Date, DateTime, Boolean, Float
- py_type: str, int, float, bool, date, datetime — append " | None" if nullable
- Status fields: use String(20) with default value, NEVER Enum
- Every entity gets "id" automatically — do NOT add id or redundant ID fields
- Use snake_case for field names

RELATIONSHIP RULES:
- If entity A "belongs to" entity B → A has b_id field (Integer, nullable=false) + relationship entry
- EVERY _id field MUST have a matching relationship entry
- Parent entities must appear BEFORE children in the entities array
- If no relationships, set "relationships": []

AVOID: redundant ID fields, generic names, more than 7 fields or 3 entities, non-English entity/field names (ALWAYS English even if description is Finnish)

EXAMPLES (adapt, don't copy):
Todo app → Todo: title(str), description(Text|None), due_date(Date|None), status(String20="pending")
E-commerce → Customer: name,email / Product: name,price(Float),stock(Int) / Order: customer_id→Customer, product_id→Product, quantity(Int), order_date(Date)
Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_id→Author, published_at(DateTime|None), status(String20="draft")`;

    function extractJson(text) {
        const m = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
        if (m) text = m[1].trim();
        let depth = 0, start = null;
        for (let i = 0; i < text.length; i++) {
            if (text[i] === '{') { if (depth === 0) start = i; depth++; }
            else if (text[i] === '}') { depth--; if (depth === 0 && start !== null) { try { return JSON.parse(text.slice(start, i+1)); } catch(e) { continue; } } }
        }
        return null;
    }

    function tmplModels(spec) {
        // Kerää tarvittavat SA-tyypit + ForeignKey jos relaatioita
        const saTypes = new Set(['Integer']);
        for (const e of spec.entities) for (const f of e.fields) saTypes.add(f.sa_type.match(/^(\w+)/)[1]);
        const relMap = {};
        for (const r of (spec.relationships || [])) {
            const target = spec.entities.find(e => e.name === r.to);
            if (target) relMap[`${r.from}.${r.field}`] = target.table_name;
        }
        const hasFk = Object.keys(relMap).length > 0;
        if (hasFk) saTypes.add('ForeignKey');
        const imports = [...saTypes].sort().join(', ');
        let code = `from sqlalchemy import create_engine, Column, ${imports}\n`;
        code += `from sqlalchemy.orm import declarative_base, sessionmaker\n\n`;
        code += `DATABASE_URL = "sqlite:///./app.db"\n`;
        code += `engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\n`;
        code += `SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n`;
        for (const e of spec.entities) {
            code += `class ${e.name}(Base):\n    __tablename__ = "${e.table_name}"\n    id = Column(Integer, primary_key=True, index=True)\n`;
            for (const f of e.fields) {
                const fkTarget = relMap[`${e.name}.${f.name}`];
                let parts = fkTarget ? [`Column(${f.sa_type}, ForeignKey("${fkTarget}.id")` ] : [`Column(${f.sa_type}`];
                if (!f.nullable) parts.push('nullable=False');
                if (f.default !== null && f.default !== undefined) parts.push(`default=${pyLiteral(f.default)}`);
                code += `    ${f.name} = ${parts.join(', ')})\n`;
            }
            code += '\n';
        }
        code += 'Base.metadata.create_all(bind=engine)\n';
        return code;
    }

    function tmplSchemas(spec) {
        // Tunnista tarvittavat datetime-importit kenttätyypeistä
        const dtTypes = new Set();
        for (const e of spec.entities) for (const f of e.fields) {
            if (/\bdate\b/i.test(f.py_type) && !/datetime/.test(f.py_type)) dtTypes.add('date');
            if (/\bdatetime\b/i.test(f.py_type)) dtTypes.add('datetime');
        }

        let code = 'from pydantic import BaseModel, ConfigDict\n';
        if (dtTypes.size > 0) code += `from datetime import ${[...dtTypes].sort().join(', ')}\n`;

        // extra_imports: suodata pois pelkät nimet kuten "datetime" (jo käsitelty yllä)
        for (const imp of (spec.extra_imports || [])) {
            if (/^(date|datetime)$/.test(imp.trim())) continue; // käsitelty jo
            if (/^from\s/.test(imp) || /^import\s/.test(imp)) code += imp + '\n';
        }
        code += '\n';

        for (const e of spec.entities) {
            code += `class ${e.name}Create(BaseModel):\n`;
            for (const f of e.fields) {
                if (f.default !== null && f.default !== undefined) code += `    ${f.name}: ${f.py_type} = ${pyLiteral(f.default)}\n`;
                else if (f.nullable && f.py_type.includes('None')) code += `    ${f.name}: ${f.py_type} = None\n`;
                else code += `    ${f.name}: ${f.py_type}\n`;
            }
            code += `\nclass ${e.name}Response(${e.name}Create):\n    id: int\n    model_config = ConfigDict(from_attributes=True)\n\n`;
        }
        return code;
    }

    function tmplMain(spec) {
        const modelNames = spec.entities.map(e => e.name).join(', ');
        const createNames = spec.entities.map(e => e.name+'Create').join(', ');
        const responseNames = spec.entities.map(e => e.name+'Response').join(', ');
        let code = `from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\n`;
        code += `from models import Base, engine, SessionLocal, ${modelNames}\nfrom schemas import ${createNames}, ${responseNames}\n\n`;
        code += `app = FastAPI()\n\ndef get_db():\n    db = SessionLocal()\n    try:\n        yield db\n    finally:\n        db.close()\n\n`;
        for (const e of spec.entities) {
            const lo = e.name.toLowerCase(), tb = e.table_name;
            code += `@app.post("/${tb}/", response_model=${e.name}Response, status_code=201)\n`;
            code += `def create_${lo}(item: ${e.name}Create, db: Session = Depends(get_db)):\n`;
            code += `    db_item = ${e.name}(**item.model_dump())\n    db.add(db_item)\n    db.commit()\n    db.refresh(db_item)\n    return db_item\n\n`;
            code += `@app.get("/${tb}/", response_model=list[${e.name}Response])\n`;
            code += `def list_${lo}s(db: Session = Depends(get_db)):\n    return db.query(${e.name}).all()\n\n`;
            code += `@app.get("/${tb}/{item_id}", response_model=${e.name}Response)\n`;
            code += `def get_${lo}(item_id: int, db: Session = Depends(get_db)):\n`;
            code += `    item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
            code += `    if not item:\n        raise HTTPException(status_code=404, detail="${e.name} not found")\n    return item\n\n`;
            code += `@app.put("/${tb}/{item_id}", response_model=${e.name}Response)\n`;
            code += `def update_${lo}(item_id: int, item: ${e.name}Create, db: Session = Depends(get_db)):\n`;
            code += `    db_item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
            code += `    if not db_item:\n        raise HTTPException(status_code=404, detail="${e.name} not found")\n`;
            code += `    for key, value in item.model_dump().items():\n        setattr(db_item, key, value)\n    db.commit()\n    db.refresh(db_item)\n    return db_item\n\n`;
            code += `@app.delete("/${tb}/{item_id}", status_code=204)\n`;
            code += `def delete_${lo}(item_id: int, db: Session = Depends(get_db)):\n`;
            code += `    db_item = db.query(${e.name}).filter(${e.name}.id == item_id).first()\n`;
            code += `    if not db_item:\n        raise HTTPException(status_code=404, detail="${e.name} not found")\n    db.delete(db_item)\n    db.commit()\n\n`;
        }
        return code;
    }

    function tmplTests(spec) {
        let code = `from fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n`;
        code += `from main import app, get_db\nfrom models import Base\n\n`;
        code += `TEST_DB = "sqlite:///./test.db"\ntest_engine = create_engine(TEST_DB, connect_args={"check_same_thread": False})\n`;
        code += `TestSession = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)\nBase.metadata.create_all(bind=test_engine)\n\n`;
        code += `def override_get_db():\n    db = TestSession()\n    try:\n        yield db\n    finally:\n        db.close()\n\n`;
        code += `app.dependency_overrides[get_db] = override_get_db\nclient = TestClient(app)\n\n`;
        for (const e of spec.entities) {
            const lo = e.name.toLowerCase(), tb = e.table_name;
            const testData = {};
            for (const f of e.fields) {
                if (f.default !== null && f.default !== undefined) { testData[f.name] = f.default; continue; }
                if (f.py_type.includes('str')) testData[f.name] = `Test ${f.name}`;
                else if (f.py_type.includes('int')) testData[f.name] = 1;
                else if (f.py_type.includes('float')) testData[f.name] = 1.0;
                else if (f.py_type.includes('bool')) testData[f.name] = true;
                else if (f.py_type.includes('date')) testData[f.name] = '2024-01-15';
            }
            const td = pyJsonLiteral(testData);
            const firstStr = e.fields.find(f => f.py_type.includes('str') && f.name !== 'status');
            const updateData = {...testData};
            if (firstStr) updateData[firstStr.name] = `Updated ${firstStr.name}`;
            const ud = pyJsonLiteral(updateData);

            code += `def test_create_${lo}():\n    response = client.post('/${tb}/', json=${td})\n    assert response.status_code == 201\n    assert 'id' in response.json()\n\n`;
            code += `def test_list_${lo}s():\n    client.post('/${tb}/', json=${td})\n    response = client.get('/${tb}/')\n    assert response.status_code == 200\n    assert len(response.json()) >= 1\n\n`;
            code += `def test_get_${lo}_by_id():\n    created = client.post('/${tb}/', json=${td}).json()\n    item_id = created['id']\n    response = client.get(f'/${tb}/{item_id}')\n    assert response.status_code == 200\n    assert response.json()['id'] == item_id\n\n`;
            code += `def test_get_${lo}_not_found():\n    response = client.get('/${tb}/99999')\n    assert response.status_code == 404\n\n`;
            code += `def test_update_${lo}():\n    created = client.post('/${tb}/', json=${td}).json()\n    item_id = created['id']\n    response = client.put(f'/${tb}/{item_id}', json=${ud})\n    assert response.status_code == 200\n\n`;
            code += `def test_delete_${lo}():\n    created = client.post('/${tb}/', json=${td}).json()\n    item_id = created['id']\n    response = client.delete(f'/${tb}/{item_id}')\n    assert response.status_code == 204\n    response = client.get(f'/${tb}/{item_id}')\n    assert response.status_code == 404\n\n`;
        }
        return code;
    }

    function tmplPyproject(spec) {
        const name = (spec.project_name || 'app').toLowerCase().replace(/\s+/g, '-');
        return `[project]\nname = "${name}"\nversion = "0.1.0"\nrequires-python = ">=3.11"\ndependencies = [\n    "fastapi",\n    "uvicorn[standard]",\n    "sqlalchemy",\n    "pytest",\n    "httpx",\n]\n`;
    }

    function tmplDockerfile() {
        return `FROM python:3.12-slim\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\nENV UV_CACHE_DIR=/tmp/uv-cache\nWORKDIR /app\nCOPY pyproject.toml .\nRUN uv sync\nCOPY *.py .\nRUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache\nUSER appuser\nEXPOSE 8000\nCMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]\n`;
    }

    function tmplDockerCompose(spec) {
        const name = (spec.project_name || 'app').toLowerCase().replace(/[^a-z0-9-]/g, '-');
        return `services:\n  app:\n    build: .\n    container_name: ${name}\n    ports:\n      - "8000:8000"\n    volumes:\n      - app-data:/app/data\n    restart: unless-stopped\n\nvolumes:\n  app-data:\n`;
    }

    function tmplDockerignore() {
        return `.venv\n__pycache__\n*.pyc\n*.db\n.pytest_cache\n.git\n`;
    }

    function tmplGenerate(spec) {
        return {
            'models.py': tmplModels(spec),
            'schemas.py': tmplSchemas(spec),
            'main.py': tmplMain(spec),
            'test_main.py': tmplTests(spec),
            'pyproject.toml': tmplPyproject(spec),
            'Dockerfile': tmplDockerfile(),
            'docker-compose.yml': tmplDockerCompose(spec),
            '.dockerignore': tmplDockerignore(),
        };
    }

    async function kpnProject(task) {
        pipelineAbort = new AbortController();
        const promptLog = [];
        const cli = agents.client || Object.values(agents)[0];
        const mgr = agents.manager || Object.values(agents)[1];
        const cdr = agents.coder || Object.values(agents)[2];

        // Asiakas: jalostaa vaatimukset
        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> — Requirements Definition`);
        highlightAgent('client');
        explainStep('Requirements Definition', `${cli.name} forms clear requirements: features, models, endpoints.`);
        const brief = await kpnRun(cli.model, `${task}`, false, cli);
        if (!brief) { termLog('  ✗ Requirements definition failed', '#f85149'); return; }
        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">Requirements ready → Manager</span>`);

        // === Vaihe 2: JSON-speksi vaatimuksista ===
        termLog(`\n<span style="color:#d29922;font-weight:bold">[1] ${esc(mgr.name)}</span> — JSON-speksi`);
        highlightAgent('manager');
        explainStep('Arkkitehtuuri', `${mgr.name} analysoi vaatimukset ja tuottaa JSON-speksin: entiteetit, kentät, tyypit.`);

        const specRaw = await kpnRun(mgr.model, `${brief}\n\nOutput a JSON spec for this project.`, false, { ...mgr, prompt: SPEC_SYSTEM, capability: 'heavy' });
        const spec = specRaw ? extractJson(specRaw) : null;
        promptLog.push({ step: 1, agentKey: 'manager', agentName: mgr.name, model: mgr.model, label: 'JSON-speksi', systemPrompt: SPEC_SYSTEM, userPrompt: brief, response: specRaw || '' });

        if (!spec || !spec.entities || spec.entities.length === 0) {
            termLog('  ✗ JSON-speksi epäonnistui — fallback vapaaseen generointiin', '#f85149');
            // TODO: fallback vanhaan pipeline-logiikkaan
            return;
        }

        termLog(`  <span style="color:#3fb950">✓ ${spec.entities.length} entiteettiä: ${spec.entities.map(e => e.name).join(', ')}</span>`);

        // === Vaihe 3: Koodigenerointi templateista ===
        const files = tmplGenerate(spec);
        const fileOrder = Object.keys(files);
        const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester', 'docker-compose.yml': 'tester', '.dockerignore': 'tester' };
        const agentNames = { data: 'Data Engineer', coder: 'Coder', qa: 'QA', tester: 'DevOps' };

        for (let i = 0; i < fileOrder.length; i++) {
            const fileName = fileOrder[i];
            const agentKey = agentMap[fileName] || 'coder';
            const agentName = agentNames[agentKey] || 'Coder';
            const agent = agents[agentKey] || cdr;

            termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}/${fileOrder.length+1}] ${esc(agent.name)}</span> — ${esc(fileName)}`);
            highlightAgent(agentKey);
            explainStep(fileName, `${agent.name} generoi ${fileName} rakennuspalasista (template pipeline).`);

            // Pieni viive UX:ää varten — näyttää agentin "työskentelevän"
            await new Promise(r => setTimeout(r, 300));

            promptLog.push({ step: promptLog.length, agentKey, agentName: agent.name, model: 'template', label: fileName, systemPrompt: '(template pipeline — ei LLM-promptia)', userPrompt: `Generated from spec: ${JSON.stringify(spec.entities.map(e => e.name))}`, response: files[fileName] });
            termLog(`  <span style="color:#3fb950">✓ ${files[fileName].split('\\n').length} riviä</span>`);
        }

        let stepN = fileOrder.length + 2;

        // === Vaihe 4: Mekaaninen QA-validointi + korjaussilmukka ===
        const qaAgent = agents.qa || Object.values(agents)[4];
        const MAX_FIX_ROUNDS = 2;
        let mechIssues = validateProjectCode(files);
        let fixRound = 0;

        while (mechIssues.length > 0 && fixRound < MAX_FIX_ROUNDS) {
            fixRound++;
            termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — validointi (kierros ${fixRound})`);
            highlightAgent('qa');
            explainStep('Validointi', `${qaAgent.name} löysi ${mechIssues.length} ongelmaa — delegoidaan korjattavaksi.`);
            termLog(`  <span style="color:#d29922">⚠ ${mechIssues.length} ongelmaa:</span>`);
            for (const issue of mechIssues) termLog(`    <span style="color:#d29922">${esc(issue)}</span>`);
            promptLog.push({ step: promptLog.length, agentKey: 'qa', agentName: qaAgent.name, model: 'mekaaninen', label: `validointi #${fixRound}`, systemPrompt: '(mekaaninen validointi)', userPrompt: 'validateProjectCode(files)', response: mechIssues.join('\n') });
            stepN++;

            // Ryhmitellään ongelmat tiedostoittain
            const issuesByFile = {};
            for (const issue of mechIssues) {
                const m = issue.match(/^ISSUE:\s*(\S+?):/);
                const fname = m ? m[1] : 'unknown';
                if (!issuesByFile[fname]) issuesByFile[fname] = [];
                issuesByFile[fname].push(issue);
            }

            // Delegoidaan korjaukset Coder-agentille tiedosto kerrallaan
            for (const [fname, fIssues] of Object.entries(issuesByFile)) {
                if (!files[fname]) continue;
                const fixAgent = agents[agentMap[fname]] || cdr;
                termLog(`\n<span style="color:#f0883e;font-weight:bold">[${stepN}] ${esc(fixAgent.name)}</span> — korjaa ${esc(fname)} (${fIssues.length} ongelmaa)`);
                highlightAgent(agentMap[fname] || 'coder');
                explainStep(`Korjaus: ${fname}`, `${fixAgent.name} korjaa validoinnin löytämät ongelmat.`);

                const fixPrompt = `Fix the following issues in this Python file. Return ONLY the complete corrected file, no explanations.\n\nISSUES:\n${fIssues.join('\n')}\n\nCURRENT FILE (${fname}):\n\`\`\`python\n${files[fname]}\`\`\``;
                const fixResult = await kpnRun(fixAgent.model, fixPrompt, false, { ...fixAgent, prompt: 'You are a Python code fixer. Return ONLY the corrected Python file. No markdown fences, no explanations — just valid Python code.', capability: 'heavy' });

                if (fixResult) {
                    // Poistetaan markdown-koodiblokit jos LLM palauttaa ne
                    let cleaned = fixResult.replace(/^```(?:python)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim() + '\n';
                    files[fname] = cleaned;
                    termLog(`  <span style="color:#3fb950">✓ ${esc(fname)} korjattu</span>`);
                } else {
                    termLog(`  <span style="color:#f85149">✗ ${esc(fname)} korjaus epäonnistui — pidetään alkuperäinen</span>`);
                }
                promptLog.push({ step: promptLog.length, agentKey: agentMap[fname] || 'coder', agentName: fixAgent.name, model: fixAgent.model, label: `korjaus: ${fname}`, systemPrompt: '(code fixer)', userPrompt: fixPrompt, response: fixResult || '(epäonnistui)' });
                stepN++;
            }

            // Validoidaan uudelleen
            mechIssues = validateProjectCode(files);
        }

        // Lopullinen validointitulos
        termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — lopullinen validointi`);
        highlightAgent('qa');
        if (mechIssues.length > 0) {
            explainStep('Validointi', `${mechIssues.length} ongelmaa jäi korjaamatta ${MAX_FIX_ROUNDS} kierroksen jälkeen.`);
            termLog(`  <span style="color:#f85149">✗ ${mechIssues.length} ongelmaa jäljellä ${fixRound} korjauskierroksen jälkeen:</span>`);
            for (const issue of mechIssues) termLog(`    <span style="color:#f85149">${esc(issue)}</span>`);
        } else {
            const msg = fixRound > 0 ? `✓ Kaikki ongelmat korjattu (${fixRound} kierrosta)` : '✓ Kaikki tiedostot validoitu — 0 ongelmaa';
            explainStep('Validointi', msg);
            termLog(`  <span style="color:#3fb950">${msg}</span>`);
        }
        promptLog.push({ step: promptLog.length, agentKey: 'qa', agentName: qaAgent.name, model: 'mekaaninen', label: 'lopullinen validointi', systemPrompt: '(mekaaninen validointi)', userPrompt: 'validateProjectCode(files)', response: mechIssues.length === 0 ? `OK — korjattu ${fixRound} kierroksessa` : mechIssues.join('\n') });
        stepN++;

        // Tarkkailija: yhteenveto + raportti + arvosana
        const obs = agents.observer || Object.values(agents)[6];
        if (obs) {
            termLog(`\n<span style="color:#8b949e;font-weight:bold">[${stepN}] ${esc(obs.name)}</span> — projektin yhteenveto`);
            highlightAgent('observer');
            explainStep('Raportti', `${obs.name} kokoaa yhteenvedon ja antaa arvosanan.`);
            const finalCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
            const fileList = Object.keys(files).join(', ');
            const obsPrompt = (obs.prompt ? obs.prompt+'\n\n' : '') +
                `Write a project report in clean markdown for: ${task}\n\n` +
                `FIRST LINE must be exactly one of:\n` +
                `VERDICT: GREEN\nVERDICT: ORANGE\nVERDICT: RED\n\n` +
                `Then write this report:\n\n` +
                `# ${task}\n\n` +
                `## Overview\nOne paragraph describing what this project does.\n\n` +
                `## Files\n| File | Purpose |\n|------|---------|` +
                Object.entries(files).map(([n]) => `\n| ${n} | ... |`).join('') + `\n\n` +
                `## Quick Start\n` +
                '```bash\n' +
                `git clone <repo>\ncd project\nuv sync\nuv run uvicorn main:app --reload\n` +
                '```\n\n' +
                `## Docker\n` +
                '```bash\n' +
                `docker build -t ${task.toLowerCase().replace(/[^a-z0-9]/g, '-')} .\ndocker run -p 8000:8000 ${task.toLowerCase().replace(/[^a-z0-9]/g, '-')}\n` +
                '```\n\n' +
                `## API Endpoints\n| Method | Path | Description |\n|--------|------|-------------|` +
                `\n| POST | /items/ | Create |\n| GET | /items/ | List all |\n| GET | /items/{id} | Get by ID |\n| PUT | /items/{id} | Update |\n| DELETE | /items/{id} | Delete |\n` +
                `(Adapt paths and descriptions to match the actual code)\n\n` +
                `## Architecture\nDescribe the project structure and design decisions.\n\n` +
                `## Risk Assessment\n| Severity | Issue |\n|----------|-------|\n| ... | ... |\n\n` +
                `Project code:\n${finalCode}`;
            const readme = await kpnRun(obs.model, obsPrompt, false, { ...obs, capability: 'heavy' });
            if (readme) {
                files['README.md'] = readme;
                // Tallennetaan raportti globaalisti jotta tarkkailija-klikkaus avaa sen
                window._lastReport = readme;
                promptLog.push({ step: promptLog.length, agentKey: 'observer', agentName: obs.name, model: obs.model, label: 'README.md', systemPrompt: obs.prompt || '', userPrompt: obsPrompt, response: readme });
                // Parsitaan arvosana → tarkkailijan kehäväri
                const firstLine = readme.split('\n')[0].toUpperCase();
                let verdictColor = '#3fb950'; // oletus: vihreä
                let verdictText = 'OK';
                if (firstLine.includes('RED')) { verdictColor = '#f85149'; verdictText = 'KRIITTISTÄ'; }
                else if (firstLine.includes('ORANGE')) { verdictColor = '#d29922'; verdictText = 'HUOMIOITA'; }
                // Asetetaan tarkkailijan kehäväri
                const obsAvatar = document.querySelector('.agent-avatar[data-agent="observer"] img');
                if (obsAvatar) {
                    obsAvatar.style.borderColor = verdictColor;
                    obsAvatar.style.boxShadow = `0 0 12px ${verdictColor}`;
                }
                termLog(`  <span style="color:${verdictColor};font-weight:bold">● ${verdictText}</span> — klikkaa Tarkkailijaa nähdäksesi raportin`);
            }
            stepN++;
        }

        // Pipeline valmis
        highlightAgent(null);

        termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`);
        explainStep('Tulos', `Projekti "${task}" generoitu ${Object.keys(files).length} tiedostoon. Klikkaa "Avaa editorissa" tutkiaksesi koodia ja README:tä.`);
        // CrewAI-yhteensopiva output
        const crewFiles = generateCrewAIFiles(promptLog, task);
        Object.assign(files, crewFiles);

        // Oppimispolku
        renderLearnView(promptLog);

        termLog(`\n<span style="color:#8b949e">Siirretään tiedostot Editoriin...</span>`);
        window._currentProjectName = task;
        setTimeout(() => window.openInEditor(files), 1000);
    }

    async function kpnPipelineSimple(task) {
        pipelineAbort = new AbortController();
        const cli = agents.client || Object.values(agents)[0];
        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> — Requirements`);
        highlightAgent('client');
        const brief = await kpnRun(cli.model, `${task}`, false, cli);
        if (!brief) return;
        termLog(`\n<span style="color:#d29922;font-weight:bold">[2/4] Manager</span>`);
        highlightAgent('manager');
        const plan = await kpnRun('qwen-coder', `Requirements:\n${brief}\n\nAnalyse briefly, write a spec:\n${task}`);
        if (!plan) return;
        termLog(`\n<span style="color:#3fb950;font-weight:bold">[3/4] Coder</span>`);
        highlightAgent('coder');
        const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`);
        if (!code) return;
        termLog(`\n<span style="color:var(--accent);font-weight:bold">[4/4] DevOps</span>`);
        highlightAgent('tester');
        await kpnRun('qwen-coder', `Review briefly:\n${code}`);
        highlightAgent(null);
        termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Done ━━━</span>`);
    }

    // === Poistettiin renderProjectCard ja siirryttiin suoraan Editorin käyttöön ===
    window.copyProjectFile = function(id, name) {
        const files = window._projectFiles[id];
        if (files && files[name]) navigator.clipboard.writeText(files[name]);
    };
    window.copyAllProjectFiles = function(id) {
        const files = window._projectFiles[id];
        if (!files) return;
        const text = Object.entries(files).map(([n,c]) => '# --- ' + n + ' ---\n' + c).join('\n\n');
        navigator.clipboard.writeText(text);
    };
    window.downloadProjectZip = function() {
        const files = window._currentEditorFiles;
        const name = window._currentProjectName || 'projekti';
        if (!files) return;
        const enc = new TextEncoder();
        const entries = Object.entries(files);
        const localParts = [];
        const centralParts = [];
        let offset = 0;

        for (const [fname, content] of entries) {
            const nameBytes = enc.encode(fname);
            const dataBytes = enc.encode(content);
            const crc = crc32(dataBytes);

            // Local file header
            const local = new Uint8Array(30 + nameBytes.length + dataBytes.length);
            const lv = new DataView(local.buffer);
            lv.setUint32(0, 0x04034b50, true);  // signature
            lv.setUint16(4, 20, true);           // version needed
            lv.setUint16(6, 0x0800, true);       // UTF-8 flag (bit 11)
            lv.setUint16(8, 0, true);            // compression: stored
            lv.setUint32(14, crc, true);         // CRC-32
            lv.setUint32(18, dataBytes.length, true); // compressed size
            lv.setUint32(22, dataBytes.length, true); // uncompressed size
            lv.setUint16(26, nameBytes.length, true); // filename length
            local.set(nameBytes, 30);
            local.set(dataBytes, 30 + nameBytes.length);
            localParts.push(local);

            // Central directory entry
            const central = new Uint8Array(46 + nameBytes.length);
            const cv = new DataView(central.buffer);
            cv.setUint32(0, 0x02014b50, true);  // signature
            cv.setUint16(4, 20, true);           // version made by
            cv.setUint16(6, 20, true);           // version needed
            cv.setUint16(8, 0x0800, true);       // UTF-8 flag (bit 11)
            cv.setUint16(10, 0, true);           // compression: stored
            cv.setUint32(16, crc, true);
            cv.setUint32(20, dataBytes.length, true);
            cv.setUint32(24, dataBytes.length, true);
            cv.setUint16(28, nameBytes.length, true);
            cv.setUint32(42, offset, true);      // local header offset
            central.set(nameBytes, 46);
            centralParts.push(central);

            offset += local.length;
        }

        const centralSize = centralParts.reduce((s, c) => s + c.length, 0);
        // End of central directory
        const eocd = new Uint8Array(22);
        const ev = new DataView(eocd.buffer);
        ev.setUint32(0, 0x06054b50, true);
        ev.setUint16(8, entries.length, true);
        ev.setUint16(10, entries.length, true);
        ev.setUint32(12, centralSize, true);
        ev.setUint32(16, offset, true);

        const blob = new Blob([...localParts, ...centralParts, eocd], { type: 'application/zip' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = (name || 'projekti').split(/[\s,;:]+/).slice(0, 3).join('-').replace(/[^a-z0-9-]/gi, '').toLowerCase() + '.zip';
        a.click();
        URL.revokeObjectURL(url);
    };

    // CRC-32 (ZIP-standardin mukainen)
    const crc32Table = new Uint32Array(256);
    for (let i = 0; i < 256; i++) {
        let c = i;
        for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
        crc32Table[i] = c;
    }
    function crc32(data) {
        let crc = 0xFFFFFFFF;
        for (let i = 0; i < data.length; i++) crc = crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
        return (crc ^ 0xFFFFFFFF) >>> 0;
    }

    window.switchProjTab = function(id,i) {
        document.querySelectorAll(`.project-tab[data-card="${id}"]`).forEach((t,j) => t.classList.toggle('active', j===i));
        document.querySelectorAll(`.proj-panel[data-card="${id}"]`).forEach((p,j) => p.style.display = j===i ? '' : 'none');
    };

    // === Monaco Editor (lazy load) ===
    window.MonacoEnvironment = { getWorkerUrl: () => `data:text/javascript,self.MonacoEnvironment={baseUrl:'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/'};importScripts('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/worker/workerMain.js')` };

    let _monacoInitPromise = null;
    function initMonaco() {
        if (_monacoInitPromise) return _monacoInitPromise;
        _monacoInitPromise = new Promise((resolve, reject) => {
            const s = document.createElement('script');
            s.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js';
            s.onerror = () => reject(new Error('Monaco loader lataus epäonnistui'));
            s.onload = () => {
                require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' }});
                require(['vs/editor/editor.main'], () => {
                    window.monaco = monaco;
                    window._monaco = monaco.editor.create(document.getElementById('monaco-container'), {
                        value: '// Valitse tiedosto tai generoi projekti\n',
                        language: 'plaintext', theme: 'vs-dark', fontSize: 14,
                        minimap: { enabled: false }, automaticLayout: true, padding: { top: 10 }
                    });
                    console.log('[Monaco] Valmis');
                    resolve();
                }, (err) => reject(new Error('Monaco AMD load: ' + err)));
            };
            document.head.appendChild(s);
        });
        return _monacoInitPromise;
    }

    window._editorModels = {};
    const langMap = {py:'python',rs:'rust',js:'javascript',ts:'typescript',toml:'toml',json:'json',html:'html',css:'css',md:'markdown',txt:'plaintext'};

    window.openInEditor = async function(files) {
        window._currentEditorFiles = files;
        switchTab('editor');
        try { await initMonaco(); } catch(e) { console.error('Monaco-virhe:', e); return; }
        const m = window.monaco;
        if (!m) { console.error('Monaco ei latautunut'); return; }
        for (const [name,code] of Object.entries(files)) {
            const ext = name.split('.').pop().toLowerCase();
            if (window._editorModels[name]) window._editorModels[name].setValue(code);
            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('');

        openFile(Object.keys(files)[0]);
    };
    window.openFile = function(name) {
        if (!window._editorModels[name] || !window._monaco) return;
        window._monaco.setModel(window._editorModels[name]);
        document.querySelectorAll('#editor-file-list .dd-item').forEach(el => el.style.background = el.textContent===name ? 'var(--border)' : '');
        document.getElementById('editor-tabs').innerHTML = `<div class="project-tab active" style="cursor:default">${name}</div>`;
    };

    // === Guide loader ===
    (async () => {
        const el = document.getElementById('guide-content');
        try {
            const r = await fetch('/GUIDE.md');
            if (r.ok) el.innerHTML = renderMd(await r.text());
            el.querySelectorAll('pre code').forEach(b => { if (typeof hljs !== 'undefined') hljs.highlightElement(b); });
            // Mermaid-kaaviot
            if (window.mermaid) {
                el.querySelectorAll('.mermaid-block').forEach(async block => {
                    try {
                        const { svg } = await window.mermaid.render('r-' + block.id, block.textContent.trim());
                        block.innerHTML = svg;
                    } catch(e) { /* fallback: näytetään koodi */ }
                });
            }
        } catch(e) { el.textContent = 'Virhe: ' + e.message; }
    })();

    function renderMd(md) {
        const lines = md.split('\n');
        let html = '', inCode = false, lang = '', buf = '';
        let inTable = false, tableRows = [];

        function inline(text) {
            return text
                .replace(/\*\*(.+?)\*\*/g, '<strong style="color:#e6edf3">$1</strong>')
                .replace(/\*(.+?)\*/g, '<em>$1</em>')
                .replace(/`([^`]+)`/g, '<code style="background:var(--panel);padding:1px 5px;border-radius:3px;font-size:13px;color:#e6edf3">$1</code>');
        }

        function flushTable() {
            if (!inTable || tableRows.length < 2) { inTable = false; tableRows = []; return; }
            const hdr = tableRows[0].split('|').filter(c => c.trim());
            const body = tableRows.slice(2);
            html += '<div style="overflow-x:auto;margin:12px 0"><table style="width:100%;border-collapse:collapse;font-size:14px">';
            html += '<thead><tr>' + hdr.map(c => `<th style="text-align:left;padding:8px 12px;border-bottom:2px solid var(--border);color:var(--accent);font-weight:600">${inline(c.trim())}</th>`).join('') + '</tr></thead><tbody>';
            for (const row of body) {
                const cells = row.split('|').filter(c => c.trim());
                if (cells.length) html += '<tr>' + cells.map(c => `<td style="padding:6px 12px;border-bottom:1px solid #21262d">${inline(c.trim())}</td>`).join('') + '</tr>';
            }
            html += '</tbody></table></div>';
            inTable = false; tableRows = [];
        }

        for (const line of lines) {
            // Koodiblokit + Mermaid
            if (line.startsWith('```')) {
                if (inCode) {
                    if (lang === 'mermaid') {
                        const mid = 'mmd-' + Math.random().toString(36).slice(2,8);
                        html += `<div class="mermaid-block" id="${mid}" style="margin:16px 0;text-align:center">${buf.replace(/</g,'&lt;')}</div>`;
                    } else {
                        html += `<pre class="code-block"><code class="language-${lang}">${buf.replace(/</g,'&lt;')}</code></pre>`;
                    }
                    inCode = false; buf = '';
                } else {
                    flushTable();
                    inCode = true; lang = line.slice(3).trim() || 'plaintext';
                }
                continue;
            }
            if (inCode) { buf += (buf ? '\n' : '') + line; continue; }

            // Taulukot
            if (line.includes('|') && line.trim().startsWith('|')) {
                if (!inTable) inTable = true;
                tableRows.push(line);
                continue;
            } else { flushTable(); }

            // Tyhjä rivi
            if (!line.trim()) { html += '<div style="height:8px"></div>'; continue; }

            // Otsikot
            if (line.startsWith('# ')) { html += `<h1 style="color:#e6edf3;font-size:26px;margin:28px 0 10px;border-bottom:1px solid var(--border);padding-bottom:8px">${inline(line.slice(2))}</h1>`; continue; }
            if (line.startsWith('## ')) { html += `<h2 style="color:#e6edf3;font-size:21px;margin:24px 0 8px;border-bottom:1px solid #21262d;padding-bottom:6px">${inline(line.slice(3))}</h2>`; continue; }
            if (line.startsWith('### ')) { html += `<h3 style="color:#e6edf3;font-size:17px;margin:18px 0 6px">${inline(line.slice(4))}</h3>`; continue; }

            // Viiva
            if (line.match(/^-{3,}$/)) { html += '<hr style="border:none;border-top:1px solid var(--border);margin:20px 0">'; continue; }

            // Listat
            if (line.match(/^[\-\*] /)) { html += `<div style="padding:2px 0 2px 20px">${inline(line.replace(/^[\-\*] /, '• '))}</div>`; continue; }
            if (line.match(/^\d+\. /)) { html += `<div style="padding:2px 0 2px 20px">${inline(line)}</div>`; continue; }

            // Normaali teksti
            html += `<p style="margin:4px 0">${inline(line)}</p>`;
        }
        flushTable();
        return html;
    }
    // === Settings panel ===
    function initSettings() {
        const els = {
            systemPrompt: document.getElementById('set-system-prompt'),
            temperature: document.getElementById('set-temperature'),
            tempVal: document.getElementById('set-temp-val'),
            topK: document.getElementById('set-topk'),
            topkVal: document.getElementById('set-topk-val'),
            repeat: document.getElementById('set-repeat'),
            repVal: document.getElementById('set-rep-val'),
            maxTokens: document.getElementById('set-maxtokens'),
            maxtokVal: document.getElementById('set-maxtok-val'),
            stopSeq: document.getElementById('set-stop-sequences'),
            model: document.getElementById('set-model'),
        };
        if (!els.systemPrompt) return;

        // Lataa arvot
        els.systemPrompt.value = settings.systemPrompt;
        els.temperature.value = settings.temperature;
        els.tempVal.textContent = settings.temperature;
        els.topK.value = settings.topK;
        els.topkVal.textContent = settings.topK;
        els.repeat.value = settings.repeatPenalty;
        els.repVal.textContent = settings.repeatPenalty;
        els.maxTokens.value = settings.maxTokens;
        els.maxtokVal.textContent = settings.maxTokens;
        els.stopSeq.value = settings.stopSequences.replace(/\\n/g, '\n');
        els.model.value = settings.model;

        // Tallenna muutokset
        els.systemPrompt.oninput = () => { settings.systemPrompt = els.systemPrompt.value; saveSettings(); };
        els.temperature.oninput = () => { settings.temperature = +els.temperature.value; els.tempVal.textContent = settings.temperature; saveSettings(); };
        els.topK.oninput = () => { settings.topK = +els.topK.value; els.topkVal.textContent = settings.topK; saveSettings(); };
        els.repeat.oninput = () => { settings.repeatPenalty = +els.repeat.value; els.repVal.textContent = settings.repeatPenalty; 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.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
    const origSwitchTab = window.switchTab;
    window.switchTab = function(tab) {
        origSwitchTab(tab);
        if (tab === 'settings') initSettings();
    };

    window.resetSettings = function() {
        if (!confirm('Palautetaanko kaikki asetukset oletuksiin?')) return;
        settings = { ...defaultSettings };
        pipelineConfig = { ...defaultPipelineConfig };
        saveSettings();
        savePipelineConfig();
        initSettings();
    };
    // === Raportti-modal ===
    window.showReportModal = function(markdown) {
        let modal = document.getElementById('report-modal');
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'report-modal';
            modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)';
            modal.innerHTML = `<div style="background:var(--panel);border:1px solid var(--border);border-radius:8px;max-width:800px;width:90%;max-height:85vh;overflow-y:auto;padding:24px;position:relative">
                <button onclick="document.getElementById('report-modal').style.display='none'" style="position:absolute;top:12px;right:12px;background:none;border:none;color:#8b949e;font-size:20px;cursor:pointer">✕</button>
                <div id="report-modal-content" style="line-height:1.7;font-size:15px"></div>
            </div>`;
            document.body.appendChild(modal);
        }
        document.getElementById('report-modal-content').innerHTML = renderMd(markdown);
        document.getElementById('report-modal-content').querySelectorAll('pre code').forEach(b => { if (typeof hljs !== 'undefined') hljs.highlightElement(b); });
        modal.style.display = 'flex';
        modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; });
    };
    </script>
</body>
</html>
