Kipinä Agentic Playground

AI-ohjelmistokehitystiimi · -

Kipinä Agentic Playground
Opas

0

Aktiivisia Nodeja

0

Verkossa Suoritettua Tehtävää (Globaali)

0 GB

Verkon yhteis-VRAM

Valitse tehtävä
Qwen2.5-Coder-0.5B-Instruct Ei yhdistetty

Code-specialized language model trained on 5.5T tokens of source code. Generates Python code in your browser via WebAssembly. Choose model size and write your own prompt.

0
Tehtäviä
0
Tokeneita
-
tok/s
Kirjoita ohjelmointitehtävä ja paina Koodaa
Kipinä Agent Workspace
Monitoring Active
Asiakas (Kettu)
Asiakas
Tuoteomistaja
Tarkkailija (Aikuinen Susi)
Tarkkailija
Laadunvalvonta
Manageri (Karhunpentu)
Manageri
KPN CLI
Koodari (Salamanteri)
Koodari
SOFTAKEHITYS
Data-Agentti (Pesukarhu)
Data
Tietokannat
QA (Pikkususi)
QA
Testaus
DevOps (Laiskiainen)
DevOps
Käyttöönotto
Tallennettu
Hub: Yhdistetään... Laskenta:
$

Ladataan opasta...

` : file.name.includes('model') ? `\nEXAMPLE output for a models.py: from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text from sqlalchemy.orm import sessionmaker, DeclarativeBase engine = create_engine("sqlite:///app.db") SessionLocal = sessionmaker(bind=engine) class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True) name = Column(String) Base.metadata.create_all(engine) def get_db(): db = SessionLocal() try: yield db finally: db.close()` : ''; const coderPrompt = `${context}Project: ${task} Write ONLY the file "${file.name}"${file.desc ? ': ' + file.desc : ''}.${extraInstructions}${coderExample} IMPORTANT: Keep the code SHORT. Max ~60 lines. No comments, no docstrings. Write minimal, working code. Output ONLY code. Serve index.html at / using FileResponse. Use /api/ prefix for JSON endpoints.`; const code = await kpnRun(agentPrompts.coder.model, coderPrompt); if (!code) { termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149'); return; } generatedFiles[file.name] = code; pipelineStep('coder', file.name, 'done', coderPrompt, code); } // Vaihe 3: Testaaja arvioi koko projektin const allCode = Object.entries(generatedFiles) .map(([name, code]) => `--- ${name} ---\n${code}`) .join('\n\n'); // Staattinen analyysi ennen LLM-arviointia termLog(`\n[${fileList.length + 2}] Testaaja — staattinen analyysi + arviointi`); pipelineStep('tester', 'Review', 'active', `${Object.keys(generatedFiles).length} tiedostoa`); // Yksinkertainen staattinen tarkistus selaimessa const staticIssues = []; for (const [name, code] of Object.entries(generatedFiles)) { if (!name.endsWith('.py')) continue; const lines = (code || '').split('\n'); // Tarkista importit const imports = lines.filter(l => l.match(/^(from|import)\s/)); const usedNames = code.replace(/^(from|import)\s.*/gm, ''); for (const imp of imports) { const match = imp.match(/import\s+(\w+)|from\s+\S+\s+import\s+(.+)/); if (match) { const names = (match[1] || match[2]).split(',').map(n => n.trim().split(' as ').pop().trim()); for (const n of names) { if (n && !usedNames.includes(n) && n !== '*') { staticIssues.push(`${name}: käyttämätön import '${n}'`); } } } } // Tarkista puuttuvat importit const importText = imports.join(' '); const needsImport = [ ['FastAPI', 'FastAPI'], ['Session', 'Session'], ['Depends', 'Depends'], ['HTTPException', 'HTTPException'], ['TestClient', 'TestClient'], ['Boolean', 'Boolean'], ['Text', 'Text'], ['Float', 'Float'], ['DateTime', 'DateTime'], ['Column', 'Column'], ['create_engine', 'create_engine'], ['DeclarativeBase', 'DeclarativeBase'], ['sessionmaker', 'sessionmaker'], ]; for (const [symbol, name_] of needsImport) { // Tarkista käytetäänkö symbolia koodissa (ei import-riveillä) const codeWithoutImports = lines.filter(l => !l.match(/^(from|import)\s/)).join('\n'); if (codeWithoutImports.includes(symbol) && !importText.includes(symbol)) { staticIssues.push(`${name}: käyttää '${symbol}' mutta ei importtaa sitä`); } } // Tarkista tyhjät funktiot const emptyFuncs = code.match(/def \w+\([^)]*\):\s*\n\s*(pass|\.\.\.)/g); if (emptyFuncs) staticIssues.push(`${name}: ${emptyFuncs.length} tyhjää funktiota`); // Tarkista tiedostojen väliset importit (from db import get_db → onko get_db db.py:ssä?) for (const imp of imports) { const crossMatch = imp.match(/from\s+(\w+)\s+import\s+(.+)/); if (crossMatch) { const modName = crossMatch[1]; const importedNames = crossMatch[2].split(',').map(n => n.trim().split(' as ')[0].trim()); const targetFile = modName + '.py'; const targetCode = generatedFiles[targetFile]; if (targetCode) { for (const sym of importedNames) { // Tarkista onko symboli määritelty kohdetiedostossa (def, class, muuttuja) const defined = targetCode.includes(`def ${sym}`) || targetCode.includes(`class ${sym}`) || targetCode.match(new RegExp(`^${sym}\\s*=`, 'm')); if (!defined) { staticIssues.push(`${name}: importtaa '${sym}' tiedostosta ${targetFile}, mutta sitä ei ole määritelty siellä`); } } } } } } if (staticIssues.length > 0) { termLog(` Staattinen analyysi (${staticIssues.length} huomautusta):`); for (const issue of staticIssues) { termLog(` ${esc(issue)}`); } } else { termLog(' Staattinen analyysi: ei huomautuksia'); } const reviewPrompt = `Review this project code. Check EVERY item and report result: 1. Imports: ✓/✗ — are all imports valid and available? 2. Database: ✓/✗ — is the DB setup correct (engine, session, models)? 3. Endpoints: ✓/✗ — do all routes have correct parameters and return types? 4. Error handling: ✓/✗ — are edge cases handled (404, validation)? 5. Security: ✓/✗ — any SQL injection, missing auth, or data exposure? EXAMPLE output: 1. Imports: ✓ — all imports are valid 2. Database: ✗ — missing Base.metadata.create_all(engine) call 3. Endpoints: ✓ — GET/POST/DELETE routes are correct 4. Error handling: ✗ — no 404 when todo not found 5. Security: ✓ — using ORM, no raw SQL ${allCode}`; const review = await kpnRun(agentPrompts.tester.model, reviewPrompt, false, 300); pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review); // Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia const hasIssues = review && (review.includes('✗') || staticIssues.length > 0); if (hasIssues) { termLog(`\n[${fileList.length + 3}] Koodari — korjaukset`); pipelineStep('coder', 'Korjaukset', 'active', review); const fixPrompt = `Fix the issues found in the review. Review feedback: ${review} Current code: ${allCode} Write the corrected code.`; const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt); pipelineStep('coder', 'Korjaukset', 'done', review, fixedCode); if (fixedCode) { termLog(`\n[${fileList.length + 4}] Testaaja — uudelleenarviointi`); pipelineStep('tester', 'Re-review', 'active', fixedCode); const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`, false, 128); pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview); } } // Vaihe 5: QA kirjoittaa testit const step5 = fileList.length + (review && !review.toLowerCase().includes('lgtm') ? 5 : 3); termLog(`\n[${step5}] QA — testit`); pipelineStep('qa', 'Testit', 'active', 'Kirjoitetaan testejä'); const qaPrompt = `Write test_app.py using pytest and FastAPI TestClient. Max 3 tests. Output ONLY code. EXAMPLE: from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_create(): r = client.post("/users", params={"name": "Test"}) assert r.status_code == 200 def test_list(): r = client.get("/users") assert r.status_code == 200 assert isinstance(r.json(), list) PROJECT CODE: ${Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n')}`; const tests = await kpnRun(agentPrompts.qa.model, qaPrompt, false, 512); if (tests) generatedFiles['test_app.py'] = tests; pipelineStep('qa', 'Testit', 'done', 'test_app.py', tests); // Vaihe 6: DevOps — Dockerfile const step6 = step5 + 1; termLog(`\n[${step6}] DevOps — Dockerfile`); pipelineStep('tester', 'Dockerfile', 'active', 'Dockerfile'); const mainFile = Object.keys(generatedFiles).find(f => f.includes('main') || f.includes('app')) || Object.keys(generatedFiles)[0]; const hasPyproject = 'pyproject.toml' in generatedFiles; const hasRequirements = 'requirements.txt' in generatedFiles; const codeFiles = Object.keys(generatedFiles).filter(f => f.endsWith('.py') || f.endsWith('.html')); // Dockerfile-templatti: ei anneta mallin keksiä omaa let depLines; if (hasPyproject) { depLines = 'COPY pyproject.toml .\nRUN uv sync --no-dev'; } else if (hasRequirements) { depLines = 'COPY requirements.txt .\nRUN uv pip install --system -r requirements.txt'; } else { depLines = 'RUN uv pip install --system fastapi uvicorn'; } const dockerfileContent = `FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app ${depLines} COPY ${codeFiles.join(' ')} ./ EXPOSE 8000 CMD ["uv", "run", "uvicorn", "${mainFile.replace('.py','')}:app", "--host", "0.0.0.0", "--port", "8000"]`; // Generoidaan Dockerfile suoraan templatesta, ei mallilla generatedFiles['Dockerfile'] = dockerfileContent; termLog(` Dockerfile generoitu (template)`); pipelineStep('tester', 'Dockerfile', 'done', dockerfileContent, dockerfileContent); // Vaihe 7: DevOps — docker-compose.yml const step7 = step6 + 1; termLog(`\n[${step7}] DevOps — docker-compose.yml`); pipelineStep('tester', 'Compose', 'active', 'docker-compose.yml'); // docker-compose.yml templatesta (ei LLM:llä — vältetään version/postgres ongelmat) const composeContent = `services: app: build: . ports: - "8000:8000" volumes: - app-data:/app/data restart: unless-stopped volumes: app-data:`; generatedFiles['docker-compose.yml'] = composeContent; termLog(` docker-compose.yml generoitu (template)`); pipelineStep('tester', 'Compose', 'done', composeContent, composeContent); // Vaihe 8: DevOps — README const step8 = step7 + 1; termLog(`\n[${step8}] DevOps — README`); pipelineStep('tester', 'README', 'active', 'README.md'); const readmePrompt = `Write a minimal README.md. Include ONLY: 1. One-line description 2. Quick start: docker compose up → open http://localhost:8000 3. Development: uv sync && uv run uvicorn main:app --reload → http://localhost:8000 4. API endpoints (if applicable) 5. Testing: uv run pytest Max 20 lines. Files: ${Object.keys(generatedFiles).join(', ')}`; const readme = await kpnRun(agentPrompts.tester.model, readmePrompt, false, 256); if (readme) generatedFiles['README.md'] = readme; pipelineStep('tester', 'README', 'done', 'README.md', readme); // Validointivaihe: QA tarkistaa kaikkien tiedostojen yhteensopivuuden const stepV = step8 + 1; termLog(`\n[${stepV}] QA — validointi`); pipelineStep('qa', 'Validointi', 'active', 'Tarkistetaan yhteensopivuus'); const allFiles = Object.entries(generatedFiles).map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n'); const validatePrompt = `You are a QA engineer. Check EVERY item below and report the result for each. Use this EXACT format: 1. Dockerfile COPY: ✓ OK / ✗ problem description 2. Dockerfile deps vs imports: ✓ OK / ✗ problem description 3. docker-compose ports: ✓ OK / ✗ problem description 4. README commands: ✓ OK / ✗ problem description 5. Test imports: ✓ OK / ✗ problem description 6. pyproject.toml deps: ✓ OK / ✗ problem (NEVER list stdlib: sqlite3, os, sys, json, typing) EXAMPLE output: 1. Dockerfile COPY: ✓ OK — copies main.py and models.py which both exist 2. Dockerfile deps: ✗ missing "sqlalchemy" in pip install 3. docker-compose ports: ✓ OK — maps 8000:8000 matching EXPOSE 4. README commands: ✓ OK — uvicorn main:app matches main.py 5. Test imports: ✓ OK — imports main.app which exists 6. pyproject.toml deps: ✓ OK — includes fastapi, uvicorn, sqlalchemy Files in project: ${Object.keys(generatedFiles).join(', ')} ${allFiles}`; const validation = await kpnRun(agentPrompts.qa.model, validatePrompt, false, 256); pipelineStep('qa', 'Validointi', 'done', 'Yhteensopivuus', validation); // Jos QA löysi ongelmia, korjataan if (validation && !validation.toLowerCase().startsWith('ok') && !validation.toLowerCase().includes('no issues') && !validation.toLowerCase().includes('everything is fine')) { const stepFix = stepV + 1; termLog(`\n[${stepFix}] DevOps — korjaukset`); pipelineStep('tester', 'Korjaukset', 'active', validation); // Korjataan vain Dockerfile ja docker-compose // Korjataan koodatiedostot (ei Dockerfilea — se on template) const fixableFiles = Object.entries(generatedFiles) .filter(([n]) => n.endsWith('.py') || n === 'pyproject.toml') .map(([n, c]) => `--- ${n} ---\n${c}`).join('\n\n'); const fixPrompt = `Fix the code files based on this feedback. Output corrected code only. Do NOT output Dockerfile or docker-compose.yml — those are auto-generated. Feedback: ${validation} ${fixableFiles}`; const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt, false, 512); // Ei ylikirjoiteta Dockerfilea — generoidaan template uudelleen if (fixedCode) { termLog(` Korjaukset generoitu`); } pipelineStep('coder', 'Korjaukset', 'done', 'Korjaukset generoitu', fixedCode); } // Generoidaan projektin dokumentaatio HTML-raporttina const reportHtml = generateProjectReport(task, generatedFiles, pipelineSteps, staticIssues); const reportBlob = new Blob([reportHtml], { type: 'text/html' }); const reportUrl = URL.createObjectURL(reportBlob); termLog(`\n━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━`); termLog(` 📄 Avaa projektiraportti`); renderProjectCard(generatedFiles, task, reportUrl); } function generateProjectReport(task, files, steps, staticIssues) { const fileEntries = Object.entries(files); const agentNames = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' }; const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' }; const stepsHtml = steps.map((s, i) => { const color = agentColors[s.agent] || '#8b949e'; const icon = s.status === 'done' ? '✓' : '◷'; const outputPreview = (s.output || '').substring(0, 500); return `
${icon} ${agentNames[s.agent] || s.agent} — ${s.label} Vaihe ${i + 1}
${s.input ? `
Prompti
${s.input.replace(/
` : ''} ${outputPreview ? `
${outputPreview.replace(/` : ''}
                    
`; }).join(''); const filesHtml = fileEntries.map(([name, content]) => `
${name}
${(content || '').replace(/
                
`).join(''); const staticHtml = (staticIssues || []).length > 0 ? `
Staattinen analyysi (${staticIssues.length} huomautusta)
` : '

✓ Staattinen analyysi: ei huomautuksia

'; return ` Kipinä Raportti — ${task}

🔥 Kipinä Projektiraportti

${task} — ${new Date().toLocaleString('fi-FI')} — ${fileEntries.length} tiedostoa, ${steps.length} vaihetta

🔄 Agenttien workflow

${generateWorkflowSwimlane(steps)}

📋 Pipeline-vaiheet

${stepsHtml}

🔍 Staattinen analyysi

${staticHtml}

📁 Tiedostot

${filesHtml}

Generoitu Kipinä Agentic Playground v0.2.2 — kipina.studio

`; } function generateWorkflowSwimlane(steps) { const agentLabels = { manager: 'Manageri', coder: 'Koodari', tester: 'DevOps', qa: 'QA', data: 'Data' }; const agentColors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' }; const agentBgs = { manager: '#1c1206', coder: '#0d1a0d', tester: '#0d1520', qa: '#170d22', data: '#1a0d22' }; const stepDescs = { 'Suunnittelu': 'Jakaa projektin tiedostoiksi', 'Review': 'Tarkistaa koodin laadun', 'Testit': 'Kirjoittaa pytest-testit', 'Dockerfile': 'Generoi Docker-imagen', 'Compose': 'Palvelumääritys', 'README': 'Käyttöohjeet', 'Validointi': 'Tarkistaa yhteensopivuuden', 'Korjaukset': 'Korjaa löydetyt ongelmat' }; var agents = []; var agentMap = {}; for (var si = 0; si < steps.length; si++) { var s = steps[si]; if (!agentMap[s.agent]) { agentMap[s.agent] = []; agents.push(s.agent); } agentMap[s.agent].push(s); } var rows = ''; for (var ai = 0; ai < agents.length; ai++) { var agent = agents[ai]; var color = agentColors[agent] || '#8b949e'; var bg = agentBgs[agent] || '#161b22'; var label = agentLabels[agent] || agent; var badges = ''; var aSteps = agentMap[agent]; for (var bi = 0; bi < aSteps.length; bi++) { var st = aSteps[bi]; var desc = stepDescs[st.label] || st.label; var outPrev = (st.output || '').substring(0, 100).replace(/ 0) badges += ''; badges += '' + icon + ' ' + st.label.replace(/'; } rows += '
' + label + '
' + badges + '
'; } return '
' + rows + '
'; } // Yksinkertainen pipeline (vanha: manageri → koodari → testaaja) async function kpnPipelineSimple(task) { termLog(`━━━ Pipeline käynnistyy ━━━`); termLog(`\n[1/3] Manageri`); const plan = await kpnRun(agentPrompts.manager.model, `Analyse this task briefly and write a technical spec for a coder:\n${task}`); if (!plan) return; termLog(`\n[2/3] Koodari`); const code = await kpnRun(agentPrompts.coder.model, `${plan}\n\nWrite the code.`); if (!code) return; termLog(`\n[3/3] Testaaja`); await kpnRun(agentPrompts.tester.model, `Review briefly:\n${code}`); termLog(`\n━━━ Pipeline valmis ━━━`); } // Autokorjaus: tunnetut kirjoitusvirheet ja lähimmän komennon ehdotus function autocorrect(input) { const typos = { 'knp': 'kpn', 'kpb': 'kpn', 'kpm': 'kpn', 'kn': 'kpn', 'kp': 'kpn', 'kpn rnu': 'kpn run', 'kpn rn': 'kpn run', 'kpn ru': 'kpn run', 'kpn laod': 'kpn load', 'kpn lod': 'kpn load', 'kpn loa': 'kpn load', 'kpn porject': 'kpn project', 'kpn projcet': 'kpn project', 'kpn proejct': 'kpn project', 'kpn pipelien': 'kpn pipeline', 'kpn pipline': 'kpn pipeline', 'kpn staus': 'kpn status', 'kpn stauts': 'kpn status', 'kpn modles': 'kpn models', 'kpn mdoels': 'kpn models', 'kpn hlep': 'kpn help', 'kpn hep': 'kpn help', 'kpn clera': 'kpn clear', 'kpn claer': 'kpn clear', 'kpn helo': 'kpn hello', 'kpn hell': 'kpn hello', }; // Tarkista koko komento ja ensimmäinen sana + alikomento const lower = input.toLowerCase(); for (const [typo, fix] of Object.entries(typos)) { if (lower === typo || lower.startsWith(typo + ' ')) { return fix + input.slice(typo.length); } } // Levenshtein-etäisyys ensimmäiselle sanalle const words = input.trim().split(/\s+/); const firstWord = words[0].toLowerCase(); if (firstWord !== 'kpn' && firstWord.length >= 2 && firstWord.length <= 5) { const dist = levenshtein(firstWord, 'kpn'); if (dist <= 2) return 'kpn' + input.slice(firstWord.length); } // Fuzzy-korjaus alikomentotasolla: "kpn rnu" → "kpn run" if (firstWord === 'kpn' && words.length >= 2) { const sub = words[1].toLowerCase(); const subCommands = ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear']; let bestMatch = null, bestDist = 3; for (const cmd of subCommands) { const d = levenshtein(sub, cmd); if (d > 0 && d < bestDist) { bestDist = d; bestMatch = cmd; } } if (bestMatch) { words[1] = bestMatch; return words.join(' '); } } return null; } function levenshtein(a, b) { const m = a.length, n = b.length; const d = Array.from({length: m + 1}, (_, i) => [i]); for (let j = 1; j <= n; j++) d[0][j] = j; for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) d[i][j] = Math.min(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0)); return d[m][n]; } function termExec(cmd) { termLog(`$ ${esc(cmd)}`); termHistory.unshift(cmd); termHistIdx = -1; // Autokorjaus const corrected = autocorrect(cmd.trim()); if (corrected && corrected !== cmd.trim()) { cmd = corrected; termLog(` → korjattu: ${esc(cmd)}`); } // Oikotie: pelkkä numero → kpn load if (/^\d+$/.test(cmd.trim())) { cmd = 'kpn load ' + cmd.trim(); termLog(` → ${esc(cmd)}`); } const parts = cmd.trim().split(/\s+/); if (parts[0] !== 'kpn') { termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149'); return; } const sub = parts[1]; if (sub === 'help' || !sub) { termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff'); termLog(' kpn run <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff'); termLog(' kpn pipeline "<tehtävä>" — nopea: manageri → koodari → testaaja', '#a5d6ff'); termLog(' kpn project "<kuvaus>" — projekti: tiedostojako + generointi + review', '#a5d6ff'); termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff'); termLog(' kpn status — verkon tila', '#a5d6ff'); termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff'); termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff'); return; } if (sub === 'clear') { termPanel.innerHTML = ''; return; } if (sub === 'load') { const arg = parts[2]; const ollamaModels = [ { id: '1', name: 'qwen2.5-coder:0.5b', size: '~400 MB', vram_mb: 0, type: 'selain + Ollama' }, { id: '2', name: 'qwen2.5-coder:1.5b', size: '~1 GB', vram_mb: 1500, type: 'Ollama GPU' }, { id: '3', name: 'qwen2.5-coder:7b', size: '~4.7 GB', vram_mb: 5500, type: 'Ollama GPU', default: true }, { id: '4', name: 'qwen2.5-coder:14b', size: '~9 GB', vram_mb: 10000, type: 'Ollama GPU' }, { id: '5', name: 'qwen2.5-coder:32b', size: '~20 GB', vram_mb: 21000, type: 'Ollama GPU' }, ]; if (!arg) { // Haetaan laitteistotiedot ja näytetään sopivat mallit fetch('/api/v1/hardware').then(r => r.json()).then(hw => { const vram = hw.vram_mb || 0; const ram = hw.ram_mb || 0; const gpu = hw.gpu_name || '?'; const available = vram || ram; // CPU-fallback käyttää RAM:ia if (vram > 0) { termLog(` GPU: ${gpu} | VRAM: ${Math.round(vram/1024)} GB | RAM: ${Math.round(ram/1024)} GB`); } else if (ram > 0) { termLog(` Ei GPU:ta | RAM: ${Math.round(ram/1024)} GB (CPU-moodi)`); } termLog(' Mallit:', '#c9d1d9'); for (const m of ollamaModels) { const fits = m.vram_mb === 0 || m.vram_mb < available; const active = m.default ? ' ← aktiivinen' : ''; const icon = fits ? `${m.id}` : `${m.id}`; const warn = !fits ? ' ⚠ ei mahdu' : ''; termLog(` ${icon} ${fits ? '' : ''}${m.name} ${m.size} | ${m.type}${fits ? '' : ''}${active}${warn}`); } termLog(' Käyttö: kpn load <numero>', '#8b949e'); }).catch(() => { termLog(' Mallit:', '#c9d1d9'); for (const m of ollamaModels) { const active = m.default ? ' ← aktiivinen' : ''; termLog(` ${m.id} ${m.name} ${m.size} | ${m.type}${active}`); } termLog(' Käyttö: kpn load <numero>', '#8b949e'); }); return; } const selected = ollamaModels.find(m => m.id === arg || m.name === arg); if (!selected) { termLog(` Tuntematon malli "${esc(arg)}". Kokeile: kpn load`, '#f85149'); return; } // Selain-WASM (vain 0.5b) if (selected.id === '1') { const btn = document.getElementById('agent-compute-btn'); if (btn?.dataset.state === 'ready') { termLog(' ✓ Qwen2.5-Coder:0.5B on jo ladattu (selain)', '#3fb950'); return; } coderSize = '05b'; termLog(' Ladataan Qwen2.5-Coder:0.5B selaimeen...', '#d29922'); if (btn) btn.click(); else ensureCoderNode(); return; } // Ollama: vaihdetaan malli hubin kautta termLog(` Vaihdetaan Ollama-malli: ${selected.name} (${selected.size})...`, '#d29922'); // Tilaindikaattori const pullLine = document.createElement('div'); pullLine.className = 'terminal-line term-pull'; pullLine.innerHTML = ' ⠋ Ladataan...'; termPanel.appendChild(pullLine); termPanel.scrollTop = termPanel.scrollHeight; const spinFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']; let spinIdx = 0; const spinTimer = setInterval(() => { spinIdx = (spinIdx + 1) % spinFrames.length; const content = pullLine.querySelector('span'); if (content) content.textContent = spinFrames[spinIdx] + ' Ladataan ' + selected.name + '...'; }, 100); // Vaihdetaan malli hubille + Ollama pull fetch('/api/v1/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: selected.name }), }).then(r => r.json()).then(hubData => { clearInterval(spinTimer); pullLine.remove(); if (hubData.status === 'ok') { termLog(` ${selected.name} valittu — natiivisolmu lataa mallin`, '#3fb950'); termLog(` Ensimmäinen pyyntö voi kestää pidempään jos mallia ei ole ladattu`); ollamaModels.forEach(m => m.default = false); selected.default = true; } else { termLog(' ✗ Mallin vaihto epäonnistui', '#f85149'); } }).catch(e => { clearInterval(spinTimer); pullLine.remove(); termLog(` ✗ ${e.message}`, '#f85149'); }); return; } if (sub === 'status') { const nodes = statNodes.textContent || '0'; const vram = statVram.textContent || '?'; termLog(` Solmuja: ${nodes} | VRAM: ${vram} | Tehtäviä: ${statTasks.textContent || '0'}`, '#a5d6ff'); return; } if (sub === 'models') { const allModels = [ { id: '1', name: 'qwen2.5-coder:0.5b', size: '~400 MB', type: 'selain + Ollama' }, { id: '2', name: 'qwen2.5-coder:1.5b', size: '~1 GB', type: 'Ollama GPU' }, { id: '3', name: 'qwen2.5-coder:7b', size: '~4.7 GB', type: 'Ollama GPU' }, { id: '4', name: 'qwen2.5-coder:14b', size: '~9 GB', type: 'Ollama GPU' }, { id: '5', name: 'qwen2.5-coder:32b', size: '~20 GB', type: 'Ollama GPU' }, ]; // Haetaan ladatut mallit Ollamasta Promise.all([ fetch('/api/v1/hardware').then(r => r.json()).catch(() => ({})), fetch('/api/v1/ollama/tags').then(r => r.json()).catch(() => ({ models: [] })), ]).then(([hw, ollama]) => { const loadedNames = (ollama.models || []).map(m => m.name.replace(':latest', '')); const btn = document.getElementById('agent-compute-btn'); const wasmLoaded = btn?.dataset.state === 'ready'; if (hw.gpu_name && hw.gpu_name !== 'ei natiivisolmua') { const vram = hw.vram_mb ? ` | VRAM: ${Math.round(hw.vram_mb/1024)} GB` : ''; termLog(` ${hw.gpu_name}${vram}`); } if (loadedNames.length > 0) { termLog(` Ollama: ${loadedNames.length} mallia ladattu`); } termLog(' Mallit (kpn load <numero>):', '#c9d1d9'); for (const m of allModels) { const nameBase = m.name.split(':')[1]; // "7b", "1.5b" etc const loaded = (m.id === '1' && wasmLoaded) || loadedNames.some(n => n.includes(nameBase)); const status = loaded ? ' ✓ ladattu' : ''; termLog(` ${m.id} ${m.name} ${m.size} | ${m.type}${status}`); } }); return; } if (sub === 'pipeline') { const afterCmd = cmd.replace(/^kpn\s+pipeline\s*/, ''); const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/); const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim(); if (!pTask) { termLog(' Käyttö: kpn pipeline "<tehtävä>"', '#f85149'); return; } kpnPipelineSimple(pTask); return; } if (sub === 'project') { const afterCmd = cmd.replace(/^kpn\s+project\s*/, ''); const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/); const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim(); if (!pTask) { termLog(' Käyttö: kpn project "<projektin kuvaus>"', '#f85149'); termLog(' Esim: kpn project "FastAPI + SQLite REST API for users"', '#8b949e'); return; } kpnPipeline(pTask); return; } if (sub === 'hello') { kpnRun('smollm-135m', 'Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.'); return; } if (sub === 'run') { let model = parts[2]; const afterModel = cmd.replace(/^kpn\s+run\s+\S+\s*/, ''); const promptMatch = afterModel.match(/^"(.+)"$|^'(.+)'$|^(.+)$/); const prompt = (promptMatch && (promptMatch[1] || promptMatch[2] || promptMatch[3] || '')).trim(); if (!model || !prompt) { termLog(' Käyttö: kpn run <agentti/malli> "<prompti>"', '#f85149'); return; } // Jos käyttäjä syötti agentin nimen (esim. "coder"), vaihdetaan se oikeaksi tekoälymalliksi ("qwen-coder") if (model === 'coder-3b') { model = 'qwen-coder-3b'; } else if (agentPrompts[model]) { model = agentPrompts[model].model; } kpnRun(model, prompt); return; } termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149'); } // Tab-completion: ennustava komennonsyöttö sana kerrallaan const kpnCommands = { 'kpn': ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'], 'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'], 'kpn load': ['1', '2'], 'kpn pipeline': ['"'], }; // Esimerkkipromptit malleittain const kpnExamples = { 'kpn run coder': ['"hello world in python"', '"fibonacci in rust"', '"quicksort in javascript"'], 'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'], 'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'], 'kpn run tester': ['"testaa login-toiminto"'], 'kpn project': ['"FastAPI + SQLite CRUD API for users (create, list, update, delete) with HTML form UI served at /"', '"FastAPI todo app with SQLite: CRUD (add, list, toggle done, delete) with simple HTML UI at /"', '"FastAPI bookmark manager with SQLite: CRUD (add, list, edit, delete) with HTML search UI at /"'], 'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'], }; function tabComplete(input) { // Autokorjaus ensin: korjaa typo ja palauta true jos korjattiin const corrected = autocorrect(input.value.trim()); if (corrected && corrected !== input.value.trim()) { input.value = corrected; return true; } const val = input.value; const words = val.trimEnd().split(/\s+/); // Etsitään sopiva täydennystaso // "kpn" → "kpn " alikomennot, "kpn run" → mallit, "kpn run coder" → prompti for (let depth = words.length; depth >= 1; depth--) { const prefix = words.slice(0, depth).join(' '); const partial = words[depth] || ''; // Tarkistetaan esimerkkipromptit ensin if (kpnExamples[prefix] && !partial) { const example = kpnExamples[prefix][Math.floor(Math.random() * kpnExamples[prefix].length)]; input.value = prefix + ' ' + example; return true; } // Komentojen täydennys const candidates = kpnCommands[prefix]; if (candidates) { const matches = partial ? candidates.filter(c => c.startsWith(partial)) : candidates; if (matches.length === 1) { words[depth] = matches[0]; input.value = words.slice(0, depth + 1).join(' ') + ' '; return true; } else if (matches.length > 1 && !partial) { input.value = prefix + ' ' + matches[0]; return true; } else if (matches.length > 1) { // Yhteinen etuliite let common = matches[0]; for (const m of matches) { while (!m.startsWith(common)) common = common.slice(0, -1); } if (common.length > partial.length) { words[depth] = common; input.value = words.slice(0, depth + 1).join(' '); return true; } } } } // Tyhjä input → "kpn " if (!val.trim()) { input.value = 'kpn '; return true; } return false; } // Dropdown-autocompletionin tila const dropdown = document.getElementById('term-dropdown'); let dropdownItems = []; let dropdownIdx = -1; let dropdownPrefix = ''; // Inputin alku joka säilyy valinnan yhteydessä function getCandidates(val) { const words = val.trimEnd().split(/\s+/); for (let depth = words.length; depth >= 1; depth--) { const prefix = words.slice(0, depth).join(' '); const partial = words[depth] || ''; // Esimerkkipromptit if (kpnExamples[prefix] && !partial) { return { items: kpnExamples[prefix], prefix: prefix + ' ' }; } // Komennot const candidates = kpnCommands[prefix]; if (candidates) { const matches = partial ? candidates.filter(c => c.startsWith(partial)) : candidates; if (matches.length > 0) { return { items: matches, prefix: prefix + ' ' }; } } } if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' }; return { items: [], prefix: val }; } function showDropdown(items, prefix) { if (!dropdown || items.length === 0) { hideDropdown(); return; } dropdownItems = items; dropdownPrefix = prefix; dropdownIdx = -1; dropdown.innerHTML = items.map((item, i) => `
${esc(item)}
` ).join(''); dropdown.style.display = 'block'; // Klikkaus-handlerit dropdown.querySelectorAll('.term-dd-item').forEach(el => { el.addEventListener('mouseenter', () => highlightDropdown(parseInt(el.dataset.idx))); el.addEventListener('click', () => { selectDropdown(); termInput.focus(); }); }); } function hideDropdown() { if (dropdown) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; } dropdownItems = []; dropdownIdx = -1; } function highlightDropdown(idx) { dropdownIdx = idx; dropdown.querySelectorAll('.term-dd-item').forEach((el, i) => { el.style.background = i === idx ? '#30363d' : 'transparent'; el.style.color = i === idx ? '#58a6ff' : '#c9d1d9'; }); // Varmistetaan näkyvyys const active = dropdown.children[idx]; if (active) active.scrollIntoView({ block: 'nearest' }); } function selectDropdown() { if (dropdownIdx >= 0 && dropdownIdx < dropdownItems.length) { termInput.value = dropdownPrefix + dropdownItems[dropdownIdx] + (dropdownItems[dropdownIdx].startsWith('"') ? '' : ' '); } hideDropdown(); } termInput?.addEventListener('keydown', (e) => { // Dropdown auki: nuolet navigoi, Enter/Tab valitsee, Esc sulkee if (dropdown && dropdown.style.display === 'block') { if (e.key === 'ArrowDown') { e.preventDefault(); highlightDropdown(Math.min(dropdownIdx + 1, dropdownItems.length - 1)); return; } if (e.key === 'ArrowUp') { e.preventDefault(); highlightDropdown(Math.max(dropdownIdx - 1, 0)); return; } if ((e.key === 'Enter' || e.key === 'Tab') && dropdownIdx >= 0) { e.preventDefault(); selectDropdown(); return; } if (e.key === 'Escape') { e.preventDefault(); hideDropdown(); return; } } if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); hideDropdown(); const val = termInput.value.trimEnd(); if (!val) return; const quoteMatch = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/); if (quoteMatch) { termInput.value = (quoteMatch[1] || quoteMatch[2]).trimEnd() + ' '; } else { const lastSpace = val.lastIndexOf(' '); termInput.value = lastSpace > 0 ? val.substring(0, lastSpace + 1) : ''; } } else if (e.key === 'Tab') { e.preventDefault(); // 1. Autokorjaus ensin const corrected = autocorrect(termInput.value.trim()); if (corrected && corrected !== termInput.value.trim()) { termInput.value = corrected; hideDropdown(); return; } // 2. Dropdown / täydennys const { items, prefix } = getCandidates(termInput.value); if (items.length === 1) { termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' '); hideDropdown(); } else if (items.length > 1) { showDropdown(items, prefix); } } else if (e.key === 'Enter') { hideDropdown(); const cmd = termInput.value.trim(); if (cmd) termExec(cmd); termInput.value = ''; } else if (e.key === 'ArrowUp' && !dropdown?.style.display?.includes('block')) { e.preventDefault(); if (termHistIdx < termHistory.length - 1) { termHistIdx++; termInput.value = termHistory[termHistIdx]; } } else if (e.key === 'ArrowDown' && !dropdown?.style.display?.includes('block')) { e.preventDefault(); if (termHistIdx > 0) { termHistIdx--; termInput.value = termHistory[termHistIdx]; } else { termHistIdx = -1; termInput.value = ''; } } }); // Suljetaan dropdown kun klikataan muualle document.addEventListener('click', (e) => { if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDropdown(); }); // Klikkaa terminaalipaneelia → fokusoi input termPanel?.addEventListener('click', () => termInput?.focus()); // Tallennetaan message-handler funktioon jotta reconnect voi käyttää samaa const _wsHandler = (event) => { try { const raw = event.data; if (raw.includes('"single_tokenize"')) return; const data = JSON.parse(raw); if (data.type === "stats") { statNodes.textContent = data.nodes; statVram.textContent = data.vram_gb + " GB"; if (data.tasks !== undefined) { statTasks.textContent = data.tasks; } if (data.version) { document.getElementById('hub-version').textContent = 'v' + data.version; } } else if (data.type === "node_joined") { chatBox.classList.remove('hidden'); } else if (data.type === "download_progress") { const dlBar = document.getElementById('download-bar'); if (data.pct < 100) { dlBar.style.display = 'block'; document.getElementById('dl-label').textContent = `Ladataan: ${data.file}`; document.getElementById('dl-pct').textContent = data.pct + '%'; document.getElementById('dl-fill').style.width = data.pct + '%'; document.getElementById('dl-detail').textContent = `${data.loaded_mb} / ${data.total_mb} MB`; } else { dlBar.style.display = 'none'; } // Terminaaliin latauksen edistyminen const term = document.getElementById('agent-terminal'); if (term) { let dlLine = term.querySelector('.term-download'); if (data.pct >= 100) { if (dlLine) dlLine.remove(); termLog(` ${data.file} ladattu`, '#a5d6ff'); } else { if (!dlLine) { dlLine = document.createElement('div'); dlLine.className = 'terminal-line term-download'; term.appendChild(dlLine); } const bar = '█'.repeat(Math.floor(data.pct / 5)) + '░'.repeat(20 - Math.floor(data.pct / 5)); dlLine.innerHTML = ` ${data.file} ${bar} ${data.pct}% ${data.loaded_mb}/${data.total_mb} MB`; term.scrollTop = term.scrollHeight; } } } else if (data.type === "single_tokenize_done") { chatBox.classList.remove('hidden'); const r = data.result || {}; const ms = data.duration_ms || 0; const nodeId = data.node_id || '?'; const cpt = parseFloat((r.chars_per_token || 0).toFixed(2)); const cptColor = cpt >= 4 ? "#3fb950" : cpt >= 3 ? "#d29922" : "#f85149"; const renderTokens = (tokens) => (tokens || []).map(t => `${esc(t)}` ).join(''); const tokHtml = renderTokens(r.tokens); const detailId = 'stok-' + Date.now(); const msgDiv = document.createElement('div'); msgDiv.className = 'chat-msg'; msgDiv.innerHTML = `
Solmu #${nodeId}
${typeof ms === 'number' ? ms.toFixed(2) : ms}ms
"${esc(r.text)}"
${r.char_count || 0} merkkiä ${r.word_count || 0} sanaa ${r.token_count || 0} tokenia ${cpt} merkkiä/token
(${r.token_count || 0}) ${tokHtml}
`; chatBox.appendChild(msgDiv); if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild); chatBox.scrollTop = chatBox.scrollHeight; flashComputing(); } else if (data.type === "pair_task" && selectedTask === 'tokenize') { chatBox.classList.remove('hidden'); if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) { chatBox.innerHTML = ''; } const msgDiv = document.createElement('div'); msgDiv.className = 'chat-msg'; msgDiv.innerHTML = `Tokenisoidaan...
EN "${esc(data.en)}"
FI "${esc(data.fi)}"
`; chatBox.appendChild(msgDiv); if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild); chatBox.scrollTop = chatBox.scrollHeight; } else if (data.type === "pair_done") { chatBox.classList.remove('hidden'); const en = data.en || {}; const fi = data.fi || {}; const overhead = data.overhead_pct || 0; const nodeId = data.node_id || "?"; const ms = data.duration_ms || 0; // Päivitetään metriikat metrics.tasks++; metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0); metrics.totalTimeMs += ms; updateMetrics(); flashComputing(); // Lokiboksiin yhteenveto console.log(`EN: ${en.token_count} tokenia (${(en.chars_per_token||0).toFixed(2)} m/t) vs FI: ${fi.token_count} tokenia (${(fi.chars_per_token||0).toFixed(2)} m/t) | ylikustannus: ${overhead}% | ${typeof ms === 'number' ? ms.toFixed(2) : ms}ms`); const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2)); const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2)); // Värit tehokkuudelle const cptColor = (v) => v >= 4 ? "#3fb950" : v >= 3 ? "#d29922" : "#f85149"; // Ylikustannuksen väri const ovColor = overhead > 20 ? "#f85149" : overhead > 0 ? "#d29922" : "#3fb950"; // Korvataan viimeisin "Tokenisoidaan..."-viesti, tai luodaan uusi const lastMsg = chatBox.lastElementChild; const msgDiv = (lastMsg && lastMsg.querySelector('.chat-prompt')?.textContent === 'Tokenisoidaan...') ? lastMsg : document.createElement('div'); msgDiv.className = 'chat-msg'; // Tokenilistat renderöitäväksi const renderTokens = (tokens, cls) => (tokens || []).map(t => `${esc(t)}` ).join(''); const enTokHtml = renderTokens(en.tokens, 'tok-en'); const fiTokHtml = renderTokens(fi.tokens, 'tok-fi'); const detailId = 'tok-' + Date.now(); msgDiv.innerHTML = `
Solmu #${nodeId}
${typeof ms === 'number' ? ms.toFixed(2) : ms}ms
EN "${esc(en.text)}" ${en.char_count} m ${en.token_count} tok ${enCpt} m/t FI "${esc(fi.text)}" ${fi.char_count} m ${fi.token_count} tok ${fiCpt} m/t
EN (${en.token_count}) ${enTokHtml}
FI (${fi.token_count}) ${fiTokHtml}
(${fi.token_count} / ${en.token_count} − 1) × 100 = ${overhead > 0 ? '+' : ''}${overhead}% FI ylikustannus: ${overhead > 0 ? '+' : ''}${overhead}%
`; if (!msgDiv.parentNode) chatBox.appendChild(msgDiv); if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild); chatBox.scrollTop = chatBox.scrollHeight; } else if (data.type === "llm_done") { // Reititetäänkö agents-näkymään vai codelab-näkymään? const isAgentsTask = data.task_id && activeStreams[data.task_id]; const isCoder = (data.model || '').includes('Coder'); if (isAgentsTask) { // Agents-pipeline: päivitetään terminaali const term = document.getElementById('agent-terminal'); if (term) { const model = data.model || 'llm'; const tokGen = data.tokens_generated || 0; const durMs = typeof data.duration_ms === 'number' ? data.duration_ms.toFixed(0) : data.duration_ms || '?'; const tokS = data.tokens_per_sec || '?'; const div = document.createElement('div'); div.className = 'terminal-line'; div.style.color = '#a5d6ff'; div.innerHTML = ` ✓ ${model} ${tokGen} tok | ${durMs}ms | ${tokS} tok/s`; term.appendChild(div); while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild); term.scrollTop = term.scrollHeight; // Avatar-aktivointi vain oikeille käyttäjäpyynnöille if (data.task_id) { document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active')); document.getElementById('avatar-kpn')?.classList.add('active'); } } } else if (isCoder) { // Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364) // Poistetaan vain streaming-kortti codelabista if (codeResults) codeResults.querySelector('.streaming-card')?.remove(); } else { // Muu malli (network-näkymä): näytetään chatBoxissa chatBox.querySelector('.streaming-card')?.remove(); chatBox.classList.remove('hidden'); const nodeId = data.node_id || "?"; const model = data.model || "LLM"; const tokGen = data.tokens_generated || 0; const durMs = data.duration_ms || 0; const tokS = data.tokens_per_sec || 0; const loadMs = data.load_time_ms || 0; const msgDiv = document.createElement('div'); msgDiv.className = 'chat-msg'; msgDiv.style.borderLeftColor = '#a371f7'; msgDiv.innerHTML = `
Solmu #${nodeId} — ${model} ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s
Prompt: "${esc(stripSystemPrompt(data.prompt))}"
${data.response ? highlightCode(data.response) : 'tyhjä vastaus'}
${tokGen} tokenia generoitu | malli ladattu: ${typeof loadMs === 'number' ? loadMs.toFixed(0) : loadMs}ms
`; chatBox.appendChild(msgDiv); if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild); chatBox.scrollTop = chatBox.scrollHeight; } metrics.tasks++; metrics.totalTokens += (data.tokens_generated || 0); metrics.totalTimeMs += (data.duration_ms || 0); flashComputing(); updateMetrics(); console.log(`[${data.model || 'LLM'}] ${data.tokens_generated || 0} tokenia | ${typeof data.duration_ms === 'number' ? data.duration_ms.toFixed(0) : data.duration_ms || '?'}ms | ${data.tokens_per_sec || '?'} tok/s | "${(data.response || '').substring(0, 60)}..."`); } else if (data.type === "llm_error") { // Virheenkäsittely: siivotaan streaming-tila const errMsg = data.error || 'Tuntematon virhe'; if (data.task_id && activeStreams[data.task_id]) { // Agents-pipeline: näytetään virhe terminaalissa activeStreams[data.task_id].remove(); delete activeStreams[data.task_id]; } chatBox.querySelector('.streaming-card')?.remove(); if (codeResults) codeResults.querySelector('.streaming-card')?.remove(); const term = document.getElementById('agent-terminal'); if (term) { const div = document.createElement('div'); div.className = 'terminal-line'; div.style.color = '#f85149'; div.innerHTML = ` ✗ LLM-virhe: ${errMsg}`; term.appendChild(div); term.scrollTop = term.scrollHeight; } console.warn('[LLM Error]', errMsg); } else if (data.type === "llm_chunk") { // Agents-terminaalin streaming: päivitetään aktiivinen rivi task_id:n perusteella if (data.task_id && activeStreams[data.task_id]) { const streamDiv = activeStreams[data.task_id]; const contentEl = streamDiv.querySelector('.stream-content'); if (contentEl) { contentEl.textContent += data.token || ''; termPanel.scrollTop = termPanel.scrollHeight; } // Agents-pipeline omistaa tämän chunkin, ei näytetä muualla } else { // Ei agents-task → näytetään streaming-kortti oikeassa näkymässä const model = data.model || ''; const isCoder = model.includes('Coder'); const targetBox = isCoder ? codeResults : chatBox; if (targetBox) { let streamEl = targetBox.querySelector('.streaming-card'); if (!streamEl) { streamEl = document.createElement('div'); streamEl.className = isCoder ? 'code-task-card streaming-card' : 'chat-msg streaming-card'; streamEl.style.borderLeftColor = '#a371f7'; streamEl.innerHTML = `
${model} 0 tok
Prompt: "${esc(stripSystemPrompt(data.prompt))}"
Generating...
`; if (isCoder) { targetBox.insertBefore(streamEl, targetBox.firstChild); } else { targetBox.appendChild(streamEl); } } const textEl = streamEl.querySelector('.stream-text'); const counterEl = streamEl.querySelector('.stream-counter'); if (textEl) textEl.textContent += data.token || ''; const tokCount = (textEl.textContent || '').split('').length; if (counterEl) counterEl.textContent = tokCount + ' tok'; targetBox.scrollTop = targetBox.scrollHeight; } } } else if (data.type === "task_routed") { const isQueued = data.status === 'queued'; const color = isQueued ? '#d29922' : '#8b949e'; const icon = isQueued ? '⏳' : '→'; const msg = esc(data.message || ''); // Päivitetään olemassaoleva status-rivi (kpnRun luo sen) const statusDiv = document.getElementById('status-' + data.task_id); if (statusDiv) { statusDiv.innerHTML = ` ${icon} ${msg}${isQueued ? '' : ' '}`; termPanel.scrollTop = termPanel.scrollHeight; } // Codelab-loading-teksti const codeLoading = document.getElementById('code-loading'); if (codeLoading && codeLoading.style.display !== 'none') { codeLoading.textContent = isQueued ? `⏳ ${msg}` : `→ ${msg} — generoidaan...`; } } else if (data.type === "llm_prompt") { // Reagoidaan VAIN agents-pipelinen tehtäviin (task_id + activeStreams) if (data.task_id && activeStreams[data.task_id]) { const term = document.getElementById('agent-terminal'); if (term) { const model = data.model || 'llm'; const promptShort = esc(stripSystemPrompt(data.prompt)).substring(0, 50); const div = document.createElement('div'); div.className = 'terminal-line'; div.innerHTML = `$ kpn run ${model} "${promptShort}"`; term.appendChild(div); while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild); term.scrollTop = term.scrollHeight; } // Avatar-aktivointi vain oikeille käyttäjäpyynnöille (task_id) if (data.task_id) { document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active')); const model = data.model || ''; const p = data.prompt ? data.prompt.toLowerCase() : ''; if (p.includes('tiiminvetäjä') || p.includes('pilko')) { document.getElementById('avatar-kpn')?.classList.add('active'); } else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) { document.getElementById('avatar-tester')?.classList.add('active'); } else if (p.includes('tervehdi')) { document.getElementById('avatar-client')?.classList.add('active'); } else if (p.includes('test')) { document.getElementById('avatar-qa')?.classList.add('active'); } else if (model.includes('coder') || model.includes('Coder')) { document.getElementById('avatar-coder')?.classList.add('active'); } else if (model.includes('deepseek') || model.includes('r1')) { document.getElementById('avatar-observer')?.classList.add('active'); } } } } } catch(e) {} }; window._wsMessageHandler = _wsHandler; if (uiSocket) uiSocket.onmessage = _wsHandler; btn.addEventListener('click', async () => { // Käytetään viewer-authissa jo tunnistettua WebGPU-tilaa let hasWebGPU = detectedWebGPU; const deviceInfo = { allocated_gb: 4, cpu_cores: navigator.hardwareConcurrency || 0, device_memory_gb: navigator.deviceMemory || 0, platform: navigator.platform || "", gpu: detectedGpuInfo, selected_task: selectedTask }; const gpuStr = hasWebGPU ? (deviceInfo.gpu?.description || deviceInfo.gpu?.vendor || "WebGPU") : "ei GPU:ta"; // Laskenta käyttää aina CPU:ta (Candle), WebGPU on vain tensorilaskennassa (Burn) const computeBackend = (selectedTask === 'tokenize') ? (hasWebGPU ? "WebGPU + CPU" : "CPU") : "CPU (Candle Wasm)"; const vramStr = deviceInfo.gpu?.estimated_vram_gb ? `~${deviceInfo.gpu.estimated_vram_gb} GB` : "?"; const ramNote = deviceInfo.device_memory_gb >= 8 ? "8+ GB (selaimen raja)" : `~${deviceInfo.device_memory_gb} GB`; // Näytetään laitetiedot paneelissa const diPanel = document.getElementById('device-info'); diPanel.style.display = 'block'; diPanel.innerHTML = [ `Laskenta: ${computeBackend}`, hasWebGPU ? `GPU: ${gpuStr}` : `GPU: ei WebGPU:ta`, hasWebGPU ? `VRAM: ${vramStr}` : null, `CPU: ${deviceInfo.cpu_cores} ydintä`, `RAM: ${ramNote}`, `Varaus: ${deviceInfo.allocated_gb} GB` ].filter(Boolean).join(' · '); // Yhteensopivuusbanneri const banner = document.getElementById('compat-banner'); banner.style.display = 'block'; if (hasWebGPU) { banner.className = 'compat-banner gpu'; banner.innerHTML = `WebGPU tunnistettu — ${gpuStr}. Tokenisaatio käyttää GPU:ta, LLM-inferenssi CPU:ta (Candle Wasm).`; } else { // Tunnistetaan selain ohjeen personointia varten const ua = navigator.userAgent; const isFirefox = ua.includes('Firefox'); const isChrome = ua.includes('Chrome') && !ua.includes('Edg'); const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave); const isSafari = ua.includes('Safari') && !ua.includes('Chrome'); const isLinux = ua.includes('Linux'); let browserTip = ''; if (isFirefox) { browserTip = `

Firefox ei tue WebGPU:ta oletuksena.

Ota käyttöön: about:configdom.webgpu.enabled = true → käynnistä uudelleen.

Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.

`; } else if ((isChrome || isBrave) && isLinux) { const browser = isBrave ? 'brave-browser' : 'google-chrome'; browserTip = `

${isBrave ? 'Brave' : 'Chrome'} + Linux: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.

Kokeile käynnistää selain komentoriviltä:

${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11`; } else if (isSafari) { browserTip = `

Safari: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).

Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.

`; } else { browserTip = `

Selaimesi ei tue WebGPU:ta. Kokeile Chrome 113+ tai Brave.

`; } banner.className = 'compat-banner cpu'; banner.innerHTML = `
CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita ${browserTip}

Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.

`; } document.getElementById('initial-state').classList.add('hidden'); document.getElementById('active-state').classList.remove('hidden'); document.getElementById('user-input-box').classList.remove('hidden'); btn.style.display = 'none'; // Nappin teksti ja placeholder tehtävän mukaan const sendBtnEl = document.getElementById('send-btn'); const placeholderEl = document.getElementById('user-text'); const t = window.currentLangDict || translations.fi; if (selectedTask === 'tokenize') { sendBtnEl.textContent = t.btn_tokenize || 'Tokenisoi'; } else if (selectedTask === 'qwen-coder') { sendBtnEl.textContent = 'Koodaa'; } else { sendBtnEl.textContent = 'Generoi'; } try { if (!wasmInitialized) { console.log("Ladataan Burn Wasm -binääriä..."); await init(); wasmInitialized = true; } window.wasm_active = true; metrics.startTime = Date.now(); // Asetetaan Connected-tila (keltainen) — vihreäksi vasta kun laskentaa tapahtuu const nodeStatusEl = document.getElementById('node-status'); nodeStatusEl.textContent = 'Connected'; nodeStatusEl.style.color = '#d29922'; // Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen set_gpu_load(parseInt(loadSlider.value)); // WebAssembly yhdistää oikeaksi Agent Nodeksi const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`; const taskIds = {'tokenize': 0, 'smollm-135m': 1, 'qwen-05b': 2, 'phi3-mini': 3, 'qwen-coder-05b': 4, 'qwen-coder-3b': 5}; const taskId = taskIds[selectedTask] || 0; await start_agent_node(wsUrl, hasWebGPU, JSON.stringify(deviceInfo), taskId); } catch(e) { console.log("Virhe GPU-käynnistyksessä: " + e); } }); // === Koodilaboratorio === const codeInput = document.getElementById('code-input'); const codeSendBtn = document.getElementById('code-send-btn'); const codeResults = document.getElementById('code-results'); const codeLoading = document.getElementById('code-loading'); let coderWsReady = false; let coderWs = null; // Erillinen WS coder-nodelle let pendingCodePrompt = null; // Yksinkertainen Python-syntaksikorostus function highlightCode(code) { if (typeof hljs !== 'undefined') { try { const result = hljs.highlightAuto(code); return result.value; } catch(e) {} } return esc(code); } function addCodeResult(data) { // Poistetaan streaming-kortti codeResults.querySelector('.streaming-card')?.remove(); const model = data.model || 'Coder'; const tokGen = data.tokens_generated || 0; const durMs = data.duration_ms || 0; const tokS = data.tokens_per_sec || 0; const response = esc(data.response); codeMetrics.tasks++; codeMetrics.tokens += tokGen; codeMetrics.lastSpeed = tokS; document.getElementById('code-m-tasks').textContent = codeMetrics.tasks; document.getElementById('code-m-tokens').textContent = codeMetrics.tokens.toLocaleString('fi-FI'); document.getElementById('code-m-speed').textContent = tokS + ' tok/s'; if (codeResults.querySelector('[data-placeholder]')) { codeResults.innerHTML = ''; } codeLoading.style.display = 'none'; codeSendBtn.disabled = false; codeSendBtn.textContent = 'Generate'; document.getElementById('coder-status').textContent = 'Connected'; document.getElementById('coder-status').style.color = '#d29922'; const card = document.createElement('div'); card.className = 'code-task-card'; card.innerHTML = `
${esc(stripSystemPrompt(data.prompt))}
${highlightCode(response)}
${model} · ${tokGen} tokenia · ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms · ${tokS} tok/s
`; codeResults.insertBefore(card, codeResults.firstChild); if (codeResults.children.length > 10) codeResults.removeChild(codeResults.lastChild); } // Kuuntele coder-tuloksia UI WebSocketista (vain codelab-tehtävät) uiSocket.addEventListener('message', (event) => { try { const data = JSON.parse(event.data); if (data.type === 'llm_done' && (data.model || '').includes('Coder')) { // Agents-pipeline asettaa aina task_id:n, codelabin user_text-polku ei koskaan if (data.task_id) return; addCodeResult(data); } } catch(e) {} }); // Pipeline-vaiheiden päivitys function setStep(id, state, extra) { const el = document.getElementById(id); if (!el) return; el.className = 'code-step ' + state; const icon = el.querySelector('.step-icon'); if (state === 'active') icon.textContent = '\u25F7'; // spinning else if (state === 'done') icon.textContent = '\u2713'; else if (state === 'error') icon.textContent = '\u2717'; if (extra) { const pct = document.getElementById(id + '-pct'); if (pct) pct.textContent = extra; } } // Kuuntele console.log-viestejä pipeline-vaiheiden seuraamiseksi // Terminaalin lataustilarivi — päivittyy dynaamisesti function termLoadStatus(phase, detail) { const term = document.getElementById('agent-terminal'); if (!term) return; let statusLine = term.querySelector('.term-load-status'); if (!statusLine) { statusLine = document.createElement('div'); statusLine.className = 'terminal-line term-load-status'; term.appendChild(statusLine); } const spinner = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']; const frame = spinner[Math.floor(Date.now() / 100) % spinner.length]; statusLine.innerHTML = ` ${frame} ${phase}${detail ? ` ${detail}` : ''}`; term.scrollTop = term.scrollHeight; } function termLoadDone() { const term = document.getElementById('agent-terminal'); if (!term) return; const statusLine = term.querySelector('.term-load-status'); if (statusLine) statusLine.remove(); } const origCodeLog = console.log; const codeLogListener = (...args) => { const msg = args.join(' '); if (msg.includes('[Coder]') || msg.includes('[Storage]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) { // Terminaalin lataustilapäivitys if (msg.includes('Agent Node käynnistyy')) termLoadStatus('WASM alustettu'); if (msg.includes('Ladataan') && msg.includes('tokenizer')) termLoadStatus('Ladataan tokenizer...'); if (msg.includes('tokenizer') && (msg.includes('löytyi') || msg.includes('tallennettu'))) termLoadStatus('Tokenizer ✓'); if (msg.includes('Ladataan') && msg.includes('gguf')) termLoadStatus('Ladataan mallia...'); const dlMatch = msg.match(/lataus: (\d+)%/); if (dlMatch) termLoadStatus('Ladataan mallia...', dlMatch[1] + '%'); if (msg.includes('tallennettu') && msg.includes('gguf')) termLoadStatus('Malli tallennettu'); if (msg.includes('Rakennetaan')) termLoadStatus('Rakennetaan mallia...'); if (msg.includes('Malli ladattu')) termLoadDone(); if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active'); if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); } // Tokenizer: [Coder] tai [Storage] -prefiksi if (msg.includes('Tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); } if (msg.includes('tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); } if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('Ladataan') && msg.includes('tokenizer')) { setStep('step-tokenizer', 'active'); } if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('tokenizer') && msg.includes('tallennettu')) { setStep('step-tokenizer', 'done'); } if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('lataus:')) { setStep('step-model', 'active'); const match = msg.match(/lataus: (\d+)%/); if (match) setStep('step-model', 'active', match[1] + '%'); } if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('löytyi')) { setStep('step-model', 'done', 'cache'); } if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('tallennettu')) { setStep('step-model', 'done', '100%'); } if (msg.includes('[Coder]') && msg.includes('Rakennetaan')) { setStep('step-build', 'active'); } if (msg.includes('Agent Node käynnistyy') || msg.includes('Rakennetaan')) { const cd = document.getElementById('agent-compute-dot'); const cl = document.getElementById('agent-compute-label'); const btn = document.getElementById('agent-compute-btn'); if (cd) cd.style.background = '#d29922'; if (cl) { cl.textContent = 'Ladataan...'; cl.style.color = '#d29922'; } if (btn && btn.dataset.state !== 'ready') { btn.dataset.state = 'loading'; btn.textContent = 'Peruuta'; btn.style.borderColor = '#f85149'; btn.style.color = '#f85149'; } } if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) { // Malli on valmis — merkataan kaikki vaiheet valmiiksi setStep('step-wasm', 'done'); setStep('step-tokenizer', 'done'); const pctSpan = document.getElementById('step-model-pct'); if (pctSpan && pctSpan.textContent.includes('100%')) { setStep('step-model', 'done', '100%'); } else { setStep('step-model', 'done', 'cache'); } setStep('step-build', 'done'); setStep('step-ready', 'done'); // Agents-sivun compute-status: valmis const cd = document.getElementById('agent-compute-dot'); const cl = document.getElementById('agent-compute-label'); const btn = document.getElementById('agent-compute-btn'); if (cd) cd.style.background = '#3fb950'; const sizeLabel = coderSize === '3b' ? '3B (3 miljardia parametria)' : '0.5B (500 miljoonaa parametria)'; if (cl) { cl.textContent = 'Qwen2.5-Coder:' + (coderSize === '3b' ? '3B' : '0.5B'); cl.style.color = '#3fb950'; cl.title = sizeLabel + ' · Candle Wasm · CPU · max 512 tok'; } if (btn) { btn.dataset.state = 'ready'; btn.textContent = '✓ Valmis'; btn.style.borderColor = '#3fb950'; btn.style.color = '#3fb950'; btn.style.cursor = 'default'; btn.title = 'Kielimalli ladattu — oma kone on valmis laskentaan'; } localStorage.setItem('kpn-coder-loaded', 'true'); // Terminaaliin valmis-viesti (vain kerran) if (!window._coderReadyLogged) { window._coderReadyLogged = true; const term = document.getElementById('agent-terminal'); if (term) { const sLabel = coderSize === '3b' ? 'Qwen2.5-Coder:1.5B Q4' : 'Qwen2.5-Coder:0.5B'; termLog(` ${sLabel} valmis — kpn run coder "prompti"`, '#3fb950'); } } } if (msg.includes('[Coder]') && msg.includes('Syöte:')) { // Pipeline piiloon kun generointi alkaa setTimeout(() => { document.getElementById('code-pipeline').style.display = 'none'; }, 1000); } } }; // Lisätään kuuntelija alkuperäisen console.log ylikirjoituksen päälle const _prevConsoleLog = console.log; console.log = function(...args) { _prevConsoleLog.apply(console, args); codeLogListener(...args); }; // Web Worker -pohjainen laskentasolmu — UI ei jäädy inferenssin aikana let coderWorker = null; async function ensureCoderNode() { if (coderJoined) return; coderJoined = true; document.getElementById('coder-status').textContent = 'Käynnistyy...'; document.getElementById('coder-status').style.color = '#d29922'; document.getElementById('code-pipeline').style.display = 'block'; setStep('step-wasm', 'active'); try { // Käynnistetään WASM Web Workerissa coderWorker = new Worker('./worker.js', { type: 'module' }); // Workerin console.log-viestit → pääsäikeen kuuntelija // Worker ei voi kutsua console.log näkyvästi, joten WASM:n console_log // ei näy automaattisesti. Workerissa console.log menee Workerin konsoliin. await new Promise((resolve, reject) => { coderWorker.onmessage = (e) => { if (e.data.type === 'ready') resolve(); else if (e.data.type === 'error') reject(new Error(e.data.message)); }; coderWorker.postMessage({ type: 'init' }); }); setStep('step-wasm', 'done'); setStep('step-tokenizer', 'active'); const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`; const deviceInfo = { allocated_gb: 4, cpu_cores: navigator.hardwareConcurrency || 0, device_memory_gb: navigator.deviceMemory || 0, platform: navigator.platform || "", gpu: null, selected_task: coderSize === '3b' ? 'qwen-coder-3b' : 'qwen-coder-05b' }; const taskId = coderSize === '3b' ? 5 : 4; // Käynnistetään node Workerissa coderWorker.onmessage = (e) => { if (e.data.type === 'started') { document.getElementById('coder-status').textContent = 'Connected'; document.getElementById('coder-status').style.color = '#d29922'; coderWsReady = true; } else if (e.data.type === 'log') { // Workerin console.log → pääsäikeen kuuntelijat (tilaindikaattori, pipeline-stepit) console.log(e.data.message); } else if (e.data.type === 'error') { console.log('[Worker] Virhe: ' + e.data.message); } }; coderWorker.postMessage({ type: 'start', data: { hubUrl: wsUrl, hasWebGPU: false, deviceInfo: JSON.stringify(deviceInfo), taskId } }); // Warmup setTimeout(() => { if (uiSocket && uiSocket.readyState === 1) { uiSocket.send(JSON.stringify({ type: 'user_text', text: '{"prompt":"warmup","max_tokens":1}', task_type: 'qwen-coder' })); } }, 500); if (pendingCodePrompt) { setTimeout(() => { sendCodeToHub(pendingCodePrompt); }, 2000); pendingCodePrompt = null; } } catch(e) { console.log("Coder-virhe: " + e); document.getElementById('coder-status').textContent = 'Virhe'; document.getElementById('coder-status').style.color = '#f85149'; coderJoined = false; } } // Mallia EI ladata automaattisesti — käyttäjä käynnistää itse: kpn load // Laskentasolmun käynnistys/pysäytys -nappi let computeAbortController = null; document.getElementById('agent-compute-btn')?.addEventListener('click', () => { const btn = document.getElementById('agent-compute-btn'); const cl = document.getElementById('agent-compute-label'); if (!btn) return; if (btn.dataset.state === 'ready') return; // Jo valmis, ei tehdä mitään if (btn.dataset.state === 'loading') { // Cancel — ladataan sivua uudelleen koska Wasm-latausta ei voi pysäyttää btn.textContent = 'Peruutetaan...'; btn.disabled = true; window.location.reload(); return; } // Käynnistetään btn.dataset.state = 'loading'; btn.textContent = 'Peruuta'; btn.style.borderColor = '#f85149'; btn.style.color = '#f85149'; btn.title = 'Peruuta kielimallin lataus'; ensureCoderNode(); }); // JSON mode toggle const jsonToggle = document.getElementById('json-mode-toggle'); const jsonHelp = document.getElementById('json-help'); const textInput = document.getElementById('code-input'); const jsonInput = document.getElementById('code-input-json'); jsonToggle?.addEventListener('change', () => { if (jsonToggle.checked) { textInput.style.display = 'none'; jsonInput.style.display = 'block'; jsonHelp.style.display = 'block'; } else { textInput.style.display = 'block'; jsonInput.style.display = 'none'; jsonHelp.style.display = 'none'; } }); function sendCodeToHub(text) { if (uiSocket && uiSocket.readyState === 1) { uiSocket.send(JSON.stringify({ type: 'user_text', text: text, task_type: 'qwen-coder' })); } } async function handleCodeSubmit() { let promptText; if (jsonToggle.checked) { // JSON mode const raw = jsonInput.value.trim(); if (!raw) return; try { const parsed = JSON.parse(raw); if (!parsed.prompt) { alert('JSON must contain "prompt" field'); return; } // Lähetetään koko JSON hubille — node lukee promptin ja parametrit promptText = raw; } catch(e) { alert('Invalid JSON: ' + e.message); return; } } else { // Text mode promptText = textInput.value.trim(); if (!promptText) return; textInput.value = ''; } codeSendBtn.disabled = true; codeSendBtn.textContent = 'Generating...'; codeLoading.style.display = 'block'; if (!coderJoined) { pendingCodePrompt = promptText; const dlSize = coderSize === '3b' ? '~6.2 GB' : '~990 MB'; codeLoading.textContent = `Loading Qwen2.5-Coder:${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`; await ensureCoderNode(); } else { codeLoading.textContent = 'Generating code...'; document.getElementById('coder-status').textContent = 'Computing'; document.getElementById('coder-status').style.color = 'var(--success-color)'; sendCodeToHub(promptText); } } codeSendBtn?.addEventListener('click', handleCodeSubmit); textInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleCodeSubmit(); }); const translations = { fi: { main_title: "Kipinä Agentic Playground", main_subtitle: "AI-ohjelmistokehitystiimi", tab_network: "Laskentaverkko", tab_codelab: "Koodilaboratorio", tab_agents: "Kipinä Agentic Playground", stat_nodes_lbl: "Aktiivisia Nodeja", stat_tasks_lbl: "Verkossa Suoritettua Tehtävää (Globaali)", stat_vram_lbl: "Verkon yhteis-VRAM", btn_select_all: "Valitse kaikki", btn_clear_all: "Tyhjennä valinnat", task_title: "Valitse tehtävä", btn_join: "Liity laskentaverkkoon", btn_disconnect: "Katkaise Yhteys", resource_mgmt: "Resurssien hallinta", power_limiter: "Laskentatehon rajoitin", auto_tasks: "Vastaanota automaattisia tehtäviä hubilta", try_own_text: "Kokeile omaa tekstiä:", btn_tokenize: "Tokenisoi", btn_code: "Koodaa", btn_generate: "Generoi", metric_tasks: "Tehtäviä", metric_avg: "Ka. aika", metric_tokens: "Tokeneita", metric_uptime: "Käynnissä" }, se: { main_title: "Kipinä Agentic Playground", main_subtitle: "AI-programvaruutvecklingsteam", tab_network: "Kalkylnätverk", tab_codelab: "Kodlaboratorium", tab_agents: "Kipinä Agentic Playground", stat_nodes_lbl: "Aktiva Noder", stat_tasks_lbl: "Slutförda Uppgifter (Globalt)", stat_vram_lbl: "Nätverkets totala VRAM", btn_select_all: "Välj alla", btn_clear_all: "Rensa val", task_title: "Välj uppgift", btn_join: "Gå med i nätverket", btn_disconnect: "Koppla från", resource_mgmt: "Resurshantering", power_limiter: "Beräkningskraftsbegränsare", auto_tasks: "Ta emot automatiska uppgifter från hubben", try_own_text: "Prova med egen text:", btn_tokenize: "Tokenisera", btn_code: "Koda", btn_generate: "Generera", metric_tasks: "Uppgifter", metric_avg: "Snittid", metric_tokens: "Tokens", metric_uptime: "Drifttid" }, en: { main_title: "Kipinä Agentic Playground", main_subtitle: "AI Software Development Team", tab_network: "Compute Network", tab_codelab: "Code Laboratory", tab_agents: "Kipinä Agentic Playground", stat_nodes_lbl: "Active Nodes", stat_tasks_lbl: "Tasks Completed (Global)", stat_vram_lbl: "Total Network VRAM", btn_select_all: "Select all", btn_clear_all: "Clear selection", task_title: "Choose task", btn_join: "Join Compute Network", btn_disconnect: "Disconnect", resource_mgmt: "Resource Management", power_limiter: "Compute Power Limiter", auto_tasks: "Receive automatic tasks from hub", try_own_text: "Test your own text:", btn_tokenize: "Tokenize", btn_code: "Code", btn_generate: "Generate", metric_tasks: "Tasks", metric_avg: "Avg. Time", metric_tokens: "Tokens", metric_uptime: "Uptime" } }; window.setLanguage = function(lang) { localStorage.setItem('kpn_lang', lang); document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active')); const btn = document.querySelector(`.lang-btn[data-lang="${lang}"]`); if (btn) btn.classList.add('active'); const t = translations[lang] || translations.fi; window.currentLangDict = t; document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (t[key]) { if(t[key].includes('<')) el.innerHTML = t[key]; else el.textContent = t[key]; } }); if(window.updatePromptEditor) window.updatePromptEditor(); // Käännä lennossa ne painikkeet jotka ovat ehkä vaihtaneet tekstiä dynaamisesti (esim. JS-tilan muutokset) const sendBtnEl = document.getElementById('send-btn'); if (sendBtnEl && window.wasm_active) { // Riippuu valitusta tehtävästä const sTask = window.selectedTask || document.querySelector('.task-option.selected')?.dataset?.task; if (sTask === 'tokenize') sendBtnEl.textContent = t.btn_tokenize || 'Tokenisoi'; else if (sTask === 'qwen-coder') sendBtnEl.textContent = t.btn_code || 'Koodaa'; else sendBtnEl.textContent = t.btn_generate || 'Generoi'; } const jbtn = document.getElementById('start-btn'); if (jbtn) { // start-btn vaihtuu connect / disconnect kun ollaan aktiivitilassa if (window.wasm_active || jbtn.textContent === 'Katkaise Yhteys' || jbtn.textContent === 'Koppla från' || jbtn.textContent === 'Disconnect') { jbtn.textContent = t.btn_disconnect || 'Katkaise Yhteys'; } else { jbtn.textContent = t.btn_join || 'Liity laskentaverkkoon'; } } const cbtn = document.getElementById('code-send-btn'); if (cbtn && !cbtn.textContent.includes('...')) { cbtn.textContent = t.btn_generate || 'Generate'; } }; document.addEventListener('DOMContentLoaded', () => { const savedLang = localStorage.getItem('kpn_lang') || 'fi'; setLanguage(savedLang); // Valitaan Asiakas-agentti automaattisesti sivun ladattua (muttei jatkossa) setTimeout(() => { if (window.selectAgent) window.selectAgent('client'); }, 100); }); // GUIDE.md:n lataus ja renderöinti (async function loadGuide() { const container = document.getElementById('guide-content'); if (!container) return; try { const res = await fetch('/GUIDE.md'); if (!res.ok) { container.innerHTML = '

Oppaan lataus epäonnistui.

'; return; } const md = await res.text(); container.innerHTML = renderMarkdown(md); // Syntaksikorostus koodiblokeille container.querySelectorAll('pre code').forEach(block => { if (typeof hljs !== 'undefined') hljs.highlightElement(block); }); // Mermaid-kaaviot if (typeof mermaid !== 'undefined') { mermaid.initialize({ startOnLoad: false, theme: 'dark', themeVariables: { primaryColor: '#58a6ff', primaryTextColor: '#c9d1d9', lineColor: '#30363d', background: '#0d1117' } }); container.querySelectorAll('.mermaid-container').forEach(async el => { try { const { svg } = await mermaid.render('m-' + el.id, el.textContent.trim()); el.innerHTML = svg; } catch(e) { /* fallback: jätetään teksti näkyviin */ } }); } } catch(e) { container.innerHTML = '

Virhe: ' + e.message + '

'; } })(); function renderMarkdown(md) { const lines = md.split('\n'); let html = ''; let inCode = false; let codeLang = ''; let codeBuffer = ''; let inTable = false; let tableRows = []; function flushTable() { if (!inTable) return; inTable = false; if (tableRows.length < 2) return; const headerCells = tableRows[0].split('|').filter(c => c.trim()); const bodyRows = tableRows.slice(2); // Skip header + separator html += '
'; html += '' + headerCells.map(c => ``).join('') + ''; html += ''; for (const row of bodyRows) { const cells = row.split('|').filter(c => c.trim()); if (cells.length === 0) continue; html += '' + cells.map(c => ``).join('') + ''; } html += '
${inlineFormat(c.trim())}
${inlineFormat(c.trim())}
'; tableRows = []; } function inlineFormat(text) { return text .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1'); } for (const line of lines) { // Koodiblokit + Mermaid-kaaviot if (line.startsWith('```')) { if (inCode) { if (codeLang === 'mermaid') { const mermaidId = 'mermaid-' + Math.random().toString(36).slice(2, 8); html += `
${codeBuffer.replace(/`; } else { html += `
${codeBuffer.replace(/
`; } inCode = false; codeBuffer = ''; } else { flushTable(); inCode = true; codeLang = line.slice(3).trim() || 'plaintext'; } continue; } if (inCode) { codeBuffer += (codeBuffer ? '\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 += '
'; continue; } // Otsikot if (line.startsWith('# ')) { html += `

${inlineFormat(line.slice(2))}

`; continue; } if (line.startsWith('## ')) { html += `

${inlineFormat(line.slice(3))}

`; continue; } if (line.startsWith('### ')) { html += `

${inlineFormat(line.slice(4))}

`; continue; } // Horisontaalinen viiva if (line.match(/^-{3,}$/)) { html += '
'; continue; } // Lista if (line.match(/^[\-\*] /)) { html += `
${inlineFormat(line.replace(/^[\-\*] /, '• '))}
`; continue; } if (line.match(/^\d+\. /)) { html += `
${inlineFormat(line)}
`; continue; } // Normaali tekstirivi html += `

${inlineFormat(line)}

`; } flushTable(); return html; }