---
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 Playground</title>
    <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 type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/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>
    <div 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 Playground</h1>
                <p style="color:#8b949e;margin:0">AI-ohjelmistokehitystiimi · <span id="hub-version">-</span></p>
            </div>
        </div>

        <!-- Välilehdet -->
        <div class="tabs">
            <div class="tab active" onclick="switchTab('agents')">Agentit</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>

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

    <script is:inline>
    // === Helpers ===
    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);
    }

    // === Globaalit tilat ===
    const defaultAgents = {
        manager: { name: 'Manageri', avatar: '/avatars/karhunpentu.webp', model: 'qwen-coder', order: 0,
            temperature: 0.5, topK: 40, repeatPenalty: 1.15, maxTokens: 512,
            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 4-5 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: 'Koodari', avatar: '/avatars/kipina_notext.webp', model: 'qwen-coder', order: 1,
            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
2. Import from other project files: from models import User, SessionLocal
3. Pydantic schemas use different names than SQLAlchemy models: UserCreate, UserResponse (not User)
4. SQLAlchemy engine: create_engine(url, connect_args={"check_same_thread": False})
5. SessionLocal: sessionmaker(autocommit=False, autoflush=False, bind=engine)
6. FastAPI dependencies: def get_db(): db = SessionLocal(); try: yield db; finally: db.close()
7. Pydantic v2: use model_dump() not dict(), class Config: from_attributes = True
8. All CRUD endpoints: POST (201), GET list, GET by id, PUT, DELETE (204)

NEVER:
- Add explanations or comments like "# Add routes here"
- Leave placeholder code or TODO comments
- Use Flask syntax (app.run) in FastAPI projects
- Forget to import from other project files
- 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', avatar: '/avatars/pesukarhu_notext.webp', model: 'qwen-coder', order: 2,
            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 or database.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

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` },
        qa: { name: 'QA', avatar: '/avatars/susi_notext.webp', model: 'qwen-coder', order: 3,
            temperature: 0.4, topK: 40, repeatPenalty: 1.15, maxTokens: 1024,
            prompt: `You are a QA engineer writing automated tests.

WRITE TESTS USING:
- pytest as the test framework
- FastAPI TestClient for API endpoint testing
- SQLAlchemy in-memory SQLite for test database isolation

TEST STRUCTURE:
1. test_create: POST valid data → 201, verify response matches input
2. test_list: GET collection → 200, verify array response
3. test_get_by_id: GET with valid/invalid ID → 200/404
4. test_update: PUT with valid data → 200, verify changes persisted
5. test_delete: DELETE → 204, verify GET returns 404 after

ALWAYS: from fastapi.testclient import TestClient` },
        tester: { name: 'DevOps', avatar: '/avatars/laiskiainen_notext.webp', model: 'qwen-coder', order: 4,
            temperature: 0.3, topK: 40, repeatPenalty: 1.1, maxTokens: 512,
            prompt: `You are a strict code reviewer and static analysis expert. Analyze the code line by line.

STATIC ANALYSIS 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

RESPOND:
- If all checks pass: "LGTM"
- If issues found: list each as "ISSUE: filename.py: description"
- Be specific and actionable, not vague` },
        observer: { name: 'Tarkkailija', avatar: '/avatars/aikuinen_susi.webp', model: 'qwen-coder', order: 5,
            temperature: 0.6, topK: 40, repeatPenalty: 1.15, maxTokens: 512,
            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 = 2;
    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)); }

    // === 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();
    };
    const initHash = window.location.hash.replace('#','');
    if (['editor','guide'].includes(initHash)) switchTab(initHash);

    // === 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 > 100 && !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)

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

    async function kpnRun(model, prompt, silent) {
        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> käsittelee...</span>`;
        termPanel.appendChild(statusDiv);
        termPanel.scrollTop = termPanel.scrollHeight;

        try {
            // Ei odotetaan Wasmia — lähetetään suoraan hubille.
            // Jos hub löytää natiivisolmun, vastaus tulee nopeasti.
            // Jos 503, käynnistetään Wasm-fallback.

            if (!silent) {
                const streamDiv = document.createElement('div');
                streamDiv.className = 'terminal-line';
                streamDiv.innerHTML = '  <span class="stream-content"></span><span style="color:#8b949e;animation:blink 1s infinite">▌</span>';
                termPanel.appendChild(streamDiv);
                termPanel.scrollTop = termPanel.scrollHeight;
                activeStreams[taskId] = streamDiv;
            }

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

            const res = await fetch('/api/v1/chat/completions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ model, prompt, task_id: taskId }),
            });

            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) {
                const firstLine = response.split('\n').find(l => l.trim()) || response;
                const lineCount = response.split('\n').filter(l => l.trim()).length;
                const uid = 'code-' + Date.now();
                termLog(
                    `  <span style="color:#3fb950;cursor:pointer" onclick="document.getElementById('${uid}').style.display=document.getElementById('${uid}').style.display==='none'?'block':'none'">`
                    + `<span style="color:#8b949e">▶</span> ${esc(firstLine.trim())} <span style="color:#8b949e">${lineCount > 1 ? '(+' + (lineCount-1) + ' riviä)' : ''}</span></span>`
                    + `<pre id="${uid}" style="display:none;margin:4px 0 0 16px;font:inherit;white-space:pre-wrap;border-left:2px solid var(--border);padding-left:10px">${highlightCode(response)}</pre>`
                );
            }
            return response;
        } 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','load','status','models','clear'],
        'kpn run': ['coder','coder-3b','manager','tester','qa','qwen-coder','smollm-135m'],
        '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"','"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: manageri→koodari→testaaja', '#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 === '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</span>');
            termLog('  <span style="color:var(--accent)">2</span>  qwen-coder-3b  Qwen2.5-Coder:3B    <span style="color:#8b949e">~6.2 GB</span>');
            termLog('  <span style="color:var(--accent)">3</span>  smollm-135m    SmolLM 135M         <span style="color:#8b949e">~270 MB</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 = {};

    // Ladataan mallipohjat
    (async () => {
        try {
            const res = await fetch('/templates/fastapi-crud.json');
            if (res.ok) { const t = await res.json(); templates[t.name] = t; }
        } catch(e) {}
    })();

    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>`);
    }

    async function kpnProject(task) {
        const cdr = agents.coder || Object.values(agents)[1];

        // Etsitään sopivin mallipohja
        const template = Object.values(templates)[0]; // Toistaiseksi vain FastAPI CRUD
        if (!template) {
            termLog('  ✗ Mallipohjia ei ladattu', '#f85149');
            return;
        }

        termLog(`<span style="color:var(--purple);font-weight:bold">━━━ ${esc(template.name)} — ${esc(task)} ━━━</span>`);
        explainStep('Mallipohja', `Käytetään "${template.name}" -mallipohjaa jossa ${template.order.length} tiedostoa: ${template.order.join(', ')}. Jokainen tiedosto generoidaan järjestyksessä, ja aiemmat tiedostot annetaan kontekstina seuraavalle.`);

        const files = {};

        for (let i = 0; i < template.order.length; i++) {
            const fileName = template.order[i];
            const fileDef = template.files[fileName];
            if (!fileDef) continue;

            const step = i + 1;
            // Valitaan oikea agentti tiedostotyypin mukaan
            const isDbFile = fileName === 'models.py' || fileName === 'database.py';
            const dataAgent = agents.data || Object.values(agents)[2];
            const fileAgent = isDbFile && dataAgent ? dataAgent : cdr;
            const fileAgentKey = isDbFile && dataAgent ? 'data' : 'coder';

            termLog(`\n<span style="color:#3fb950;font-weight:bold">[${step}/${template.order.length}] ${esc(fileAgent.name)}</span> — ${esc(fileName)}`);
            highlightAgent(fileAgentKey);

            // Opettava selitys: miksi tämä tiedosto, mitä se sisältää
            explainStep(fileName, fileDef.instructions);

            // Rakennetaan prompti: esimerkki + konteksti + ohje
            let prompt = '';

            // Agentin system prompt (data-agentti models.py:lle, koodari muille)
            if (fileAgent.prompt) prompt += fileAgent.prompt + '\n\n';

            // Esimerkki (few-shot)
            prompt += `EXAMPLE of ${fileName} (for a different project, adapt to this one):\n`;
            prompt += '```\n' + fileDef.example + '\n```\n\n';

            // Aiemmin generoidut tiedostot (konteksti)
            const prevFiles = Object.entries(files);
            if (prevFiles.length > 0) {
                prompt += 'Already written files in THIS project:\n';
                for (const [name, code] of prevFiles) {
                    prompt += `--- ${name} ---\n${code}\n\n`;
                }
            }

            // Tehtävä
            prompt += `NOW write "${fileName}" for THIS project: ${task}\n`;
            prompt += fileDef.instructions + '\n';
            prompt += 'Adapt the example to match the project description. Import from already written files. Write ONLY the code, no explanations.';

            const code = await kpnRun(fileAgent.model, prompt);
            if (!code) {
                termLog(`  ✗ Keskeytyi (${fileName})`, '#f85149');
                return;
            }
            files[fileName] = code;
        }

        const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
        let stepN = template.order.length + 1;

        // Review-korjausluuppi: max 2 kierrosta
        const tst = agents.tester || Object.values(agents)[4];
        const MAX_REVIEW_ROUNDS = 3;

        for (let round = 0; round < MAX_REVIEW_ROUNDS; round++) {
            const currentCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');

            // DevOps review
            termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — koodikatselmointi${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
            highlightAgent('tester');
            if (round === 0) explainStep('Koodikatselmointi', `${tst.name} analysoi koodin rivi riviltä: importit, nimeämiset, virheenkäsittely, tietoturva.`);
            else explainStep('Uudelleentarkistus', `${tst.name} tarkistaa korjaukset.`);

            const reviewPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') + `Review this project:\n\n${currentCode}`;
            const review = await kpnRun(tst.model, reviewPrompt);
            stepN++;

            // LGTM → ei korjauksia tarvita
            if (!review || review.toLowerCase().includes('lgtm')) {
                termLog(`  <span style="color:#3fb950">✓ ${esc(tst.name)}: LGTM</span>`);
                break;
            }

            // Korjaukset
            termLog(`\n<span style="color:#d29922;font-weight:bold">[${stepN}] ${esc(cdr.name)}</span> — korjaukset${round > 0 ? ' (kierros '+(round+1)+')' : ''}`);
            highlightAgent('coder');
            explainStep('Korjaus', `${tst.name} löysi ongelmia. ${cdr.name} saa palautteen ja korjaa.`);

            const fixPrompt = `${cdr.prompt ? cdr.prompt+'\n\n' : ''}Fix these issues:\n${review}\n\nCurrent code:\n${currentCode}\n\nWrite ALL corrected files. Start each file with: --- filename.py ---`;
            const fixedCode = await kpnRun(cdr.model, fixPrompt);

            // Parsitaan korjatut tiedostot takaisin files-objektiin
            if (fixedCode) {
                const fixedParts = fixedCode.split(/^---\s*(\S+)\s*---$/m);
                for (let j = 1; j < fixedParts.length; j += 2) {
                    const fname = fixedParts[j].trim();
                    const fcode = (fixedParts[j+1] || '').trim();
                    if (fname && fcode && files[fname] !== undefined) {
                        files[fname] = fcode;
                    }
                }
            }
            stepN++;
        } // for review round

        // Päivitetään allCode korjausten jälkeen
        const updatedCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');

        // QA: testit (saa korjatut tiedostot)
        const qaAgent = agents.qa || Object.values(agents)[3];
        if (qaAgent) {
            termLog(`\n<span style="color:#d2a8ff;font-weight:bold">[${stepN}] ${esc(qaAgent.name)}</span> — testit`);
            highlightAgent('qa');
            explainStep('Testit', `${qaAgent.name} kirjoittaa pytest-testit korjatulle koodille.`);
            const qaPrompt = (qaAgent.prompt ? qaAgent.prompt+'\n\n' : '') + `Write pytest tests for this project:\n\n${updatedCode}\n\nWrite a complete test_main.py file with TestClient.`;
            const tests = await kpnRun(qaAgent.model, qaPrompt);
            if (tests) files['test_main.py'] = tests;
            stepN++;
        }

        // DevOps: Dockerfile (saa kaikki tiedostot mukaan lukien testit)
        const allFilesNow = Object.keys(files).join(', ');
        termLog(`\n<span style="color:var(--accent);font-weight:bold">[${stepN}] ${esc(tst.name)}</span> — Dockerfile`);
        highlightAgent('tester');
        explainStep('Dockerfile', `${tst.name} generoi Docker-kontin kaikista ${Object.keys(files).length} tiedostosta: ${allFilesNow}`);
        const dockerPrompt = (tst.prompt ? tst.prompt+'\n\n' : '') +
            `Write a Dockerfile for this Python FastAPI project.\n\n` +
            `Project files: ${allFilesNow}\n\n` +
            `Requirements:\n` +
            `- Use python:3.12-slim as base\n` +
            `- Install uv: COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\n` +
            `- Copy pyproject.toml first, then uv sync, then copy source\n` +
            `- Expose port 8000\n` +
            `- CMD: uv run uvicorn main:app --host 0.0.0.0 --port 8000\n` +
            `\nWrite ONLY the Dockerfile, no explanations.`;
        const dockerfile = await kpnRun(tst.model, dockerPrompt);
        if (dockerfile) files['Dockerfile'] = dockerfile;
        stepN++;

        // Tarkkailija: yhteenveto + raportti + arvosana
        const obs = agents.observer || Object.values(agents)[5];
        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);
            if (readme) {
                files['README.md'] = readme;
                // Tallennetaan raportti globaalisti jotta tarkkailija-klikkaus avaa sen
                window._lastReport = 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ä.`);
        renderProjectCard(files, task);
    }

    async function kpnPipelineSimple(task) {
        termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Pipeline ━━━</span>`);
        termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span>`);
        const plan = await kpnRun('qwen-coder', `Analyse briefly, write a spec:\n${task}`);
        if (!plan) return;
        termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span>`);
        const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`);
        if (!code) return;
        termLog(`\n<span style="color:var(--accent);font-weight:bold">[3/3] Testaaja</span>`);
        await kpnRun('qwen-coder', `Review briefly:\n${code}`);
        termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
    }

    // === Project card ===
    window._projectFiles = {}; // id → files

    function renderProjectCard(files, name) {
        const entries = Object.entries(files);
        if (!entries.length) return;
        const id = 'proj-' + Date.now();
        window._projectFiles[id] = files;

        const tabs = entries.map(([n],i) =>
            `<div class="project-tab${i===0?' active':''}" data-card="${id}" data-i="${i}" onclick="switchProjTab('${id}',${i})">${esc(n)}</div>`
        ).join('');

        const panels = entries.map(([n,c],i) =>
            `<div class="proj-panel" data-card="${id}" data-i="${i}" style="${i>0?'display:none':''}">` +
            `<div style="text-align:right;padding:4px 8px;background:var(--bg);border-bottom:1px solid #21262d">` +
            `<button class="btn btn-muted" onclick="copyProjectFile('${id}','${esc(n)}')">Kopioi</button></div>` +
            `<pre class="code-block">${highlightCode(c)}</pre></div>`
        ).join('');

        const html = `<div id="${id}" class="project-card">` +
            `<div class="project-header">` +
            `<span style="color:var(--purple);font-weight:600">${esc(name||'Projekti')} <span style="color:#8b949e;font-weight:normal">(${entries.length})</span></span>` +
            `<span style="display:flex;gap:6px">` +
            `<button class="btn btn-muted" onclick="copyAllProjectFiles('${id}')">Kopioi kaikki</button>` +
            `<button class="btn btn-green" onclick="openInEditor(window._projectFiles['${id}'])">Avaa editorissa</button>` +
            `</span></div>` +
            `<div class="project-tabs">${tabs}</div>${panels}</div>`;

        const div = document.createElement('div');
        div.innerHTML = html;
        termPanel.appendChild(div.firstElementChild);
        termPanel.scrollTop = termPanel.scrollHeight;
    }
    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.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) {
        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('');
        document.getElementById('editor-tabs').innerHTML = Object.keys(files).map(n => `<div class="project-tab" onclick="openFile('${n}')">${n}</div>`).join('');
        openFile(Object.keys(files)[0]);
    };
    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.querySelectorAll('#editor-tabs .project-tab').forEach(el => el.classList.toggle('active', el.textContent===name));
    };

    // === 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(); };
    }
    // 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 };
        saveSettings();
        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>
