Frontend uudelleenrakennettu: Astro-komponentit, Wasm pääsäikeessä, ei Workeria

Vanha frontend siirretty temp/. Uusi rakenne:
- StatusBar.astro, Terminal.astro, Editor.astro, Guide.astro
- global.css erillinen
- Wasm pääsäikeessä (ei Worker — yksinkertainen, debugattava)
- Tab-completion, dropdown, projektikortti, Monaco, GUIDE.md
- Ei tokenisointia eikä koodilaboratoriota

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-09 20:17:39 +03:00
parent e3fdb91ac5
commit a8c4af0975
9617 changed files with 996171 additions and 5349 deletions

413
network-poc/frontend/dist/GUIDE.md vendored Normal file
View File

@@ -0,0 +1,413 @@
# Kipinä Agentic Studio — Opas
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
tuloksia voi parantaa.
---
## Kielimallit ja niiden koot
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|-------|-----------|-------------|-------------------|-------------|
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
ja oppikirjan välillä.
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
---
## Tokenit — kielimallin "sanat"
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
merkkijonot harjoitusdatasta.
### Esimerkki: suomi vs. englanti
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
tokeneita saman merkityksen välittämiseen:
![Tokenisointivertailu EN/FI](/images/tokenization-example.png)
**Huomaa miten:**
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
### Miksi tämä merkitsee?
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
→ 50% pidempi odotusaika
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
tunnista → hallusinaatiot lisääntyvät
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
**Token-budjetti tässä järjestelmässä:**
| Osa | Tokeneita | Osuus |
|-----|-----------|-------|
| System prompt | ~30 | kiinteä |
| Agent prompt | ~25 | kiinteä |
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
| Käyttäjän prompti | ~20-50 | vaihtelee |
| **Syöte yhteensä** | **~75-400** | |
| Generoitu vastaus (max) | 512 | raja |
| **Yhteensä** | **~600-900** | /32 768 |
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
---
## Promptit — miten mallia ohjataan
### Kolmitasoinen prompttirakenne
```mermaid
flowchart TD
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
S --> A --> U --> P
P -->|malli jatkaa| R["Generoitu koodi"]
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
```
### Miksi promptit ovat englanniksi?
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
suomeksi, mutta tekniset ohjeet mallille englanniksi.
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
kuin tekninen ohje.
---
## Prefill-tekniikka
Normaalisti malli päättää vapaasti miten vastaa:
```
Ilman prefilliä:
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
→ 25 tokenia, joista 15 turhia
Prefillin kanssa:
Me syötämme: ```
Malli jatkaa: python\nprint('Hello')\n```
→ 5 tokenia, kaikki hyödyllisiä
```
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
---
## Sampling — miten malli valitsee seuraavan tokenin
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
ohjataan kolmella parametrilla:
### Temperature (0.7)
Kontrolloi "luovuutta" vs. "varmuutta":
```
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
```
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
mutta tarpeeksi vaihtelevaa välttämään toistoa.
### Top-k (40)
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
täysin absurdeja vaihtoehtoja:
```
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
```
### Repetition penalty (1.15)
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
juuttumasta luuppiin:
```
Ilman rangaistusta: "print print print print print..."
Penalty 1.15: "print('Hello')\nprint('World')"
```
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
saman avainsanan (esim. `return`) esiintymisen useasti.
---
## Stop-sekvenssit — milloin generointi loppuu
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
2. **Max tokens** (512): Kovakoodattu raja
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
```
fn fibonacci(n: usize) -> usize {
if n <= 1 { return n; }
fibonacci(n-1) + fibonacci(n-2)
}
← Tähän asti koodia, ok
// Example usage: ← Stop! Tämä ei ole enää vastausta
let result = fibonacci(10); ← Ei generoida
```
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
stop-kohtaan.
---
## Projekti-pipeline — miten agenttitiimi toimii
```mermaid
flowchart TD
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
T1 -->|LGTM| Done["✅ Projekti valmis"]
C4["🟡 Koodari: Korjaukset"] --> T2
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
```
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
mitä luokkia importata.
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
---
## Laadun parantaminen
### 1. Isompi malli (suurin vaikutus)
| | 0.5B | 3B | Pilvi-API |
|---|---|---|---|
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
### 2. Paremmat promptit (ilmaista)
**Huono:** `"tee fibonacci"`
- Malli ei tiedä kieltä, formaattia tai kontekstia
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
- Kieli, palautustyyppi ja rakenne määritelty
**Promptin säännöt:**
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
- Positiivinen ("Write X" ei "Don't write Y")
### 3. Kontekstin hallinta (pipeline-taso)
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
**Ratkaisu:** Pienet, kohdennetut promptit:
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
- Vain relevantit aiemmat tiedostot kontekstina
- Max 4 tiedostoa per projekti
### 4. Iterointi (review-luuppi)
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
Pipeline-arkkitehtuuri mahdollistaa:
1. **Generointi** — ensimmäinen versio
2. **Review** — testaaja löytää ongelmat
3. **Korjaus** — koodari saa palautteen ja korjaa
4. **Uusi review** — tarkistetaan korjaukset
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
### 5. Erikoistetut system promptit
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
parantavat laatua merkittävästi:
```
Oletus: "Olet kokenut ohjelmistokehittäjä."
Parempi: "You are a Python backend developer specializing in FastAPI.
Always use Pydantic models for request/response schemas.
Always use dependency injection for database sessions.
Follow the repository pattern."
```
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
### 6. Few-shot esimerkit
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
```
Write a GET endpoint like this example:
@app.get("/items")
def list_items():
db = SessionLocal()
return db.query(Item).all()
Now write a similar endpoint for /users.
```
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
### 7. Temperature-säätö tehtävän mukaan
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
| Tehtävä | Paras temperature | Miksi |
|---------|-------------------|-------|
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
### 8. Ensemble — sama prompti usealle mallille
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
koska sampling on stokastinen.
### 9. Post-processing (nykyinen)
Mallin raakavastaus siivotaan:
1. Kielitunniste poistetaan (`python`, `rust`, ...)
2. Sulkeva ` ``` ` poistetaan
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
4. Selityskommentit poistetaan ("# This is a simple...")
5. Stop-sekvenssit katkaisevat generoinnin
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
### 10. Mallin hienosäätö (fine-tuning)
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
---
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
```
Ensimmäinen lataus (hidas):
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
~990 MB lataus, ~30-60s
Toinen lataus samalla sivulatauksella (nopea):
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
~0ms
Refresh jälkeen (keskitaso):
IndexedDB → RAM → Mallin rakennus
~0 MB lataus, ~2-5s rakennus
Uusi selain/laite (hidas):
Verkko → IndexedDB → RAM → Mallin rakennus
Kuten ensimmäinen lataus
```
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
välillä jotta edellinen vastaus ei vuoda seuraavaan.
---
## Lukuja käytännöstä
**Yksittäinen funktio** (esim. fibonacci):
- Input: ~80 tokenia
- Output: ~50-100 tokenia
- Aika: ~10-20s (0.5B, selain)
- Laatu: Yleensä toimiva, joskus loogisia virheitä
**3 tiedoston projekti** (esim. FastAPI CRUD):
- Manageri: ~30 tok out
- Koodari (3x): ~100-150 tok out per tiedosto
- Testeri: ~50 tok out
- Korjaukset: ~100 tok out (jos tarpeen)
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
- Laatu: Rakenne oikein, yksittäisiä bugeja
**Token-kustannus vs. pilvipalvelu:**
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.

View File

@@ -0,0 +1 @@
import"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js";

519
network-poc/frontend/dist/index.html vendored Normal file
View File

@@ -0,0 +1,519 @@
<!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 type="module" src="/_astro/index.astro_astro_type_script_index_0_lang.B57BFWqC.js"></script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css"><style>:root{--bg: #0d1117;--panel: #161b22;--text: #c9d1d9;--accent: #58a6ff;--green: #3fb950;--yellow: #d29922;--red: #f85149;--purple: #a371f7;--border: #30363d}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}.container{max-width:1200px;margin:0 auto;padding:20px}.tabs{display:flex;gap:4px;margin-bottom:16px}.tab{padding:8px 16px;border-radius:6px 6px 0 0;cursor:pointer;border:1px solid var(--border);border-bottom:none;background:var(--bg);color:#8b949e;font-size:14px}.tab.active{background:var(--panel);color:var(--accent);border-color:var(--border)}.panel{display:none}.panel.active{display:block}.status-bar{display:flex;align-items:center;gap:12px;padding:8px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px 6px 0 0;font-family:Courier New,monospace;font-size:13px}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.status-group{display:flex;align-items:center;gap:6px}.status-separator{color:var(--border)}.terminal{background:#010409;border:1px solid var(--border);border-top:none;font-family:Courier New,monospace;font-size:14px;min-height:300px;max-height:60vh;overflow-y:auto;padding:8px 12px}.terminal-line{padding:1px 0;white-space:pre-wrap;word-break:break-word}.terminal-prompt{color:var(--yellow);margin-right:8px}.terminal-input-row{display:flex;align-items:center;position:relative;background:#010409;border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:Courier New,monospace;font-size:14px}.terminal-input{flex:1;background:transparent;border:none;outline:none;color:var(--green);font-family:inherit;font-size:inherit}.terminal-dropdown{display:none;position:absolute;bottom:100%;left:30px;background:var(--panel);border:1px solid var(--border);border-radius:6px;max-height:200px;overflow-y:auto;font-size:13px;min-width:200px;z-index:100;box-shadow:0 4px 12px #0006}.dd-item{padding:6px 12px;cursor:pointer;color:var(--text);white-space:nowrap;border-bottom:1px solid #21262d}.dd-item:hover,.dd-item.active{background:var(--border);color:var(--accent)}.pipeline-bar{display:none;padding:8px 14px;background:var(--bg);border:1px solid var(--border);border-top:none;font-family:Courier New,monospace;font-size:12px;overflow-x:auto;white-space:nowrap}.project-card{margin:8px 0;border:1px solid var(--border);border-radius:6px;background:var(--panel);overflow:hidden}.project-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg);border-bottom:1px solid var(--border)}.project-tabs{display:flex;gap:2px;padding:6px 8px 0;background:var(--bg)}.project-tab{padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;color:#8b949e}.project-tab.active{background:var(--panel);color:var(--accent);border:1px solid var(--border);border-bottom:none}.btn{padding:2px 10px;border-radius:4px;border:1px solid var(--border);background:var(--panel);font-size:12px;font-family:inherit;cursor:pointer}.btn-accent{color:var(--accent)}.btn-green{color:var(--green);border-color:var(--green)}.btn-red{color:var(--red);border-color:var(--red)}.btn-muted{color:#8b949e;background:none}.code-block{font-family:Courier New,monospace;background:#010409;border:1px solid var(--border);border-radius:6px;padding:14px;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-x:auto;max-height:400px;overflow-y:auto}.code-block .hljs{background:transparent;padding:0}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}@keyframes spin{to{transform:rotate(360deg)}}
</style></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> <!-- Agents-paneeli --> <div id="panel-agents" class="panel active"> <!-- Hub-yhteys + laskentasolmun tila --><div class="status-bar"> <span class="status-group" title="Hub-yhteyden tila"> <span id="hub-dot" class="status-dot" style="background:#d29922"></span> <span style="color:#8b949e">Hub:</span> <span id="hub-label" style="color:#d29922">Yhdistetään...</span> </span> <span class="status-separator"></span> <span class="status-group"> <span id="compute-dot" class="status-dot" style="background:#30363d"></span> <span style="color:#8b949e">Laskenta:</span> <span id="compute-label" style="color:#8b949e"></span> <button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli">Alusta</button> </span> </div> <!-- Pipeline-palkki + Terminaali + Input --><div id="pipeline-bar" class="pipeline-bar"></div> <div id="terminal" class="terminal"></div> <div class="terminal-input-row"> <span class="terminal-prompt">$</span> <input id="term-input" class="terminal-input" type="text" placeholder="kpn run coder &quot;hello world in python&quot;" spellcheck="false" autocomplete="off"> <div id="term-dropdown" class="terminal-dropdown"></div> </div> </div> <!-- Monaco Editor paneeli --><div id="panel-editor" class="panel"> <div style="display:flex;height:calc(100vh - 200px);gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden"> <div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow-y:auto;font-family:'Courier New',monospace;font-size:13px"> <div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</div> <div id="editor-file-list" style="padding:4px 0"> <div style="padding:8px 16px;color:#8b949e;font-size:12px">Generoi projekti:<br><code style="color:var(--accent)">kpn project "..."</code></div> </div> </div> <div style="flex:1;display:flex;flex-direction:column"> <div id="editor-tabs" style="display:flex;background:var(--bg);border-bottom:1px solid var(--border);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div> <div id="monaco-container" style="flex:1"></div> </div> </div> </div> <!-- Opas-paneeli: ladataan GUIDE.md fetchillä --><div id="panel-guide" class="panel"> <div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;line-height:1.7;font-size:15px"> <p style="color:#8b949e">Ladataan opasta...</p> </div> </div> </div> <script>
// === 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);
}
// === 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);
// === WebSocket ===
const wsUrl = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`;
const uiSocket = new WebSocket(wsUrl);
window._uiSocket = uiSocket;
uiSocket.onopen = () => {
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',
}));
};
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();
});
// Autostart
if (localStorage.getItem('kpn-coder-loaded') === 'true') {
setTimeout(() => ensureNode(), 300);
}
// === 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 {
// Varmistetaan solmu
if (!wasmNodeStarted) {
statusDiv.innerHTML = ' <span style="color:#d29922">→ Käynnistetään laskentasolmua...</span>';
await ensureNode();
// Odotetaan kunnes malli on latautunut tai 60s
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;
}
}
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.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 = { coder: 'qwen-coder', 'coder-3b': 'qwen-coder-3b', manager: 'qwen-coder', tester: 'smollm-135m', qa: 'smollm-135m' };
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(); });
// === Project pipeline ===
async function kpnProject(task) {
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti käynnistyy ━━━</span>`);
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — suunnittelu`);
const plan = await kpnRun('qwen-coder', `List the source files needed for this project. One file per line, format:\nfilename.py: what this file contains\n\nRules:\n- Max 4 files\n- Only .py, .toml, .json, .html files\n- No directories, just filenames\n- Dependencies first (models.py before main.py)\n- Use pyproject.toml for deps\n\nProject: ${task}`);
if (!plan) { termLog(' ✗ Keskeytyi', '#f85149'); return; }
const fileList = plan.split('\n').map(l => l.trim().replace(/^[\d\.\-\*\s]+/,'').replace(/\*+/g,'').replace(/`/g,'')).map(l => {
if (l.includes(':')) { const [n,...d] = l.split(':'); return { name: n.trim(), desc: d.join(':').trim() }; }
return { name: l.trim(), desc: '' };
}).filter(f => f.name.length > 0 && f.name.length < 40 && !f.name.includes('/') && !f.name.includes(' ') && /\.\w{1,5}$/.test(f.name));
if (!fileList.length) {
termLog(' Ei tiedostojakoa — generoidaan yhtenä', '#8b949e');
await kpnRun('qwen-coder', `Project: ${task}\n\nWrite all the code.`);
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
return;
}
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f=>f.name).join(', ')}</span>`);
const files = {};
for (let i = 0; i < fileList.length; i++) {
const f = fileList[i];
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}] Koodari</span> — ${esc(f.name)}`);
let ctx = '';
const prev = Object.entries(files);
if (prev.length) ctx = 'Already written:\n' + prev.map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') + '\n\n';
let extra = '';
if (f.name === 'pyproject.toml') extra = '\nUse format: [project]\\nname="proj"\\nversion="0.1.0"\\nrequires-python=">=3.11"\\ndependencies=["fastapi","uvicorn"]';
const code = await kpnRun('qwen-coder', `${ctx}Project: ${task}\nWrite ONLY "${f.name}"${f.desc ? ': '+f.desc : ''}.${extra}\nUse exact libraries from project description.`);
if (!code) { termLog(` ✗ Keskeytyi (${f.name})`, '#f85149'); return; }
files[f.name] = code;
}
// Review
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] Testaaja</span> — review`);
const review = await kpnRun('smollm-135m', `Review briefly. Say LGTM if ok.\n${allCode}`);
if (review && !review.toLowerCase().includes('lgtm')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] Korjaukset</span>`);
await kpnRun('qwen-coder', `Fix issues:\n${review}\n\nCode:\n${allCode}`);
}
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`);
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('smollm-135m', `Review briefly:\n${code}`);
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
}
// === Project card ===
function renderProjectCard(files, name) {
const entries = Object.entries(files);
if (!entries.length) return;
const id = 'proj-' + Date.now();
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="navigator.clipboard.writeText(JSON.parse(document.getElementById('${id}').dataset.files)['${n}'])">Kopioi</button></div><pre class="code-block">${highlightCode(c)}</pre></div>`).join('');
const html = `<div id="${id}" class="project-card" data-files='${esc(JSON.stringify(files))}'><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="navigator.clipboard.writeText(Object.entries(JSON.parse(document.getElementById('${id}').dataset.files)).map(([n,c])=>'# --- '+n+' ---\\n'+c).join('\\n\\n'))">Kopioi kaikki</button><button class="btn btn-green" onclick="openInEditor(JSON.parse(document.getElementById('${id}').dataset.files))">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.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) ===
let monacoLoaded = false;
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')` };
async function initMonaco() {
if (monacoLoaded) return;
monacoLoaded = true;
await new Promise(r => { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js'; s.onload = () => { require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs'}}); require(['vs/editor/editor.main'],()=>r()); }; document.head.appendChild(s); });
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} });
}
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 = function(files) {
switchTab('editor');
initMonaco().then(() => {
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] = monaco.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); });
} catch(e) { el.textContent = 'Virhe: ' + e.message; }
})();
function renderMd(md) {
let html = '', inCode = false, lang = '', buf = '';
for (const line of md.split('\n')) {
if (line.startsWith('```')) { if (inCode) { html += `<pre class="code-block"><code class="language-${lang}">${buf.replace(/</g,'&lt;')}</code></pre>`; inCode=false; buf=''; } else { inCode=true; lang=line.slice(3).trim()||'plaintext'; } continue; }
if (inCode) { buf += (buf?'\n':'') + line; continue; }
if (!line.trim()) { html += '<br>'; continue; }
if (line.startsWith('# ')) html += `<h1 style="color:#e6edf3;font-size:24px;margin:24px 0 8px;border-bottom:1px solid var(--border);padding-bottom:6px">${line.slice(2)}</h1>`;
else if (line.startsWith('## ')) html += `<h2 style="color:#e6edf3;font-size:20px;margin:20px 0 8px">${line.slice(3)}</h2>`;
else if (line.startsWith('### ')) html += `<h3 style="color:#e6edf3;font-size:16px;margin:16px 0 6px">${line.slice(4)}</h3>`;
else if (line.startsWith('---')) html += '<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';
else if (line.match(/^[\-\*] /)) html += `<div style="padding:2px 0 2px 20px">${line.replace(/^[\-\*] /,'• ').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`(.+?)`/g,'<code style="background:var(--panel);padding:1px 4px;border-radius:3px;font-size:13px">$1</code>')}</div>`;
else html += `<p style="margin:4px 0">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`(.+?)`/g,'<code style="background:var(--panel);padding:1px 4px;border-radius:3px;font-size:13px">$1</code>')}</p>`;
}
return html;
}
</script> </body> </html>

63
network-poc/frontend/dist/pkg/node.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
/* tslint:disable */
/* eslint-disable */
export function set_auto_tasks(enabled: boolean): void;
export function set_gpu_load(load: number): void;
export function start_agent_node(hub_url: string, has_webgpu: boolean, device_info_json: string, task_id: number): Promise<void>;
/**
* JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
* Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
*/
export function tokenize_js(text: string): Promise<string>;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly set_auto_tasks: (a: number) => void;
readonly set_gpu_load: (a: number) => void;
readonly start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
readonly tokenize_js: (a: number, b: number) => any;
readonly wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
readonly wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

1741
network-poc/frontend/dist/pkg/node.js vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const set_auto_tasks: (a: number) => void;
export const set_gpu_load: (a: number) => void;
export const start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
export const tokenize_js: (a: number, b: number) => any;
export const wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_externrefs: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_destroy_closure: (a: number, b: number) => void;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -0,0 +1,15 @@
{
"name": "node",
"type": "module",
"version": "0.1.0",
"files": [
"node_bg.wasm",
"node.js",
"node.d.ts"
],
"main": "node.js",
"types": "node.d.ts",
"sideEffects": [
"./snippets/*"
]
}