142 Commits

Author SHA1 Message Date
Jaakko Vanhala
ee048b0b68 kipina-node: automaattinen Ollama-instanssien haku + konttituki
Skripti skannaa localhost, 127.0.0.1, ollama, host.docker.internal
ja tarjoaa valikon jos useampi löytyy. Ei vaadi enää paikallista
ollama-binääriä — toimii myös Docker-konttia tai remote-instanssia
vasten. OLLAMA_URL välitetään Rust-binäärille.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:19:00 +03:00
Jaakko Vanhala
4e83569194 Konsoliloki näyttää mallin nimen: ✓ qwen2.5-coder:3b | 438 tok | 4952ms | 93.4 tok/s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:01:32 +03:00
Jaakko Vanhala
f42b692eeb Lyhennetty konsolilogi: yksi rivi per pyyntö + yksi rivi per tulos
Ennen: koko prompti + vastaus logitettiin (satoja rivejä)
Jälkeen:
  → task_id:abc | 42r prompti | "Write ONLY models.py..."
  ✓ 128 tok | 3200ms | 40.0 tok/s | "from sqlalchemy import..."

llm_done-viestissä prompt lyhennetty viimeiseen riviin (ei koko kontekstia).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:00:39 +03:00
Jaakko Vanhala
f79bb16f3d kipina-node binäärijakelu: download-skripti + macOS ARM64 binääri
kipina.studio/kipina-node — shell-skripti joka:
1. Tunnistaa OS/arch (macOS ARM, Linux x86/ARM)
2. Tarkistaa Ollaman (asennettu? käynnissä?)
3. Lataa kielimallin automaattisesti
4. Lataa oikean binäärin kipina.studio/download/
5. Käynnistää noden → yhdistää hubiin

Käyttö: curl -sSL https://kipina.studio/kipina-node | bash
Tai:    curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node

build-binaries.sh — kääntää binäärit kaikille alustoille (Docker).
macOS ARM64 binääri (4.9MB) valmis, Linux x86_64 build käynnissä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:51:31 +03:00
Jaakko Vanhala
e81fc33faf Join-dialogi: kaksi selkeää vaihetta (Ollama + kipina-node binääri)
Vaihe 1: Asenna Ollama
  curl -fsSL https://ollama.ai/install.sh | sh
  (+ brew/Windows-vaihtoehdot)

Vaihe 2: Lataa ja käynnistä kipina-node
  curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node

Ei vaadi Rustia — valmis binääri ladataan suoraan.
Molemmat komennot kopioitavissa yhdellä klikkauksella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:56:00 +03:00
Jaakko Vanhala
433726c553 Palautettu docker-compose.prod.yml: vain Caddy + Hub (ei Ollamaa palvelimella)
Ollama ajetaan käyttäjien omilla koneilla join.sh:n kautta,
ei palvelimella. Selain-Wasm toimii fallbackina.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:53:06 +03:00
Jaakko Vanhala
dec2e24e2f "Liitä koneesi" -nappi + join.sh + Docker native-node
UI: status-palkissa vihreä "+ Liitä koneesi" -nappi joka avaa dialogin:
  curl -sSL https://kipina.studio/join.sh | bash

join.sh:
- Tarkistaa Ollaman → tarjoaa asennusta jos puuttuu
- Käynnistää Ollaman jos ei pyöri
- Lataa kielimallin (qwen2.5-coder:3b)
- Käynnistää native-noden → yhdistää wss://kipina.studio/ws

Docker: Dockerfile.native + docker-compose.prod.yml päivitetty
ollama + native-node -konteilla palvelinpuolelle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:46:22 +03:00
Jaakko Vanhala
9058033669 Poistettu fonttiskaalaus (A-/A+) — ei vaikuttanut terminaaliin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:33:20 +03:00
Jaakko Vanhala
8bd86e6325 Fonttikoon A-/A+ säädin: ±20% viidessä askeleessa
Oikeassa yläkulmassa A- ja A+ napit. Skaalaa 80-120%, tallennetaan localStorageen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:31:33 +03:00
Jaakko Vanhala
c1133bb075 Terminaalin fontti 15→16px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:27:31 +03:00
Jaakko Vanhala
6502d75efc Terminaalin syöttökenttä korostettu: sininen reunus, varjo, isompi fontti 16px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:25:37 +03:00
Jaakko Vanhala
9f8b7fe920 UI-fonttikoot kasvatettu: body 16px, terminaali 15px, tabit 15px, status 14px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:23:46 +03:00
Jaakko Vanhala
746bc20fcb Agenttikuvakkeet kasvatettu: 50→64px kuva, 72→90px kortti, isompi fontti
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:22:42 +03:00
Jaakko Vanhala
93f6baa0ea UI kasvatettu: container 1200→1600px, terminaali korkeampi, padding leveämpi
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:18:34 +03:00
Jaakko Vanhala
cc8e871735 deploy-fast.sh: luo hakemisto palvelimelle ennen rsync:iä
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:15:19 +03:00
Jaakko Vanhala
e90f3460c3 deploy-fast.sh: päivitä vain frontend ilman kontin uudelleenkäynnistystä
docker-compose.prod.yml: frontend/dist mountataan volumena (read-only).
Hub servaa tiedostot suoraan — rsync päivittää ne lennossa.

Kolme deploy-tasoa:
1. deploy-fast.sh — vain frontend (sekunteja, ei downtime)
2. deploy-light.sh — rsync + remote Docker build (minuutteja)
3. deploy.sh — lokaali build + image siirto (hidas mutta varma)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:12:10 +03:00
Jaakko Vanhala
4d74c38618 Dockerfile: poistettu turha COPY pkg (Astro kopioi public/:n automaattisesti)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:06:39 +03:00
Jaakko Vanhala
8a1b204179 v0.3.1: Avatarit WebP (18MB→256KB), PNG:t temp-kansioon
Kaikki avatar-viittaukset .png → .webp (200px, quality 80).
Alkuperäiset PNG:t siirretty temp/avatars-png/ (gitignored).
Hub-versio 0.3.0 → 0.3.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:06:02 +03:00
Jaakko Vanhala
b19f5a3518 deploy-light.sh: rsync + remote build (ei image-siirtoa)
Lähettää vain lähdekoodin rsync:llä (~2MB muuttuneet tiedostot),
palvelin buildaa Docker-imagen itse. Nopeampi kuin 80MB imagen siirto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:56:01 +03:00
Jaakko Vanhala
38dc36e846 Dockerfile wasm-builder: lisätty dummy-cratet workspace-yhteensopivuuteen
cargo metadata vaatii kaikkien workspace-jäsenten Cargo.toml:n.
Lisätty hub/, native-node/, cli/ dummy-tiedostot wasm-builder-vaiheeseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:07:28 +03:00
Jaakko Vanhala
4fe6931b5f Revolutionized 2026-04-10 08:06:15 +03:00
Jaakko Vanhala
b8e8a83e49 Dockerfile.prod päivitetty Astro-frontendille
Muutokset:
- Vaihe 1: Node.js buildaa Astro-frontendin (frontend/dist/)
- Vaihe 2: wasm-pack buildaa Wasm-moduulin erikseen
- Vaihe 3: Hub rakennetaan Rustilla
- Vaihe 4: Yhdistetään dist/ + pkg/ + avatars/ + templates/ + GUIDE.md

STATIC_DIR=/app/frontend/dist (ei enää /app/static)
Avatarit, templates ja GUIDE.md kopioidaan erikseen koska
Astro ei kopioi public/-tiedostoja dist/:iin buildin yhteydessä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:04:49 +03:00
Jaakko Vanhala
3d6914974d Prompti-textarea kasvaa automaattisesti sisällön mukaan
Koko prompti näkyy kerralla kun avatarin klikkaa — ei scrollausta.
Textarea saa overflow:hidden + auto-height sekä avatessa että kirjoittaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:03:18 +03:00
Jaakko Vanhala
9aff2ec154 uv kauttaaltaan + Tarkkailijan raportti rakenteelliseksi markdowniksi
uv-päivitykset:
- Koodarin NEVER-lista: ei requirements.txt, ei pip, käytä uv
- Template pyproject.toml: PEP 621, uv-yhteensopiva
- Raportin Quick Start: uv sync + uv run uvicorn

Tarkkailijan raportti uudessa formaatissa:
- Overview (yksi kappale)
- Files (taulukko: tiedosto + tarkoitus)
- Quick Start (uv-komennot koodiblokissa)
- Docker (build + run koodiblokissa)
- API Endpoints (taulukko: method, path, description)
- Architecture (rakenne ja päätökset)
- Risk Assessment (taulukko: severity, issue)

Malli saa taulukkopohjat valmiina → täyttää ne oikealla datalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:59:10 +03:00
Jaakko Vanhala
ecd4525a7f Review-korjauskierrokset nostettu 2→3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:51:41 +03:00
Jaakko Vanhala
7a3e5278b9 Review-korjausluuppi: DevOps tarkistaa korjaukset, max 2 kierrosta
Pipeline:
1. DevOps review → löytää virheitä
2. Koodari korjaa → päivittää files-objektin
3. DevOps review (kierros 2) → tarkistaa korjaukset
4. Jos yhä virheitä → Koodari korjaa uudelleen
5. LGTM tai max 2 kierrosta → eteenpäin

Terminaalissa näkyy kierrosnumero: "koodikatselmointi (kierros 2)"
LGTM merkitään vihreällä ✓-merkillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:50:43 +03:00
Jaakko Vanhala
8dcf269b42 strip_code_fences: poistetaan kaikki backtick-rivit aggressiivisesti
Ollama tuottaa \`\`\`python ... \`\`\` -blokkeja vaikka system prompt
kieltää ne. Nyt kaikki rivit jotka alkavat \`\`\` suodatetaan pois,
myös keskeltä vastausta (useita koodiblokkeja per vastaus).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:47:03 +03:00
Jaakko Vanhala
cb16f35265 Tiedostot kulkevat agentilta toiselle: korjaukset päivittyvät, QA saa korjatun koodin
Kontekstiketju nyt:
1. Data → models.py
2. Koodari → schemas.py (saa models.py kontekstina)
3. Koodari → main.py (saa models.py + schemas.py)
4. Koodari → pyproject.toml
5. DevOps → review (saa kaikki tiedostot)
6. Koodari → korjaukset → parsitaan takaisin files-objektiin (--- filename ---)
7. QA → testit (saa KORJATUT tiedostot, ei alkuperäisiä)
8. DevOps → Dockerfile (saa kaikki tiedostot + testit)
9. Tarkkailija → README (saa kaiken)

Aiemmin: korjaukset menivät terminaaliin mutta eivät päivittyneet files-objektiin.
Nyt: korjattu koodi parsitaan --- filename --- -erottimilla takaisin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:44:01 +03:00
Jaakko Vanhala
b9d340b4b4 install.sh: Debian/Ubuntu-asennusskripti
Asentaa automaattisesti:
1. Build-työkalut (build-essential, pkg-config, libssl-dev)
2. Rust (rustup)
3. Node.js 22 (nodesource)
4. Ollama
5. qwen2.5-coder:3b -malli

Käyttö: ./network-poc/install.sh && ./network-poc/local.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:42:27 +03:00
Jaakko Vanhala
dd07e536f0 Korjattu duplikaatti const tst -määrittely kpnProject:ssa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:39:51 +03:00
Jaakko Vanhala
9af481a022 DevOps-agentin prompti laajennettu staattiseksi koodianalyysiksi
9-kohdan checklist: importit, nimeämiset, tyypit, virheenkäsittely,
resurssivuodot, tietoturva, endpointit, Pydantic v2, täydellisyys.

Aiemmin 7 kohtaa, nyt 9 — lisätty: type hints, tietoturva (raw SQL,
hardcoded secrets), Pydantic v2 (model_dump, from_attributes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:36:57 +03:00
Jaakko Vanhala
529a30a6e1 Korjattu harhaanjohtava GPU-viesti: Ollama käyttää GPU:ta automaattisesti
Kun --no-default-features (ei wgpu/nvml), viesti on nyt:
"GPU-tunnistus ei käytössä. Ollama käyttää GPU:ta automaattisesti."
eikä "GPU:ta ei havaittu — CPU-moodissa" (joka oli väärä M2:lla).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:35:00 +03:00
Jaakko Vanhala
7d842529b1 Tarkkailijan raportti: klikkaa avataria → modal + kehäväri arvosanalla
Tarkkailijan vastaus alkaa VERDICT-rivillä:
- GREEN → vihreä kehä → "OK"
- ORANGE → oranssi kehä → "HUOMIOITA"
- RED → punainen kehä → "KRIITTISTÄ"

Kehäväri ja glow jäävät näkyviin pipelinen jälkeen.
Klikkaamalla Tarkkailija-avataria avautuu raportti-modal jossa
README.md renderöidään markdown-muotoiltuna (taulukot, koodi, listat).
Modal sulkeutuu ✕-napista tai klikkaamalla taustaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:33:11 +03:00
Jaakko Vanhala
c731c18360 DevOps generoi Dockerfilen + Tarkkailija kirjoittaa README.md-raportin
Uudet pipeline-vaiheet:

DevOps — Dockerfile:
- python:3.12-slim + uv (astral-sh)
- Oikea COPY-järjestys (pyproject.toml → sync → source)
- Expose 8000, CMD uvicorn

Tarkkailija — README.md:
- Markdown-raportti joka sisältää:
  - Tiedostolista ja kuvaukset
  - Käyttöohjeet (uv + Docker)
  - API Endpoints -taulukko
  - Arkkitehtuurihuomiot
  - Riskiarviointi
- README.md lisätään projektikorttin tiedostoihin
  → avattavissa editorissa

Pipeline nyt: Data → Koodari → DevOps review → korjaukset →
QA testit → DevOps Dockerfile → Tarkkailija README → Valmis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:29:45 +03:00
Jaakko Vanhala
5498eb6cbb Kaikki 6 agenttia osallistuvat pipeline-projektiin
Pipeline-vaiheet:
1. Data-agentti: models.py (tietokanta-asiantuntija)
2. Koodari: schemas.py, main.py (ohjelmistokehittäjä)
3. Koodari: pyproject.toml
4. DevOps: koodikatselmointi (importit, nimeämiset, virheet)
5. Koodari: korjaukset (jos DevOps löysi ongelmia)
6. QA: pytest-testit (test_main.py lisätään projektiin)
7. Tarkkailija: riskianalyysi (arkkitehtuuri, tietoturva)

Data-agentti valitaan automaattisesti models.py/database.py -tiedostoille.
Jokainen vaihe highlightaa oikean avatarin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:26:57 +03:00
Jaakko Vanhala
43f0aebf54 Poistettu pystylayout — highlight toimii vaakarivillä pipelinen aikana
Avatarit pysyvät aina vaakarivillä. Aktiivinen agentti saa
glow-highlightin kun pipeline etenee (koodari → testaaja → koodari).
Highlight poistuu kun pipeline valmistuu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:23:06 +03:00
Jaakko Vanhala
6413f0238f Avatarit vaihtuvat pystyyn pipelinen aikana, aktiivinen agentti highlightataan
Pipeline käynnistyessä:
- Avataririvi vaihtuu pystyyn (column) → näkee kuka tekee mitä
- Aktiivinen agentti saa glow-highlightin (coder sininen, tester sininen)
- Korjausvaiheessa koodari highlightataan uudelleen

Pipeline valmistuttua:
- Palautetaan vaakarivi (row)
- Poistetaan highlight

Poistettu manuaalinen layout-toggle-nappi — layout on automaattinen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:17:30 +03:00
Jaakko Vanhala
6b0394586e Mermaid-kaaviot oppaaseen + avatareiden vaaka/pysty-toggle
Mermaid ladataan CDN:stä (esm module). Opas-sivun renderMd tunnistaa
\`\`\`mermaid -koodiblokit ja renderöi ne SVG-kaavioiksi dark-teemalla.

toggleAgentLayout vaihtaa avatareiden suunnan (row/column),
tallennetaan localStorageen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:11:01 +03:00
Jaakko Vanhala
108094b06a Opas-sivun markdown-renderöijä laajennettu: taulukot, inline-muotoilu, listat
Lisätty tuki:
- Taulukot (| header | ... | -parsinta, thead/tbody, border-collapse)
- Inline: **bold**, *italic*, \`code\` (kaikissa elementeissä)
- Numeroidut listat (1. 2. 3.)
- Parempi tyhjien rivien käsittely (8px spacer, ei <br>)
- Otsikkojen border-bottom h1/h2:lle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:01:54 +03:00
Jaakko Vanhala
d7c974792d Agenttien päivitys kysyy käyttäjältä: OK = oletukset, Peruuta = säilytä omat
Kun AGENTS_VERSION kasvaa eikä localStorage ole tyhjä, näytetään confirm-dialogi:
"Agenttien oletuspromptit on päivitetty. Haluatko ottaa uudet käyttöön?"
- OK: ylikirjoitetaan oletuksilla
- Peruuta: käyttäjän muokkaukset säilyvät

Ensimmäisellä käyttökerralla (tyhjä localStorage) ladataan oletukset ilman kysymystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:00:08 +03:00
Jaakko Vanhala
1987eb57a0 Agenttien oletuspromptit päivittyvät automaattisesti (AGENTS_VERSION)
Kun AGENTS_VERSION kasvaa, localStorage ylikirjoitetaan uusilla oletuksilla.
Ei tarvitse enää manuaalisesti tyhjentää localStorage.removeItem('kpn-agents').
Kasvata AGENTS_VERSION aina kun oletusprompteja muutetaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:58:30 +03:00
Jaakko Vanhala
12ca87415c Poistettu native-noden kovakoodattu system prompt — agentin prompti toimii nyt
Ollaman system-kenttä yliajoi agentin konfiguroiman promptin.
Nyt system-kenttää ei lähetetä ollenkaan — agentin prompti tulee
osana prompt-kenttää (kpnRun koostaa sen frontendissä).

Tämä mahdollistaa per-agentti promptien toimimisen myös natiivilaskennalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:54:18 +03:00
Jaakko Vanhala
a0e52faa44 Localhost vapautettu IP-yhteysrajasta, tuotannon raja nostettu 20:een
Kehitysympäristössä (127.0.0.1) ei enää yhteysrajaa — useita
selainikkunoita ja native-nodeja voi yhdistää vapaasti.
Tuotannossa raja 10→20 per ulkoinen IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:14:06 +03:00
Jaakko Vanhala
f910cd8c61 Kattavat promptit + opettavat tooltipit jokaiselle parametrille
Promptit laajennettu moninkertaisiksi jokaiselle agentille:
- Manageri: RULES + EXAMPLE OUTPUT -formaatti
- Koodari: 8 CRITICAL RULES + NEVER-lista (importit, nimeäminen, Pydantic v2)
- Data: SQLAlchemy-spesifit ohjeet (String(length), connect_args, sessionmaker)
- QA: pytest-testirakenne (5 testitapausta enumeroituna)
- DevOps: 7-kohdan CHECKLIST + LGTM/ISSUE-vastausformaatti
- Tarkkailija: 4-alueen arviointi + RISK-formaatti + SHIP IT/NEEDS WORK

Tooltipit (hover 💡):
- Temperature: milloin matala/korkea, suositukset per rooli
- Max tokens: milloin nostaa/laskea, ~1 token ≈ 4 merkkiä
- Top-K: milloin muuttaa, harvoin tarpeen
- Repetition penalty: miksi liian korkea rikkoo koodin
- System prompt: hyvän promptin rakenne (rooli → säännöt → esimerkit → kiellot)

Prompt-tekstikenttä kasvatettu 4 → 8 riviä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:11:11 +03:00
Jaakko Vanhala
91dc7579bc Per-agentti sampling-parametrit: temperature, top-k, max tokens, repetition penalty
Jokainen agentti saa omat parametrit jotka näkyvät avatarin konfigurointipaneelissa:
- Temperature: Manageri 0.5 (tarkka), Koodari 0.7, Testaaja 0.3 (deterministinen)
- Max tokens: Manageri 512, Koodari 1024, Testaaja 512
- Top-K ja Repetition penalty per agentti
- Sliderit reaaliaikaisilla arvoilla

Parametrit tallentuvat localStorageen agentin mukana.
Perustelut: manageri ja testaaja hyötyvät matalasta temperaturesta
(determinismi tärkeää), koodari tarvitsee enemmän tokeneita ja
hieman korkeamman temperaturen luovempiin ratkaisuihin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:03:11 +03:00
Jaakko Vanhala
90c9a7e4fa Asetukset-välilehti: kaikki LLM-parametrit muokattavissa UI:sta
Uusi "Asetukset"-tab jossa:
- System Prompt (tekstikenttä, Courier-fontti)
- Temperature (slider 0-1.5, reaaliaikainen arvo)
- Top-K (slider 1-100)
- Repetition Penalty (slider 1.0-2.0)
- Max Tokens (slider 64-4096)
- Stop-sekvenssit (yksi per rivi)
- Mallinvalinta (dropdown: 1.5B/3B/7B Q4/7B)
- "Palauta oletukset" -nappi

Kaikki tallentuvat localStorageen (kpn-settings).
Jokainen parametri selitetty hint-tekstillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:44:03 +03:00
Jaakko Vanhala
1216e016c2 Template-pohjainen projektipipeline + opettavat selitykset
Uusi lähestymistapa: sen sijaan, että malli keksii rakenteen tyhjästä,
sille annetaan mallipohja (template) joka sisältää:
- Tiedostojärjestys (models → schemas → main → pyproject.toml)
- Esimerkkikoodi jokaiselle tiedostolle (few-shot)
- Yksityiskohtaiset ohjeet (importit, nimeämiskäytännöt, patternit)

Jokainen vaihe selitetään terminaalissa:
💡 models.py — "Define the SQLAlchemy model. Always include engine
   with check_same_thread=False for SQLite..."
💡 Koodikatselmointi — "Testaaja tarkistaa importit, nimeämiset..."
💡 Tulos — "Aja: uv run uvicorn main:app --reload"

templates/fastapi-crud.json sisältää täydellisen esimerkkiprojektin
jota malli adaptoi käyttäjän kuvaukseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:32:03 +03:00
Jaakko Vanhala
d85cab4bc0 Native-noden vastausten siivous: stop-sekvenssit + selitystekstien poisto
Stop-sekvenssit laajennettu: Please note, This is, Example, ```
strip_code_fences laajennettu poistamaan:
- Selitystekstit lopusta (Please note, This is a basic, Note that, ...)
- Johdantolauseet alusta (Sure!, Here is, Certainly!)
System prompt vahvistettu: "No 'Please note' or 'Here is' text"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:04:43 +03:00
Jaakko Vanhala
4fef8824e1 Kattavat oletuspromptit kaikille agenteille
Manageri: arkkitehtuuri + tiedostojako + pyproject.toml
Koodari: importit + Pydantic/SQLAlchemy-erottelu + moderni Python
Data: normalisointi + SQLAlchemy + migration-ystävälliset patternit
QA: pytest + TestClient + edge caset + mock
DevOps: koodireview + importtien tarkistus + LGTM-protokolla
Tarkkailija: riskianalyysi + tietoturva + arkkitehtuurihuomiot

Promptit näkyvät klikkaamalla avataria → konfigurointipaneeli.
Tyhjennä localStorage (kpn-agents) jotta uudet promptit tulevat voimaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:59:19 +03:00
Jaakko Vanhala
009bf492c8 Parannetut koodarin promptit + token-raja 512→1024
Koodarin prompti sisältää nyt:
- Import-vihjeen: "from models import ..." aiemmista tiedostoista
- Nimeämisvihjeen: Pydantic-schemat (UserCreate) vs SQLAlchemy (User)
- "Include all necessary imports. Write complete, working code."

Native-noden max_tokens nostettu 512→1024 jotta CRUD-endpointit
mahtuvat yhteen vastaukseen.

Testattu API:n kautta: 3B-malli tuottaa nyt oikeat importit,
erilliset Pydantic-schemat ja kaikki 5 CRUD-endpointtia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:56:24 +03:00
Jaakko Vanhala
f7e0e8dff8 Status-palkki tunnistaa natiivisolmun: ei enää turhaa Wasm-latausta
Sivulatauksessa tarkistetaan onko hubissa jo laskentasolmu (natiivi/Wasm).
Jos on → "Valmis (natiivi)", llmReady=true, ei Wasm-latausta.
Jos 503 → Wasm-fallback "Alusta"-napista.

Poistettu automaattinen Wasm-käynnistys (ensureNode) sivulatauksessa
koska natiivisolmu hoitaa laskennan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:48:23 +03:00
Jaakko Vanhala
eb57ee7b92 Avatarit ja tyylit palautettu main-haarasta: glassmorphism-kortit, oikeat hahmot
Avatarit: karhunpentu (Manageri), kipina (Koodari), pesukarhu (Data),
susi (QA), laiskiainen (DevOps), aikuinen_susi (Tarkkailija).

CSS: glassmorphism-taustat, hover-animaatio, active-glow,
pyöristetyt reunat, varjostukset. Sama tyyli kuin main-haarassa.

Huom: tyhjennä localStorage (kpn-agents) selainkonsolin kautta
jotta uudet oletusavatarit tulevat voimaan:
  localStorage.removeItem('kpn-agents')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:46:21 +03:00
Jaakko Vanhala
84d13153ed Korjattu: agents ja defaultAgents siirretty scriptin alkuun (let hoisting)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:40:35 +03:00
Jaakko Vanhala
8beac57b50 Agenttien konfigurointi: promptit, mallit, järjestys, omat agentit
Klikatessa avataria avautuu konfigurointipaneeli:
- Nimen muokkaus
- Mallinvalinta (0.5B/1.5B/3B/7B)
- System promptin muokkaus
- Pipeline-järjestys (drag & drop avatarit ja pipeline-tagit)
- Agentin poisto

"+" -nappi luo uuden agentin satunnaisella avatarilla.
Konfiguraatio tallennetaan localStorageen (kpn-agents).
Pipeline (kpn project) käyttää agenttien prompteja ja malleja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:38:30 +03:00
Jaakko Vanhala
44067efdb6 Monaco-lataus refaktoroitu: singleton Promise, parempi virheenkäsittely
initMonaco palauttaa nyt aina saman Promisen (ei moninkertaista latausta).
AMD-loader virheet ja CDN-virheet napataan ja logitetaan.
monacoLoaded-lippu korvattu Promise-pohjaisella tilanhallinnalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:24:08 +03:00
Jaakko Vanhala
5528be1812 Korjattu monacoLoaded: siirretty scriptin alkuun ennen switchTab-kutsua
let ei hoistu — monacoLoaded pitää olla määritelty ennen initMonaco-kutsua.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:21:22 +03:00
Jaakko Vanhala
f4cf4c73b9 Korjattu syntaksivirhe: ylimääräinen }); poistettu openInEditorista
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:18:58 +03:00
Jaakko Vanhala
e19852a509 Korjattu Monaco: window.monaco asetetaan eksplisiittisesti AMD-loaderin jälkeen
openInEditor käyttää nyt async/await initMonacoa ja window.monaco-globaalia
eikä oleta AMD-moduulin scopea. Korjaa "monaco is not defined" -virheen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:16:20 +03:00
Jaakko Vanhala
6de0df365e Korjattu projektikortin JSON-parsintavirhe: tiedostot globaaliin muuttujaan
Koodin sisältämät lainausmerkit ja erikoismerkit rikkoivat data-files
HTML-attribuutin. Nyt tiedostot tallennetaan window._projectFiles[id]:hen
ja onclick-handlerit viittaavat siihen suoraan. Ei JSON DOM:ssa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:11:11 +03:00
Jaakko Vanhala
28f620f901 kpnRun lähettää suoraan hubille, Wasm-fallback vain 503:lla
Ei enää odoteta Wasm-latausta ennen API-kutsua. Jos natiivisolmu
on hubissa, vastaus tulee suoraan. Jos hub palauttaa 503 (ei solmua),
käynnistetään Wasm-fallback ja yritetään uudelleen.

Tämä korjaa tilanteen jossa native-node on jo käynnissä mutta
selain yritti silti ladata Wasmia ensin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:09:26 +03:00
Jaakko Vanhala
3497f66db7 Agenttiavatarit palautettu: AgentBar-komponentti viidellä roolilla
Manageri (pöllö), Koodari (kameleontti), Testaaja (rukoilijasirkka),
QA (kilpikonna), Data (norsu). Klikattavat, highlight aktiiviselle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:53:35 +03:00
Jaakko Vanhala
1c7362c9b0 Native-node oletusmalli: qwen2.5-coder:3b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:46:48 +03:00
Jaakko Vanhala
9983c80ef1 Native-noden oletusmalli vaihdettu kvantisoiduksi: qwen2.5-coder:7b-instruct-q4_K_M
Q4-kvantisointi: ~4GB (vs. 7GB), ~40 tok/s M2:lla (vs. ~25 tok/s).
Parempi nopeus/laatu-suhde.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:35:11 +03:00
Jaakko Vanhala
fc1fb33d5e local.sh: käynnistää native-noden automaattisesti jos Ollama on käynnissä
Käynnistysjärjestys: frontend build → hub → native-node (jos Ollama löytyy).
Ilman Ollamaa käytetään selaimen Wasm-laskentaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:28:56 +03:00
Jaakko Vanhala
3bee8e8020 Kaikki pipeline-vaiheet käyttävät qwen-coder -mallia (smollm-135m poistettu)
Testaaja ja QA saivat 503 koska smollm-solmua ei ole ladattu.
Kaikki agentit käyttävät nyt samaa qwen-coder -mallia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:25:04 +03:00
Jaakko Vanhala
f8ea5ed76e Yksinkertaistettu reititys: poistettu busy-tila ja jonotus
Pipelinen peräkkäiset kpnRun-kutsut saivat 503 koska hub merkitsi
solmun busyksi eikä vapauttanut sitä ajoissa. Reititetään aina
ensimmäiselle matchaavalle solmulle. LLM_BUSY suojaa Wasm-puolella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:53 +03:00
Jaakko Vanhala
6c7c2d6dd3 local.sh: npm install automaattisesti jos node_modules puuttuu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:47 +03:00
Jaakko Vanhala
c179b4ab7e .gitignore: node_modules, dist, .astro poistettu gitistä
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:16 +03:00
Jaakko Vanhala
a8c4af0975 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>
2026-04-09 20:17:39 +03:00
Jaakko Vanhala
e3fdb91ac5 local.sh: buildaa frontendin ja käynnistää hubin yhdellä komennolla
Käyttö: ./network-poc/local.sh → http://localhost:3000

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:42:24 +03:00
Jaakko Vanhala
9925079729 Korjattu laskenta: poistettu warmup (aiheutti busy-konfliktin) + parempi solmun odotus
Ongelma: warmup-prompt käynnisti inferenssin heti yhdistymisen jälkeen,
jolloin oikea kpnRun-prompt tuli solmulle kun se oli vielä busy.
Solmu lähetti llm_error mutta UI:ssa ei ollut käsittelijää → ikuinen odotus.

Korjaukset:
1. Poistettu warmup ensureCoderNode:sta — ei tarvita koska kpnRun
   käynnistää solmun automaattisesti
2. kpnRun odottaa coderWsReady-lippua max 15s (pollaus 500ms välein)
   kiinteän 1s viiveen sijaan
3. Selkeä virheilmoitus jos solmu ei käynnisty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:37:28 +03:00
Jaakko Vanhala
6031737f83 Laskentasolmu käynnistyy automaattisesti: kpnRun + refresh-autostart
Kaksi korjausta laskentaan:
1. kpnRun kutsuu ensureCoderNode() automaattisesti jos solmu ei ole
   vielä käynnissä — käyttäjän ei tarvitse muistaa kpn load
2. localStorage-autostart: jos malli oli ladattu ennen refreshiä,
   ensureCoderNode() ajetaan automaattisesti sivulatauksessa

Tämä korjaa "Ei vapaata solmua" -virheen kpn run coder -komennoissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:06:51 +03:00
Jaakko Vanhala
b6a8fa2671 Monaco Editor -välilehti: selainpohjainen koodieditori agenttien tuottamalle koodille
Uusi "Editor"-välilehti jossa:
- Monaco Editor (VS Coden ydin) CDN:stä, dark-teema
- Tiedostopuu vasemmalla (klikataan tiedostoa)
- Välilehdet ylhäällä (useita tiedostoja auki)
- Kielitunnistus tiedostopäätteestä (Python, Rust, JS, ...)
- "Avaa editorissa" -nappi projektikorteissa

Monaco ladataan taustalla requestIdleCallback:llä — ei hidasta
sivun käynnistymistä. Editor alustetaan vasta kun sitä tarvitaan.

Projektikortin "Avaa editorissa" -nappi:
1. Avaa Editor-välilehden
2. Luo Monaco-mallit jokaiselle tiedostolle
3. Renderöi tiedostopuun ja välilehdet
4. Avaa ensimmäisen tiedoston editoriin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:04:59 +03:00
Jaakko Vanhala
0dc53dba1c Korjattu Worker-laskentasolmun timing: odotetaan WS-yhteyden avautumista
Ongelma: start_agent_node palautui heti ennen kuin WebSocket ehti avautua.
Worker lähetti 'started' ja warmup lähti liian aikaisin → hub ei löytänyt
solmua → "Ei vapaata solmua" -virhe.

Korjaukset:
1. worker.js: kuuntelee "Yhteys Hubiin avattu" -logia Wasmista ja
   resolveaa started-Promisen vasta sen jälkeen (15s timeout)
2. worker.js: onerror + onunhandledrejection käsittelijät
3. worker.js: console.error välitetään pääsäikeelle
4. index.astro: ensureCoderNode odottaa (await) workerStarted-Promisea
   ennen warmupia ja pending-promptin lähetystä

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:55:22 +03:00
Jaakko Vanhala
857afbe111 feat: complete revolution architecture modernization
- Decoupled robust frontend into an Astro framework in `frontend/`.
- Replaced direct WebSocket broadcast with Smart Routing to distribute workload only to idle capable nodes, preventing 503 errors and duplicate responses.
- Rewrote WASM panic points (`unwrap()` handling) into panic-safe match blocks in qwen_coder.rs preventing Node Web Workers from crashing.
- Integrated robust dynamic Three.js 3D visualization.
- Resolved mermaid and THREE.js frontend hydration issues.
2026-04-09 16:38:24 +03:00
Jaakko Vanhala
84b78eb9c6 GPU-tunnistus valinnainen: cargo run --no-default-features toimii ilman nvml/wgpu
Native-node kääntyy nyt macOS:llä ja muilla koneilla ilman NVIDIA-ajureita:
  cargo run --no-default-features  ← vain Ollama, ei GPU-tunnistusta
  cargo run                        ← oletus: GPU-tunnistus mukana (nvml + wgpu)

Feature flag "gpu-detect" kontrolloi nvml-wrapper ja wgpu -riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:35 +03:00
Jaakko Vanhala
4f18377a3b Native-node lähettää NODE_API_KEY auth-viestissä hubille
Luetaan NODE_API_KEY-ympäristömuuttuja ja lisätään api_key-kenttä
auth-viestiin. Hub tarkistaa avaimen ja hylkää solmun jos se ei täsmää.

Käyttö:
  NODE_API_KEY=kpn_sk_abc123 HUB_URL=ws://hub:3000/ws cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:48 +03:00
Jaakko Vanhala
7f5bb45138 API-avain -autentikaatio natiivisolmuille
Natiivisolmujen (node_type: native) auth-viesti vaatii api_key-kentän
joka vastaa hubin NODE_API_KEY-ympäristömuuttujaa. Virheellinen avain
sulkee WebSocket-yhteyden.

Selainsolmut eivät vaadi avainta (Origin-validointi suojaa niitä).
Jos NODE_API_KEY ei ole asetettu, kaikki natiivisolmut hyväksytään
(kehitysympäristö).

Käyttö:
  Hub:  NODE_API_KEY=kpn_sk_abc123 cargo run
  Node: NODE_API_KEY=kpn_sk_abc123 cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:05 +03:00
973d7a69c7 Galleria: hahmokuvaukset ja ehdotetut roolit kaikille 21 avatarille
Jokaiselle avatarille lisätty:
- Ehdotettu rooli (Arkkitehti, CI/CD, UI/UX, Tech Lead, SRE jne.)
- Persoonakuvaus joka kuvaa hahmon luonnetta ja erikoisosaamista

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:57:21 +03:00
aebc64e76e Tofuist poistettu Playgroundilta (jää Agent Builder -esimerkiksi)
Poistettu: avatar-kortti, gallery-head, agentPrompts-entry,
avatarMap, värimapit. Tofuist on edelleen Agent Builderin
esimerkkiagentti ja docs/tofu-cheatsheet.md säilytetään.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:43:15 +03:00
48c832c61b Module import absoluuttiseksi: ./pkg/node.js → /pkg/node.js
Suhteellinen polku rikkoi sivun kun navigoitiin suoraan
/avatars/ tai muuhun alihakemistoon (selain yritti ladata
/avatars/pkg/node.js jota ei ollut).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:37:47 +03:00
8435bd32a9 Hahmogalleria: #gallery -tabi kaikilla 21 avatarilla
- Uusi välilehti: Galleria (navigointi #gallery)
- Grid-näkymä: avatar, nimi, rooli, tiedostonimi
- Klikkaa korttia → kopioi polku leikepöydälle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:33:25 +03:00
ece41dd622 Export CrewAI: generoi ZIP-projektin agenttitiimistä
Export-nappi Agent Builderissa generoi kipina-crew.zip sisältäen:
- crew.py: CrewAI agentit, tehtävät ja sequential pipeline
- Dockerfile: Python 3.12 + uv
- docker-compose.yml: Ollama + healthcheck + mallilataus + crew
- pyproject.toml: crewai[tools] riippuvuus
- .env: Ollama-asetukset
- README.md: käynnistysohjeet

Käynnistys: docker compose run crew uv run python crew.py "projekti"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:26:27 +03:00
c7f3b0d79f Agent Builder: tooltip-ohjeistukset kaikille kentille
- System Prompt: miten kirjoittaa hyvä prompti, esimerkit hyvä/huono
- Temperature: mitä arvot tarkoittavat (0.0–1.0+)
- Top-k: tokenivalinnan laajuus
- Max tokens: vastauksen pituus
- Malli: Ollama-malliesimerkit
- Docs: referenssidokumentin käyttö
- CSS .builder-tip ::after tooltip (white-space: pre-wrap)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:20:28 +03:00
8905b50f41 Builder-funktiot window-scopeen (onclick vaatii globaalin)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:54:01 +03:00
43b0612004 Agent Builder 2026-04-08 10:51:35 +03:00
599ac2d2d9 Agent Builder: kaikki 21 avataria valittavissa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:49 +03:00
d1975bd55c Uudet Gemini-generoidut avatar-kuvat (13 kpl)
bear, beaver, chameleon, elephant, gecko, lion, mantis, owl,
penguin, serpent, spider, tortoise, walrus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:21 +03:00
24a8139d3e Agent Builder UI: #builder -tabi, lomake, CRUD-integraatio
- Uusi välilehti: Agent Builder (navigointi #builder)
- Agenttilista: ladataan /api/v1/agents, näytetään kortteina
- Lomake: avatar-valitsin, rooli-template, malli, väri, docs, prompt, parametrit
- Tallenna → POST /api/v1/agents, Poista → DELETE /api/v1/agents/:id
- Avatar-grid: 8 valmista hahmoa valittavissa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:45:31 +03:00
21aac49a52 Agent Builder: SQLite-taulu + REST API (GET/POST/DELETE)
- DB skeemaversio 3: agents-taulu (id, name, avatar, role, model, color,
  docs, prompt, temperature, top_k, max_tokens, repetition_penalty)
- CRUD: upsert_agent, get_agents, delete_agent
- API: GET/POST /api/v1/agents, DELETE /api/v1/agents/:id
- Oletusagentteja (is_default=1) ei voi poistaa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:43:22 +03:00
8a5f1b753c AGENTBUILDER.md: Agent Builder -suunnitelma ja building blockit
Kuvaa hahmolomakkeen arkkitehtuurin: agenttiskeema, rooli-templatet,
malli-valitsin, avatar-grid, docs-kenttä, localStorage-tallennus,
export/import ja 4-vaiheinen toteutussuunnitelma.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:39:25 +03:00
1b0b5eb198 Eksaktit mallinimet agenteille: qwen-coder → qwen2.5-coder:7b
- Kaikki agentPrompts.model vaihdettu 'qwen-coder' → 'qwen2.5-coder:7b'
- Native-node selected_task: 'qwen2.5-coder:7b'
- Hub-reititys: qwen-perhe matchaa keskenään (selain qwen-coder-05b,
  natiivi qwen2.5-coder:7b) taaksepäin yhteensopivuuden vuoksi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:33:43 +03:00
44c8a189b6 Tofuist malli qwen2.5-coder:7b, hub-reititys laajennettu
- Tofuist-agentin model vaihdettu qwen-coder → qwen2.5-coder:7b
- Hub: qwen2.5-coder:* matchaa nyt qwen-coder*-solmuille ja päinvastoin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:31:53 +03:00
1a58324689 Tofuist-agentti: OpenTofu/IaC-asiantuntija gecko-avatarilla
- Uusi agentti: Tofuist (gecko-avatar, oranssinkulta #e3a336)
- System prompt: HCL-koodi, moduulit, lifecycle, state encryption
- docs-kenttä: lataa automaattisesti /docs/tofu-cheatsheet.md referenssiksi
- kpnRun: tukee nyt agentin docs-kenttää (haetaan kerran, cachetetaan)
- OpenTofu-dokumentaatio haettu GitHubista + tiivistetty cheatsheet
- Avatar, gallery-head, värimapit ja pipeline-tuet lisätty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:29:42 +03:00
afc7f9bcee Versio 0.2.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:52:21 +03:00
5d2027b2ca Native-node: automaattinen Ollama-haistelu käynnistyksessä
Jos OLLAMA_URL ei ole asetettu, kokeillaan järjestyksessä:
1. localhost:11434 (paikallinen Ollama)
2. 127.0.0.1:11434
3. ollama:11434 (Docker-verkko)
4. host.docker.internal:11434 (Docker-kontti → isäntä)

Ensimmäinen joka vastaa /api/version-kutsuun valitaan.
Timeout 2s per kokeilu. Jos OLLAMA_URL on asetettu, sitä käytetään suoraan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:41:44 +03:00
8a4d515eed Client-compose: Ollama lisätty jokaiseen profiiliin (nvidia/amd/cpu)
Ollama-palvelu puuttui client-composesta — native-node yritti yhdistää
ollamaan jota ei ollut. Nyt jokaisessa profiilissa on oma Ollama
(nvidia: latest+GPU, amd: rocm+/dev/kfd, cpu: latest) network alias
'ollama' jotta native-node löytää sen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:40:33 +03:00
987a370a05 Docker: Cargo.lock valinnainen, Ollama AMD ROCm -tuella
- Dockerfile.native-node: Cargo.lock kopioidaan glob-patternilla (ei kaadu
  jos puuttuu, esim. .gitignore poistaa sen)
- docker-compose: Ollama vaihdettu ollama/ollama:rocm -imageen AMD GPU:lle,
  /dev/kfd + /dev/dri laitemappaukset, poistettu nvidia deploy.resources

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:33:27 +03:00
f75e7f07e9 Opas: tokenisointiesimerkki korvattu oikealla kuvakaappauksella
- Staattinen tekstitokenisointiesimerkki korvattu kuvalla joka
  näyttää värikoodatut tokenit EN/FI-vertailussa
- Markdown-renderöijään lisätty ![alt](src) kuvatuki
- Kuva: static/images/tokenization-example.png

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:20:50 +03:00
eb6f720fcc Wasm tokenize_js() exportti oppaan live-tokenizeria varten
Lisätty #[wasm_bindgen] tokenize_js(text) → JSON-funktio joka lataa
tokenizerin IndexedDB:stä tai HuggingFacesta tarvittaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:18:07 +03:00
e25d0ea8f2 Versio 0.2.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:15:05 +03:00
4d1e89da34 Avatar-kuvakkeet 48px → 50px (+5%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:14:14 +03:00
bf535b6256 Raportti: syntaksikorostus, CSS-tooltipsit rivityksellä, parannettu layout
- Lisätty highlightCode() — regex-pohjainen korostus .py, .html, Dockerfile
- Swimlane-badget: title-attr → CSS ::after tooltip (white-space:pre-wrap)
- Tiedostokortit: kieli + rivimäärä headeriin
- Yleinen ilme: pyöristetyt kulmat, monospace-fontti, parempi line-height
- Pipeline-vaiheet: selkeämmät erottimet ja väripaletti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:10:37 +03:00
29e1c440c6 Native-node osoittaa nyt tuotantohubiin (kipina.studio)
HUB_URL vaihdettu ws://agentic-poc:3000/ws → wss://kipina.studio/ws
jotta lokaali GPU-solmu palvelee tuotantoympäristöä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:05:19 +03:00
560bee1369 Laskentaverkko ja Koodilaboratorio -tabit takaisin näkyviin
Tabit olivat piilotettu display:none-tyylillä, mikä rikkoi myös
hash-navigoinnin (#codelab, #network).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:01:21 +03:00
b074e0cb49 Avatar-korttien opacity nostettu 0.5 → 0.8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:55:46 +03:00
9307c75516 Pysty/vaaka-toggle avatareille
Lisätty nappi jolla org-chartin voi vaihtaa vaaka- ja pystyasennon välillä.
Vaaka on oletuksena (kompakti), pysty lisää vertical-CSS-luokan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:55:07 +03:00
86191fbb6c Avatarit vaakariviin: kompakti layout, säästää pystytilaa
Org-chart muutettu vertikaalisesta hierarkiasta horisontaaliseksi riviksi:
Asiakas → Manageri → [Koodari, Data, QA, DevOps, Tarkkailija]
- Connector-viivat muutettu pystysuuntaisista vaakanuoliksi
- Avatar-kortit 72px leveitä (oli 130px), kuvat 48px (oli 80px)
- Roolikuvaus poistettu korteista — pelkkä nimi riittää
- Tarkkailija siirretty absolute-asemasta rivin loppuun
- Responsive-tyylit päivitetty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:53:25 +03:00
a6a94f7688 Avatar-logiikka: poistettu kilpaileva llm_prompt-handler, korjattu vilkkumisjärjestys
Ongelma: kaksi erillistä avatar-aktivointilogiikkaa kilpaili:
1) pipelineStep() — oikea agentti statuksen perusteella
2) llm_prompt-handler — arvasi agentin prompt-tekstistä (usein väärin → DevOps vilkkui QA:n/Datan sijaan)

Korjaus:
- Poistettu llm_prompt- ja llm_done-handlerien avatar-heuristiikat
- pipelineStep() hoitaa kaiken: active → syttyy, done → sammuu heti
- Pipeline-lopussa kaikki avataret sammutetaan eksplisiittisesti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:42:02 +03:00
8d5c5440d2 Avatar-vilahdus: oikea agentti aktivoituu vuorollaan, manageri-bugi korjattu
pipelineStep() aktivoi nyt oikean agentin avatarin (sekä card että gallery-head)
kun status on 'active', ja poistaa 'done'-statuksella sekunnin viiveellä.
Poistettu llm_done-handlerin turha manageri-aktivointi joka vilkutti aina manageria.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:38:08 +03:00
a12bd7ce7f One-liner koodi: system prompt vaatii rivinvaihdot + staattinen tarkistus
Ollaman system prompt: 'Use proper newlines and indentation'.
Staattinen analyysi: havaitsee jos .py-tiedosto on yhdellä rivillä.
Native node vaatii rebuildin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:07:51 +03:00
9ac90aa540 pyproject.toml esimerkki: lisätty httpx ja pytest riippuvuuksiin
TestClient vaatii httpx:n, testit vaativat pyTestin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:01:35 +03:00
32065d5818 Korjattu </script> index.html-esimerkissä joka katkaisi pääsivun JS:n
Sama bugi kuin aiemmin: template-literalin </script> sulkee
ulomman script-tagin. Pilkottu: '<'+'/script>'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:00:53 +03:00
321943ee3c Esimerkkiprojektit: täysi CRUD + HTML UI root-osoitteessa
Few-shot esimerkit päivitetty:
- main.py: GET/POST/PUT/DELETE + FileResponse("/") index.html:lle
- index.html: yksinkertainen UI fetch()-kutsuilla API:in
- /api/ -prefiksi JSON-endpointeille
- Esimerkkipromptit kuvaavat CRUD-operaatiot eksplisiittisesti

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:49:38 +03:00
1b75c89320 Raportti: Mermaid → custom swimlane (kompakti, tooltipsit)
Agenteittain ryhmitelty visualisointi:
  Manageri  ✓ Suunnittelu
  Koodari   ✓ models.py → ✓ main.py → ✓ index.html
  DevOps    ✓ Review → ✓ Dockerfile → ✓ Compose → ✓ README
  QA        ✓ Testit → ✓ Validointi
Hover näyttää selityksen + output-esikatselun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:39:23 +03:00
01622a960f Mermaid-kaavio LR (vaakasuunta) + tooltipsit joka vaiheelle
graph TD → graph LR: kaavio kiemurtelee vasemmalta oikealle.
Hover näyttää vaiheen selityksen ja output-esikatselun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:33:23 +03:00
4e4efda67d Korjattu </script> template-literalissa joka katkaisi pääsivun JS:n
Template-stringin sisällä oleva </script> sulki ulomman script-tagin.
Pilkottu string-konkatenoimalla: '</'+' script>'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:25:36 +03:00
f5db2eb034 Esimerkkiprojektit HTML UI:lla + Mermaid-kaavio raporttiin + tooltips
- Esimerkkipromptit sisältävät nyt HTML-käyttöliittymän
- Manageri generoi index.html tiedoston, Dockerfile kopioi sen
- README: docker compose up → http://localhost:8000
- Raporttiin Mermaid-kaavio agenttien workflowsta (CDN)
- Pipeline-vaiheiden hover näyttää selityksen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:06:15 +03:00
77c8d46e7b Staattinen analyysi: tiedostojen väliset importit tarkistetaan
from db import get_db → tarkistaa onko get_db määritelty db.py:ssä
(def, class tai muuttuja). Löytää puuttuvat exportit ennen Docker-buildia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:34:41 +03:00
f14eba1b49 Projektiraportti: HTML-dokumentaatio pipeline-vaiheista ja tiedostoista
Pipeline generoi HTML-raportin joka sisältää:
- Pipeline-vaiheet prompteineen ja tuloksineen (avattavat details)
- Staattisen analyysin tulokset
- Kaikki generoidut tiedostot korostettuina
- Raportti avautuu uuteen välilehteen linkistä terminaalissa
- Projektikorttiin lisätty 📄 Raportti -nappi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:22:03 +03:00
6d15298418 Parannettu staattinen analyysi + docker-compose template
Staattinen analyysi: tarkistaa 15 yleistä symbolia (Boolean, Text,
Depends, HTTPException jne.) puuttuvista importeista.
Models-esimerkki: lisätty Boolean ja Text importteihin.
docker-compose.yml: template ilman LLM:ää — ei enää version tai
turhaa PostgreSQL-containeria SQLite-projektissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:16:19 +03:00
cea1961183 Testaaja: staattinen analyysi + strukturoitu LLM-arviointi
Staattinen analyysi selaimessa (ennen LLM:ää):
- Käyttämättömät importit
- Puuttuvat importit (FastAPI, Session)
- Tyhjät funktiot

LLM-arviointi 5 kohdalla (esimerkkivastauksineen):
1. Imports, 2. Database, 3. Endpoints, 4. Error handling, 5. Security

Korjausluuppi käynnistyy jos ✗ löytyy tai staattinen analyysi huomauttaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:13:15 +03:00
21a8015ea3 SQLAlchemy esimerkki: declarative_base() → DeclarativeBase (v2.0+)
declarative_base() on poistettu SQLAlchemy 2.0:sta.
Päivitetty molemmat esimerkit käyttämään class Base(DeclarativeBase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:07:23 +03:00
c3991193d9 Pipeline-vaiheet rivittyvät + fixedDockerfile viittaus korjattu
Poistettu fixedDockerfile-viittaus joka kaatoi pipelinen ennen
renderProjectCard:ia → ZIP ei generoitunut.
Pipeline-vaiheet käyttävät nyt flex-wrap:ia eikä overflow-x:ää.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:03:14 +03:00
02c6d67218 Korjausvaihe ei ylikirjoita Dockerfilea — template pysyy
QA-validoinnin korjausvaihe antoi LLM:n generoida uuden Dockerfilen
joka sekoitti pip:n ja uv:n. Nyt korjaus kohdistuu vain .py ja
pyproject.toml -tiedostoihin. Dockerfile pysyy templatena.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:58:49 +03:00
de1cf009fa pyproject.toml: stdlib-moduulit (sqlite3, os, sys) kielletty riippuvuuksista
Malli laittoi sqlite3:n dependencies-listaan → uv sync epäonnistui.
Koodarin, QA:n ja validoinnin prompteihin lisätty selkeä kielto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:53:10 +03:00
060f36f479 ZIP-lataus: null-tarkistus tiedostoille + virheilmoitus
Lisätty guard puuttuvalle projectFiles-datalle ja null-safe content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:46:34 +03:00
e2ec0fa43d v0.2.2: responsiivinen UI, Ollama-proxy, mixed content korjaus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:23:35 +03:00
8752c0f465 Responsiivinen korkeus: terminaali ja UI skaalautuvat viewport-korkeuteen
Terminaali: clamp(200px, 35vh, 500px) — skaalautuu ikkunan mukaan.
<900px korkeus: pienempi otsikko, tiiviimmät avataret, matalampi terminaali.
>1200px korkeus: isompi terminaali ja promptikenttä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:22:15 +03:00
8c95282654 Tiivistetty layout: terminaali 500→300px, pienemmät marginaalit
Mahtuu 1440px korkeuteen ilman vieritystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:20:06 +03:00
a1bc1af646 Hardware API: Ollama-fallback kun wgpu ei tunnista GPU:ta Dockerissa
/api/v1/hardware tarkistaa nyt myös Ollaman tilan fallbackina.
kpn models näyttää ladattujen mallien määrän ja ✓ oikein.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:18:00 +03:00
6b27cbbade kpn load: rehellinen viesti — 'valittu' eikä 'ladattu ja aktiivinen'
Frontend ei tiedä onko malli oikeasti ladattu Ollamaan.
Nyt näytetään 'valittu — natiivisolmu lataa mallin' ja
varoitus ensimmäisen pyynnön hitaudesta.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:04:11 +03:00
4d9c51a86f .gitignore: *.db — ajonaikaiset tietokannat pois versionhallinnasta
nodes.db muuttui jatkuvasti ja esti deployn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:59:35 +03:00
66d1e8c4b1 Ollama-kutsut hubin kautta: ei mixed content HTTPS-sivulla
Lisätty GET /api/v1/ollama/tags proxy-endpoint hubiin.
Poistettu suorat http://hostname:11434 -kutsut frontendistä.
Hub välittää Ollama-kutsut sisäisessä Docker-verkossa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:57:48 +03:00
2eeac255f6 Piilotetut paneelit tavoitettavissa hashilla: #network, #laskentaverkko, #codelab
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:52:57 +03:00
6097cfc263 Ylimääräiset rönsyt karsittu. Playground suht ok. 2026-04-07 08:51:47 +03:00
8aed9f97a2 Puhuvat päät ja simulaatio-nappi piilotettu (koodi säilytetty)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:45:39 +03:00
c0ccd76a4c v0.2.1: Ollama-integraatio, pipeline, prompt-editori
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:41:28 +03:00
d2edb38879 Alaotsikko: 'Hajautettu WebGPU Laskentaverkko' → 'AI-ohjelmistokehitystiimi'
Simulaatio-viittaukset poistettu näkyvistä. Käännökset päivitetty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:40:22 +03:00
2755794554 Agenttien valinta: klikkaus = yksi, Shift+klikkaus = multi-select
Normaali klikkaus valitsee yhden agentin (poistaa muut valinnat).
Shift+klikkaus lisää/poistaa agentin valinnasta yhteistä promptia varten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:36:52 +03:00
dbb37b3c60 Laskentaverkko ja Koodilaboratorio piilotettu, Agents oletuksena
Simulaatio-välilehdet piilotettu display:none:lla (koodi säilytetty).
Agents & CLI on nyt oletusvälilehti.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:34:57 +03:00
0e7497b627 Oletuspromptit agentille + klikattavat pipeline-vaiheet
- Jokaisella agentilla on nyt oletusprompt joka näkyy heti modalissa
- Muokatut promptit tallentuvat localStorageen
- Pipeline-vaiherivin (✓ Suunnittelu → ✓ models.py → ...) klikkaus
  avaa modalin jossa näkyy kyseisen vaiheen prompti + tulos
- 📋 Näytä prompti -nappi näkyy aina kun agentti on valittu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:30:56 +03:00
248 changed files with 54116 additions and 446 deletions

6
.gitignore vendored
View File

@@ -34,3 +34,9 @@ Cargo.lock
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust,linux
# Ajonaikaiset tietokannat
*.db
# Wanha versio
temp/

215
network-poc/AGENTBUILDER.md Normal file
View File

@@ -0,0 +1,215 @@
# Kipinä Agent Builder — Suunnitelma
Käyttäjä voi rakentaa omia agentteja "hahmolomakkeella": valitsee avatarin, roolin, kielimallin ja muokkaa prompteja. Agentit tallentuvat localStorageen ja ovat käytettävissä pipelineissa.
## Nykytila
```js
// Kovakoodattu agentPrompts-objekti
const agentPrompts = {
manager: { name: 'Manageri', model: 'qwen2.5-coder:7b', default: '...' },
coder: { name: 'Koodari', model: 'qwen2.5-coder:7b', default: '...' },
tofuist: { name: 'Tofuist', model: 'qwen2.5-coder:7b', docs: '/docs/tofu-cheatsheet.md', default: '...' },
// ...
};
```
**Ongelma:** Uuden agentin lisääminen vaatii koodimuutoksen index.html:ään.
## Tavoite
```
┌─────────────────────────────────────────────────────┐
│ Agent Builder -lomake │
│ │
│ ┌─────────┐ Nimi: [Tofuist ] │
│ │ 🦎 │ Rooli: [IaC / Infra ▼] │
│ │ avatar │ Malli: [qwen2.5-coder:7b ▼] │
│ └─────────┘ Docs: [/docs/tofu-cheatsheet.md] │
│ │
│ System Prompt: │
│ ┌─────────────────────────────────────────────┐ │
│ │ You are an OpenTofu/Terraform IaC specialist│ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ LLM-parametrit: │
│ Temperature: [0.7] Top-k: [40] Max tokens: [512]│
│ │
│ [💾 Tallenna] [🗑️ Poista] [📤 Export JSON] │
└─────────────────────────────────────────────────────┘
```
## Building Blocks
### 1. Agenttiskeema
```js
{
id: 'tofuist', // uniikki tunniste
name: 'Tofuist', // näyttönimi
avatar: '/avatars/gecko_notext.png', // avatar-kuvan polku
role: 'iac', // rooli-template
model: 'qwen2.5-coder:7b', // eksakti Ollama-mallinimi
color: '#e3a336', // teemaväri UI:ssa
docs: '/docs/tofu-cheatsheet.md', // valinnainen referenssidokumentti
prompt: 'You are an OpenTofu...', // system prompt
params: { // LLM-parametrit
temperature: 0.7,
top_k: 40,
max_tokens: 512,
repetition_penalty: 1.15
}
}
```
### 2. Rooli-templatet (alasvetovalikko)
Valmiit pohjat jotka tuovat oletuspromptit ja parametrit:
| Rooli | Oletusprompt | Parametrit |
|-------|-------------|------------|
| Koodari | "Kirjoita selkeää, testattavaa koodia" | temp 0.7, max 512 |
| QA / Testaus | "Kirjoita testejä, etsi virheitä" | temp 0.4, max 512 |
| DevOps | "Dockerfile, Compose, CI/CD" | temp 0.5, max 512 |
| DevSecOps | "Tietoturva-auditointi, OWASP" | temp 0.3, max 512 |
| Arkkitehti | "Järjestelmäsuunnittelu, rajapinnat" | temp 0.6, max 512 |
| IaC / Infra | "OpenTofu/Terraform HCL-koodi" | temp 0.5, max 512 |
| Data | "Tietokannat, SQL, datamallit" | temp 0.5, max 512 |
| Manageri | "Tehtävien jako ja koordinointi" | temp 0.8, max 200 |
| Kirjoittaja | "Dokumentaatio, README, ohjeet" | temp 0.8, max 512 |
| Vapaa | (tyhjä, käyttäjä kirjoittaa) | temp 0.7, max 512 |
### 3. Malli-valitsin
Lista saatavilla olevista malleista — haetaan dynaamisesti:
```
Hub-kysely: GET /api/models → palauttaa yhdistettyjen solmujen mallit
Tai staattinen lista:
- qwen2.5-coder:7b (oletus, natiivi GPU)
- qwen2.5-coder:1.5b (kevyt)
- qwen2.5-coder:0.5b (selain Wasm)
- deepseek-r1 (reasoning)
- llama3.2:3b (yleiskäyttö)
```
Pitkän aikavälin tavoite: hub ilmoittaa WebSocketin kautta mitkä mallit ovat saatavilla.
### 4. Avatar-valitsin
Valmiit avatarit + mahdollisuus ladata oma:
| Hahmo | Tiedosto | Eläin |
|-------|----------|-------|
| Asiakas | kettu_notext.png | Kettu |
| Manageri | karhunpentu.png | Karhunpentu |
| Koodari | kipina_notext.png | Salamanteri |
| Data | pesukarhu_notext.png | Pesukarhu |
| QA | susi_notext.png | Pikkususi |
| DevOps | laiskiainen_notext.png | Laiskiainen |
| Tarkkailija | aikuinen_susi.png | Aikuinen susi |
| Tofuist | gecko_notext.png | Gecko/Lisko |
| Arkkitehti | ??? | (tulossa) |
| DevSecOps | ??? | (tulossa) |
### 5. Docs-kenttä (referenssidokumentti)
Agentti voi viitata ulkoiseen dokumenttiin joka ladataan promptiin:
```
docs: '/docs/tofu-cheatsheet.md' → haetaan fetch():llä, cachetetaan _docsCache-kenttään
```
**Toiminta:**
1. Ensimmäisellä `kpnRun`-kutsulla ladataan docs-URL
2. Sisältö cachetetaan `agent._docsCache`-kenttään
3. Liitetään promptiin: `"Reference:\n" + docsContent`
4. Ei ladata uudelleen saman session aikana
**Rajoitukset:**
- Max ~3000 tokenia (~10 KB) — pidempi docs tiivistetään
- Vain tekstitiedostot (.md, .txt)
### 6. Tallennus (localStorage)
```js
// Tallennusavain
'kpn-custom-agents' JSON.stringify([ agentSkeema1, agentSkeema2, ... ])
// Ladattaessa
const customAgents = JSON.parse(localStorage.getItem('kpn-custom-agents') || '[]');
const defaultAgents = { manager: {...}, coder: {...}, ... };
const agentPrompts = { ...defaultAgents };
for (const agent of customAgents) {
agentPrompts[agent.id] = agent;
}
```
**Oletusagentit** (manager, coder, tester, qa, data) ovat aina mukana — niitä ei voi poistaa, mutta prompteja voi muokata.
**Käyttäjäagentit** (tofuist, arkkitehti, devsecops, ...) tallentuvat localStorageen ja latautuvat käynnistyksessä.
### 7. Export / Import
```js
// Export — JSON-tiedosto
const blob = new Blob([JSON.stringify(agent, null, 2)], { type: 'application/json' });
// → agent-tofuist.json
// Import — tiedoston valinta tai drag & drop
// Validoidaan skeema, lisätään agentPrompts-objektiin
```
Mahdollistaa agenttien jakamisen tiimin kesken.
## Toteutusvaiheet
### Vaihe 1: Hahmolomake UI
- Avatar-grid valitsin
- Rooli-template alasvetovalikko (täyttää oletuspromptit)
- Malli-valitsin
- System prompt -tekstikenttä
- LLM-parametrit (temperature, top-k, max_tokens)
- Tallenna/Poista-napit
### Vaihe 2: Dynaaminen agenttirekisteri
- `agentPrompts` ladataan localStoragesta
- Oletusagentit + käyttäjän agentit yhdistetään
- Avatar-kortit renderöidään dynaamisesti (ei HTML:ssä)
- Värimapit generoidaan agenttiskeemasta
### Vaihe 3: Pipeline käyttää dynaamisia agentteja
- Pipeline-vaiheet viittaavat agentin id:hen (ei kovakoodattuun nimeen)
- Käyttäjä voi valita mitkä agentit osallistuvat pipelineen
- Tofuist voi korvata DevOpsin IaC-projekteissa
### Vaihe 4: Mallirekisteri (hub-integraatio)
- Hub tarjoaa `/api/models`-endpointin
- Saatavilla olevat mallit näkyvät valitsimessa reaaliajassa
- Solmun liittyessä/poistuessa mallit päivittyvät
## Arkkitehtuurikaavio
```
┌──────────────────────────────────────────────────┐
│ Agent Builder UI │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Avatar │ │ Rooli │ │ Malli-valitsin │ │
│ │ Grid │ │ Template │ │ (hub/staattinen) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ └─────────────┼───────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Agent Schema { id, name, avatar, model, │ │
│ │ role, color, docs, prompt, │ │
│ │ params } │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ localStorage Org Chart Pipeline │
│ (persist) (render) (execute) │
└──────────────────────────────────────────────────┘
```

View File

@@ -0,0 +1,21 @@
# Native-node: Rust + Ollama-client (ei GPU-tunnistusta)
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY native-node/src native-node/src
# Dummy-cratet workspace-yhteensopivuuteen
COPY hub/Cargo.toml hub/Cargo.toml
COPY node/Cargo.toml node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
RUN mkdir -p hub/src node/src cli/src && touch hub/src/main.rs node/src/lib.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release -p native-node --no-default-features \
&& cp /app/target/release/native-node /usr/local/bin/native-node
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/native-node /usr/local/bin/native-node
CMD ["native-node"]

View File

@@ -5,7 +5,8 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY Cargo.toml ./
COPY Cargo.loc[k] ./
COPY hub/Cargo.toml hub/Cargo.toml
COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml

View File

@@ -1,47 +1,63 @@
# syntax=docker/dockerfile:1
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y \
curl pkg-config libssl-dev g++ \
&& rm -rf /var/lib/apt/lists/*
# --- Vaihe 1: Frontend (Astro) ---
FROM node:22-slim AS frontend
WORKDIR /app/frontend
# Riippuvuudet ensin → cache-kerros (muuttuu harvoin)
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install --silent
# Lähdekoodi → muuttuu usein, mutta npm install on cachessa
COPY frontend/ .
RUN npm run build
# --- Vaihe 2: Wasm (wasm-pack) ---
FROM rust:slim AS wasm-builder
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev g++ && rm -rf /var/lib/apt/lists/*
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
WORKDIR /app
# Kopioi kaikki Cargo-tiedostot
COPY Cargo.toml ./
COPY Cargo.lock* ./
COPY Cargo.toml Cargo.lock* ./
COPY node/Cargo.toml node/Cargo.toml
COPY node/src node/src
# Dummy-cratet jotta workspace Cargo.toml on tyytyväinen
COPY hub/Cargo.toml hub/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
RUN mkdir -p hub/src native-node/src cli/src && touch hub/src/main.rs native-node/src/main.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir /app/wasm-pkg
# --- Vaihe 3: Hub (Rust) ---
FROM rust:slim AS hub-builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY hub/Cargo.toml hub/Cargo.toml
COPY hub/src hub/src
# Tarvitaan dummy-cratet jotta workspace kompiloi
COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
# Kopioi lähdekoodi
COPY hub/src hub/src
COPY node/src node/src
COPY native-node/src native-node/src
COPY cli/src cli/src
COPY static static
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir ../static/pkg
# Rakenna Hub
RUN mkdir -p node/src native-node/src cli/src && touch node/src/lib.rs native-node/src/main.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release -p hub \
&& cp /app/target/release/hub /usr/local/bin/hub
# --- Vaihe 4: Tuotantoimage ---
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=builder /app/static /app/static
COPY --from=hub-builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=frontend /app/frontend/dist /app/frontend/dist
COPY --from=wasm-builder /app/wasm-pkg /app/frontend/dist/pkg
# Kopioidaan GUIDE.md ja templates
COPY frontend/public/GUIDE.md /app/frontend/dist/GUIDE.md
COPY frontend/public/templates /app/frontend/dist/templates
COPY frontend/public/avatars /app/frontend/dist/avatars
WORKDIR /app
ENV STATIC_DIR=/app/static
ENV STATIC_DIR=/app/frontend/dist
EXPOSE 3000
CMD ["hub"]

38
network-poc/build-binaries.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Käännä kipina-node binäärit kaikille alustoille
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUT="$SCRIPT_DIR/frontend/public/download"
mkdir -p "$OUT"
echo "=== Kipinä Node — Binary Build ==="
# macOS ARM (natiivi)
echo "[1/3] macOS ARM64..."
cd "$SCRIPT_DIR"
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
cp target/release/native-node "$OUT/kipina-node-macos-arm64"
echo " $(ls -lh "$OUT/kipina-node-macos-arm64" | awk '{print $5}')"
# Linux x86_64 (Docker)
echo "[2/3] Linux x86_64..."
docker run --rm \
-v "$SCRIPT_DIR":/app -w /app \
--platform linux/amd64 \
rust:slim \
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-x86_64"
echo " $(ls -lh "$OUT/kipina-node-linux-x86_64" | awk '{print $5}')"
# Linux ARM64 (Docker)
echo "[3/3] Linux ARM64..."
docker run --rm \
-v "$SCRIPT_DIR":/app -w /app \
--platform linux/arm64 \
rust:slim \
bash -c "apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1 && cargo build --release -p native-node --no-default-features 2>&1 | tail -1 && cp target/release/native-node /app/frontend/public/download/kipina-node-linux-arm64"
echo " $(ls -lh "$OUT/kipina-node-linux-arm64" | awk '{print $5}')"
echo ""
echo "=== Binäärit valmiina ==="
ls -lh "$OUT"/kipina-node-*

28
network-poc/deploy-fast.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Nopea deploy: päivittää vain frontendin (ei kontin uudelleenkäynnistystä)
# Hub-binäärin päivitys: käytä deploy.sh tai deploy-light.sh
set -e
SERVER="ubuntu@86.50.252.98"
REMOTE_DIR="~/code/agentic-studio/network-poc"
SSH_OPTS="-o StrictHostKeyChecking=no"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio — Frontend Deploy ==="
# 1. Buildaa frontend paikallisesti
echo "[1/2] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
[ -d node_modules ] || npm install --silent
npm run build --silent 2>&1 | tail -1
# 2. Synkataan dist/ palvelimelle (vain muuttuneet tiedostot)
echo "[2/2] Synkataan dist/ → palvelin..."
ssh $SSH_OPTS $SERVER "mkdir -p $REMOTE_DIR/frontend/dist"
rsync -az --delete -e "ssh $SSH_OPTS" "$SCRIPT_DIR/frontend/dist/" "$SERVER:$REMOTE_DIR/frontend/dist/"
echo ""
echo "=== Valmis! Frontend päivitetty — ei uudelleenkäynnistystä ==="
echo " https://kipina.studio"
echo ""
echo "Huom: Jos Rust-koodi (hub/) muuttui, aja: ./deploy.sh"

33
network-poc/deploy-light.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Kevyt deploy: lähetetään vain koodi, palvelin buildaa itse
set -e
SERVER="ubuntu@86.50.252.98"
REMOTE_DIR="~/code/agentic-studio/network-poc"
SSH_OPTS="-o StrictHostKeyChecking=no"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Deploy (remote build) ==="
# 1. Synkataan koodi palvelimelle (vain muuttuneet tiedostot)
echo "[1/3] Synkataan koodi..."
rsync -az --delete \
--exclude 'target/' \
--exclude 'node_modules/' \
--exclude 'dist/' \
--exclude '.astro/' \
--exclude 'temp/' \
--exclude '*.db' \
--exclude '.git/' \
"$SCRIPT_DIR/" "$SERVER:$REMOTE_DIR/"
# 2. Rakennetaan image palvelimella
echo "[2/3] Rakennetaan image palvelimella..."
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker build -f Dockerfile.prod -t kipina-agentic:latest ."
# 3. Käynnistetään
echo "[3/3] Käynnistetään..."
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d"
echo "=== Valmis! https://kipina.studio ==="

View File

@@ -1,13 +1,12 @@
services:
# NVIDIA GPU -solmu
native-node-nvidia:
build:
context: .
dockerfile: Dockerfile.native-node
container_name: kipina-node-nvidia
environment:
- HUB_URL=wss://kipina.studio/ws
- ALLOCATED_GB=4
# Ollama NVIDIA GPU:lla
ollama-nvidia:
image: ollama/ollama:latest
container_name: kipina-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
deploy:
resources:
@@ -16,6 +15,65 @@ services:
- driver: nvidia
count: all
capabilities: [gpu]
networks:
default:
aliases:
- ollama
profiles:
- nvidia
# Ollama AMD ROCm GPU:lla
ollama-amd:
image: ollama/ollama:rocm
container_name: kipina-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
group_add:
- video
- render
networks:
default:
aliases:
- ollama
profiles:
- amd
# Ollama CPU:lla
ollama-cpu:
image: ollama/ollama:latest
container_name: kipina-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
networks:
default:
aliases:
- ollama
profiles:
- cpu
# NVIDIA GPU -solmu
native-node-nvidia:
build:
context: .
dockerfile: Dockerfile.native-node
container_name: kipina-node-nvidia
environment:
- HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4
restart: unless-stopped
depends_on:
- ollama-nvidia
profiles:
- nvidia
@@ -27,14 +85,12 @@ services:
container_name: kipina-node-amd
environment:
- HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4
restart: unless-stopped
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
group_add:
- video
- render
depends_on:
- ollama-amd
profiles:
- amd
@@ -46,7 +102,14 @@ services:
container_name: kipina-node-cpu
environment:
- HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=2
restart: unless-stopped
depends_on:
- ollama-cpu
profiles:
- cpu
volumes:
ollama-models:

View File

@@ -19,8 +19,12 @@ services:
restart: unless-stopped
environment:
- DATABASE_PATH=/data/nodes.db
- STATIC_DIR=/app/frontend/dist
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
- NODE_API_KEY=${NODE_API_KEY:-}
volumes:
- hub_data:/data
- ./frontend/dist:/app/frontend/dist:ro
volumes:
caddy_data:

View File

@@ -11,21 +11,19 @@ services:
# Käännetään aina käynnistyksen yhteydessä varmuuden vuoksi Wasm uusimmista koodeista, ja päälle pyöräytetään Hub!
command: bash -c "cd node && wasm-pack build --release --target web --out-dir ../static/pkg && cd ../hub && cargo run"
# Ollama — LLM-inferenssi GPU:lla (NVIDIA/AMD/Apple)
# Ollama — LLM-inferenssi
# NVIDIA: vaihda image → ollama/ollama:latest ja lisää deploy.resources (ks. README)
# CPU: vaihda image → ollama/ollama:latest ja poista devices
ollama:
image: ollama/ollama:latest
image: ollama/ollama:rocm
container_name: kipina_ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
devices:
- /dev/kfd
- /dev/dri
profiles:
- native
@@ -36,12 +34,11 @@ services:
dockerfile: Dockerfile.native-node
container_name: kipina_native_node
environment:
- HUB_URL=ws://agentic-poc:3000/ws
- HUB_URL=wss://kipina.studio/ws
- OLLAMA_URL=http://ollama:11434
- OLLAMA_MODEL=qwen2.5-coder:7b
- ALLOCATED_GB=4
depends_on:
- agentic-poc
- ollama
profiles:
- native

3
network-poc/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.astro/

View File

@@ -0,0 +1,2 @@
import { defineConfig } from 'astro/config';
export default defineConfig({});

4721
network-poc/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"name": "kipina-frontend",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^6.1.5"
}
}

View File

@@ -36,29 +36,13 @@ sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
merkkijonot harjoitusdatasta.
### Esimerkki: koodi
```
"print('Hello')" → [print] [(' ] [Hello] [')] = 4 tokenia
"tulosta('Hei')" → [tul] [osta] [(' ] [He] [i] [')] = 6 tokenia
```
Koodi tokenisoidaan tehokkaasti koska `print`, `def`, `return` yms.
ovat kokonaisia tokeneita. Suomenkielinen `tulosta` joudutaan pilkkomaan
osiin koska se ei esiinny harjoitusdatassa kokonaisena.
### Esimerkki: suomi vs. englanti
Sama lause kahdella kielellä Qwen2.5-Coder -tokenisaattorilla:
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:
| | Teksti | Tokenit | Määrä | Merkkejä/token |
|---|---|---|---|---|
| EN | The cat sat on the mat | [The] [ cat] [ sat] [ on] [ the] [ mat] | **6** | 3.7 |
| FI | Kissa istui matolla | [K] [issa] [ ist] [ui] [ mat] [olla] | **6** | 3.2 |
| EN | Distributed computing in the browser | [Dist] [ributed] [ computing] [ in] [ the] [ browser] | **6** | 6.0 |
| FI | Hajautettu laskenta selaimessa | [H] [aj] [au] [tettu] [ las] [kenta] [ sel] [aim] [essa] | **9** | 3.3 |
| EN | Write a function that sorts a list | [Write] [ a] [ function] [ that] [ sorts] [ a] [ list] | **7** | 5.0 |
| FI | Kirjoita funktio joka lajittelee listan | [K] [irj] [oita] [ funkt] [io] [ joka] [ laj] [ittel] [ee] [ listan] | **10** | 4.0 |
![Tokenisointivertailu EN/FI](/images/tokenization-example.png)
**Huomaa miten:**
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Kipinä — liitä koneesi laskentaverkkoon
set -e
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ Kipinä Agentic Network — Node Join ║"
echo " ╚══════════════════════════════════════╝"
echo ""
# 1. Ollama
if command -v ollama &>/dev/null; then
echo " ✓ Ollama löytyi: $(ollama --version 2>/dev/null || echo 'asennettu')"
else
echo " Ollama ei ole asennettu."
echo ""
read -p " Asennetaanko Ollama? (k/e) " -n 1 -r; echo
if [[ $REPLY =~ ^[Kk]$ ]]; then
echo " Asennetaan Ollama..."
curl -fsSL https://ollama.ai/install.sh | sh
else
echo " Ollama vaaditaan laskentaan. Asenna: https://ollama.ai"
exit 1
fi
fi
# 2. Varmistetaan että Ollama on käynnissä
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
echo " Käynnistetään Ollama..."
ollama serve &>/dev/null &
sleep 3
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
echo " ✗ Ollama ei käynnistynyt. Aja: ollama serve"
exit 1
fi
fi
echo " ✓ Ollama käynnissä"
# 3. Malli
if ollama list 2>/dev/null | grep -q "$MODEL"; then
echo " ✓ Malli $MODEL ladattu"
else
echo " Ladataan malli $MODEL..."
ollama pull "$MODEL"
fi
# 4. Native-node
echo ""
echo " Yhdistetään hubiin: $HUB_URL"
echo " Malli: $MODEL"
echo " Ctrl+C pysäyttää"
echo ""
# Tarkistetaan onko native-node käännetty
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NATIVE_BIN="$SCRIPT_DIR/target/release/native-node"
if [ -f "$NATIVE_BIN" ]; then
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
elif command -v cargo &>/dev/null && [ -f "$SCRIPT_DIR/native-node/Cargo.toml" ]; then
echo " Käännetään native-node..."
cd "$SCRIPT_DIR"
cargo build --release -p native-node --no-default-features 2>&1 | tail -1
HUB_URL="$HUB_URL" OLLAMA_MODEL="$MODEL" "$NATIVE_BIN"
else
echo " ✗ native-node binääriä ei löydy eikä Rust ole asennettu."
echo " Asenna Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo " Tai lataa valmis binääri: https://kipina.studio/download"
exit 1
fi

View File

@@ -0,0 +1,121 @@
#!/bin/bash
# Kipinä Node — lataa oikea binääri ja käynnistä
set -e
BASE_URL="https://kipina.studio/download"
HUB_URL="${KIPINA_HUB:-wss://kipina.studio/ws}"
MODEL="${KIPINA_MODEL:-qwen2.5-coder:3b}"
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
# Tunnista OS ja arkkitehtuuri
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS-$ARCH" in
darwin-arm64) BINARY="kipina-node-macos-arm64" ;;
darwin-x86_64) BINARY="kipina-node-macos-arm64" ;; # Rosetta
linux-x86_64) BINARY="kipina-node-linux-x86_64" ;;
linux-aarch64) BINARY="kipina-node-linux-arm64" ;;
*) echo "Ei tuettu: $OS-$ARCH"; exit 1 ;;
esac
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ Kipinä Agentic Node ║"
echo " ╚══════════════════════════════════════╝"
echo ""
echo " OS: $OS ($ARCH)"
echo ""
# Etsi Ollama-instanssit
CANDIDATES=(
"http://localhost:11434"
"http://127.0.0.1:11434"
"http://ollama:11434"
"http://host.docker.internal:11434"
)
# Lisää OLLAMA_URL listaan jos asetettu ja ei jo mukana
if [ -n "$OLLAMA_URL" ]; then
ALREADY=false
for c in "${CANDIDATES[@]}"; do
[ "$c" = "$OLLAMA_URL" ] && ALREADY=true
done
$ALREADY || CANDIDATES=("$OLLAMA_URL" "${CANDIDATES[@]}")
fi
echo " Etsitään Ollama-instansseja..."
FOUND=()
for url in "${CANDIDATES[@]}"; do
if curl -s --connect-timeout 1 "$url/api/tags" &>/dev/null; then
FOUND+=("$url")
fi
done
if [ ${#FOUND[@]} -eq 0 ]; then
# Ei löytynyt — yritä käynnistää lokaali
if command -v ollama &>/dev/null; then
echo " Käynnistetään Ollama..."
ollama serve &>/dev/null &
sleep 3
if curl -s --connect-timeout 1 "http://localhost:11434/api/tags" &>/dev/null; then
OLLAMA_URL="http://localhost:11434"
echo " ✓ Ollama käynnistetty ($OLLAMA_URL)"
else
echo " ✗ Ollaman käynnistys epäonnistui."
exit 1
fi
else
echo ""
echo " ✗ Ollamaa ei löytynyt."
echo " Kontti/remote: OLLAMA_URL=http://HOST:11434 ./kipina-node"
echo " Asenna: curl -fsSL https://ollama.ai/install.sh | sh"
exit 1
fi
elif [ ${#FOUND[@]} -eq 1 ]; then
OLLAMA_URL="${FOUND[0]}"
echo " ✓ Ollama löytyi: $OLLAMA_URL"
else
echo ""
echo " Löytyi ${#FOUND[@]} Ollama-instanssia:"
echo ""
for i in "${!FOUND[@]}"; do
echo " $((i+1))) ${FOUND[$i]}"
done
echo ""
read -p " Valitse [1-${#FOUND[@]}]: " -r CHOICE
if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le ${#FOUND[@]} ]; then
OLLAMA_URL="${FOUND[$((CHOICE-1))]}"
else
OLLAMA_URL="${FOUND[0]}"
echo " Käytetään oletusta: $OLLAMA_URL"
fi
echo " ✓ Valittu: $OLLAMA_URL"
fi
echo ""
echo " Hub: $HUB_URL"
echo " Ollama: $OLLAMA_URL"
echo " Malli: $MODEL"
# Lataa malli (toimii sekä lokaalilla binäärillä että API:n kautta)
if ! curl -s "$OLLAMA_URL/api/tags" | grep -q "$MODEL"; then
echo " Ladataan $MODEL..."
curl -s "$OLLAMA_URL/api/pull" -d "{\"name\":\"$MODEL\"}" > /dev/null
fi
echo " ✓ Malli $MODEL valmis"
# Lataa binääri
BIN_PATH="./kipina-node-bin"
if [ ! -f "$BIN_PATH" ]; then
echo " Ladataan $BINARY..."
curl -sSL "$BASE_URL/$BINARY" -o "$BIN_PATH"
chmod +x "$BIN_PATH"
fi
echo ""
echo " ✓ Yhdistetään laskentaverkkoon..."
echo " Ctrl+C pysäyttää"
echo ""
HUB_URL="$HUB_URL" OLLAMA_URL="$OLLAMA_URL" OLLAMA_MODEL="$MODEL" exec "$BIN_PATH"

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>;

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/*"
]
}

View File

@@ -0,0 +1,27 @@
{
"name": "FastAPI CRUD",
"description": "REST API with SQLite database",
"files": {
"models.py": {
"description": "SQLAlchemy models, engine, and session",
"example": "from sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nDATABASE_URL = \"sqlite:///./app.db\"\nengine = create_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\nclass Item(Base):\n __tablename__ = \"items\"\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(100), nullable=False)\n description = Column(String(500))",
"instructions": "Define the SQLAlchemy model based on the project description. Always include:\n- engine with check_same_thread=False for SQLite\n- SessionLocal with autocommit=False\n- Base = declarative_base()\n- Model class with __tablename__, primary key, and fields"
},
"schemas.py": {
"description": "Pydantic request/response schemas",
"example": "from pydantic import BaseModel\n\nclass ItemCreate(BaseModel):\n name: str\n description: str | None = None\n\nclass ItemResponse(ItemCreate):\n id: int\n\n class Config:\n from_attributes = True",
"instructions": "Create Pydantic schemas that match the SQLAlchemy model:\n- Create schema: fields without id (user provides these)\n- Response schema: inherits from Create, adds id\n- Add class Config with from_attributes = True (required for SQLAlchemy ORM)"
},
"main.py": {
"description": "FastAPI app with CRUD endpoints",
"example": "from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom models import Base, engine, SessionLocal, Item\nfrom schemas import ItemCreate, ItemResponse\n\nBase.metadata.create_all(bind=engine)\napp = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n@app.post(\"/items/\", response_model=ItemResponse, status_code=201)\ndef create_item(item: ItemCreate, db: Session = Depends(get_db)):\n db_item = Item(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.get(\"/items/\", response_model=list[ItemResponse])\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@app.get(\"/items/{item_id}\", response_model=ItemResponse)\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n if not item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n return item\n\n@app.put(\"/items/{item_id}\", response_model=ItemResponse)\ndef update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n for key, value in item.model_dump().items():\n setattr(db_item, key, value)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.delete(\"/items/{item_id}\", status_code=204)\ndef delete_item(item_id: int, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n db.delete(db_item)\n db.commit()",
"instructions": "Create the FastAPI app with all CRUD endpoints:\n- Import from models.py and schemas.py (use exact class names)\n- create_all(bind=engine) at module level\n- get_db dependency with yield pattern\n- POST (201), GET list, GET by id, PUT, DELETE (204)\n- Use response_model for type safety\n- Use model_dump() not dict() (Pydantic v2)"
},
"pyproject.toml": {
"description": "Project dependencies",
"example": "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"fastapi\",\n \"uvicorn[standard]\",\n \"sqlalchemy\",\n]\n\n[project.scripts]\ndev = \"uvicorn main:app --reload\"",
"instructions": "Use [project] format (PEP 621, compatible with uv). List dependencies under [project.dependencies]. Add [project.scripts] with dev command. Never use requirements.txt or Poetry format. Run with: uv run uvicorn main:app --reload"
}
},
"order": ["models.py", "schemas.py", "main.py", "pyproject.toml"]
}

View File

@@ -0,0 +1,79 @@
<!-- Agenttigalleria + konfigurointipaneeli -->
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
<!-- Agenttilista (drag & drop) -->
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
<!-- Renderöidään JS:stä -->
</div>
<!-- + Lisää agentti -->
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Lisää</span>
</div>
</div>
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px">
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
<div>
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agentin nimi">
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
</div>
</div>
<!-- Malli -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</label>
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
<option value="qwen-coder">Qwen2.5-Coder:0.5B (selain)</option>
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
</select>
</div>
<!-- System prompt -->
<div style="margin-bottom:10px" title="Agentin perusohje joka lähetetään kielimallille jokaisessa pyynnössä.&#10;&#10;Hyvän promptin rakenne:&#10;1. Rooli: 'You are an expert...'&#10;2. Säännöt: RULES/CRITICAL RULES listana&#10;3. Esimerkit: EXAMPLE OUTPUT&#10;4. Kiellot: NEVER-lista&#10;&#10;Vinkki: käytä englantia — malli ymmärtää sen paremmin ja se kuluttaa vähemmän tokeneita.">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px;cursor:help">System prompt 💡</label>
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
</div>
<!-- Sampling-parametrit -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling-parametrit</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div title="Kontrolloi 'luovuutta'. Matala arvo (0.2-0.4) tuottaa ennustettavaa, toistettavaa koodia — hyvä testaajille ja reviewereille. Keskiarvo (0.6-0.8) on paras koodin generointiin. Korkea arvo (1.0+) lisää vaihtelua mutta myös virheitä.&#10;&#10;Suositus:&#10;• Manageri: 0.5 (tarkat tiedostolistat)&#10;• Koodari: 0.7 (toimiva koodi + vaihtelu)&#10;• Testaaja: 0.3 (deterministinen arviointi)">
<label style="font-size:11px;color:#8b949e;cursor:help">Temperature 💡 <span id="config-temp-val" style="color:var(--accent);float:right">0.7</span></label>
<input type="range" id="config-temperature" min="0" max="1.5" step="0.1" value="0.7" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">0=tarkka · 0.7=oletus · 1.5=luova</div>
</div>
<div title="Vastauksen maksimipituus tokeneina (~1 token ≈ 4 merkkiä).&#10;&#10;Suositus:&#10;• Manageri: 256-512 (lyhyet tiedostolistat)&#10;• Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit)&#10;• Testaaja: 256-512 (lyhyet arvioinnit)&#10;&#10;Jos koodi katkeaa kesken, nosta tätä. Jos malli tuottaa turhaa toistoa, laske.">
<label style="font-size:11px;color:#8b949e;cursor:help">Max tokens 💡 <span id="config-maxtok-val" style="color:var(--accent);float:right">1024</span></label>
<input type="range" id="config-maxtokens" min="64" max="4096" step="64" value="1024" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">Vastauksen maksimipituus</div>
</div>
<div title="Montako todennäköisintä tokenia huomioidaan valinnassa. Pieni arvo (1-10) tekee vastauksesta deterministisen. Suuri arvo (50-100) sallii harvinaisempia sanoja.&#10;&#10;Suositus:&#10;• Boilerplate-koodi: 20-30 (tutut patternit)&#10;• Yleiskoodi: 40 (hyvä oletus)&#10;• Luova teksti: 60-80&#10;&#10;Yleensä ei tarvitse muuttaa oletuksesta.">
<label style="font-size:11px;color:#8b949e;cursor:help">Top-K 💡 <span id="config-topk-val" style="color:var(--accent);float:right">40</span></label>
<input type="range" id="config-topk" min="1" max="100" step="1" value="40" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1=greedy · 40=oletus · 100=laaja</div>
</div>
<div title="Vähentää jo tuotettujen sanojen todennäköisyyttä. Estää mallia toistamasta samaa lausetta. Liian korkea arvo (>1.5) voi rikkoa koodin koska samat avainsanat (return, if, def) ovat tarpeellisia.&#10;&#10;Suositus:&#10;• Koodi: 1.1-1.2 (lievä, sallii toiston)&#10;• Teksti: 1.15-1.3 (vahvempi)&#10;• Review: 1.0-1.1 (ei rangaistusta, lyhyet vastaukset)">
<label style="font-size:11px;color:#8b949e;cursor:help">Repetition penalty 💡 <span id="config-rep-val" style="color:var(--accent);float:right">1.15</span></label>
<input type="range" id="config-repeat" min="1.0" max="2.0" step="0.05" value="1.15" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1.0=ei · 1.15=oletus · 2.0=vahva</div>
</div>
</div>
</div>
<!-- Pipeline-järjestys -->
<div>
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- 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>

View File

@@ -0,0 +1,6 @@
<!-- 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>

View File

@@ -0,0 +1,67 @@
<!-- Asetukset-paneeli: kaikki LLM-parametrit muokattavissa -->
<div id="panel-settings" class="panel">
<div style="max-width:800px;margin:0 auto;padding:20px">
<h2 style="color:#e6edf3;margin-bottom:16px">Asetukset</h2>
<p style="color:#8b949e;margin-bottom:20px;font-size:14px">Kaikki kielimallin toimintaan vaikuttavat parametrit. Muutokset tallentuvat automaattisesti.</p>
<!-- System prompt -->
<div class="settings-section">
<h3 class="settings-title">System Prompt</h3>
<p class="settings-desc">Kielimallin perusohje joka lähetetään jokaisessa pyynnössä. Määrittää mallin käyttäytymisen.</p>
<textarea id="set-system-prompt" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Sampling -->
<div class="settings-section">
<h3 class="settings-title">Sampling-parametrit</h3>
<p class="settings-desc">Kontrolloi miten malli valitsee seuraavan tokenin. <a href="#guide" onclick="switchTab('guide')" style="color:var(--accent)">Lue lisää oppaasta.</a></p>
<div class="settings-grid">
<div>
<label class="settings-label">Temperature <span id="set-temp-val" class="settings-val">0.7</span></label>
<input type="range" id="set-temperature" min="0" max="1.5" step="0.1" value="0.7" class="settings-slider">
<div class="settings-hint">0 = deterministic, 0.7 = balanced, 1.5 = creative</div>
</div>
<div>
<label class="settings-label">Top-K <span id="set-topk-val" class="settings-val">40</span></label>
<input type="range" id="set-topk" min="1" max="100" step="1" value="40" class="settings-slider">
<div class="settings-hint">Montako tokenia huomioidaan. 1 = greedy, 40 = oletus</div>
</div>
<div>
<label class="settings-label">Repetition Penalty <span id="set-rep-val" class="settings-val">1.15</span></label>
<input type="range" id="set-repeat" min="1.0" max="2.0" step="0.05" value="1.15" class="settings-slider">
<div class="settings-hint">Estää toistoa. 1.0 = ei rangaistusta, 1.15 = oletus</div>
</div>
<div>
<label class="settings-label">Max Tokens <span id="set-maxtok-val" class="settings-val">1024</span></label>
<input type="range" id="set-maxtokens" min="64" max="4096" step="64" value="1024" class="settings-slider">
<div class="settings-hint">Vastauksen maksimipituus tokeneina</div>
</div>
</div>
</div>
<!-- Stop-sekvenssit -->
<div class="settings-section">
<h3 class="settings-title">Stop-sekvenssit</h3>
<p class="settings-desc">Generointi katkeaa kun malli tuottaa jonkin näistä. Yksi per rivi.</p>
<textarea id="set-stop-sequences" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Malli -->
<div class="settings-section">
<h3 class="settings-title">Malli (Ollama)</h3>
<p class="settings-desc">Natiivisolmun käyttämä kielimalli. Muutos vaatii native-noden uudelleenkäynnistyksen.</p>
<select id="set-model" class="settings-select">
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (~80 tok/s, ~1GB)</option>
<option value="qwen2.5-coder:3b">Qwen2.5-Coder:3B (~50 tok/s, ~2GB)</option>
<option value="qwen2.5-coder:7b-instruct-q4_K_M">Qwen2.5-Coder:7B Q4 (~30 tok/s, ~4GB)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (~20 tok/s, ~7GB)</option>
</select>
</div>
<!-- Reset -->
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button>
<span style="color:#8b949e;font-size:12px;margin-left:8px">Palauttaa kaikki parametrit oletusarvoihin</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
<!-- 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 selaimessa">Alusta</button>
</span>
<span class="status-separator">│</span>
<span class="status-group">
<button id="join-btn" class="btn btn-green" onclick="showJoinDialog()" title="Liitä oma koneesi laskentaverkkoon (natiivi, nopea)">+ Liitä koneesi</button>
</span>
</div>
<!-- Join-dialogi -->
<div id="join-dialog" style="display:none;margin-top:8px;padding:16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;font-size:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="color:#e6edf3;font-weight:600;font-size:16px">Liitä koneesi laskentaverkkoon</span>
<button onclick="document.getElementById('join-dialog').style.display='none'" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px">✕</button>
</div>
<p style="color:#8b949e;margin-bottom:16px">Koneesi suorittaa tehtäviä ~10-50x nopeammin kuin selainlaskenta. Kaksi vaihetta:</p>
<!-- Vaihe 1: Ollama -->
<div style="margin-bottom:14px;padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--accent)">
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">1. Asenna Ollama <span style="color:#8b949e;font-weight:normal">(kielimallimoottori)</span></div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -fsSL https://ollama.ai/install.sh | sh</code>
<button onclick="navigator.clipboard.writeText('curl -fsSL https://ollama.ai/install.sh | sh');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-accent" style="padding:6px 10px">Kopioi</button>
</div>
<div style="color:#8b949e;font-size:12px">macOS: <code style="color:var(--accent)">brew install ollama</code> · Windows: <a href="https://ollama.ai/download" target="_blank" style="color:var(--accent)">ollama.ai/download</a> · Jos jo asennettu → siirry vaiheeseen 2.</div>
</div>
<!-- Vaihe 2: Kipinä-node -->
<div style="padding:12px;background:var(--bg);border-radius:4px;border-left:3px solid var(--green)">
<div style="color:#e6edf3;font-weight:600;margin-bottom:6px">2. Käynnistä Kipinä-node</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
<code style="flex:1;background:#010409;padding:8px 12px;border-radius:4px;color:var(--green);font-family:'Courier New',monospace;font-size:13px;user-select:all">curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node</code>
<button onclick="navigator.clipboard.writeText('curl -sSL https://kipina.studio/kipina-node -o kipina-node && chmod +x kipina-node && ./kipina-node');this.textContent='✓';setTimeout(()=>this.textContent='Kopioi',1500)" class="btn btn-green" style="padding:6px 10px">Kopioi</button>
</div>
<div style="color:#8b949e;font-size:12px">Lataa kielimallin (~2GB) automaattisesti ensimmäisellä kerralla. Ctrl+C pysäyttää.</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<!-- 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 "hello world in python"'
spellcheck="false" autocomplete="off">
<div id="term-dropdown" class="terminal-dropdown"></div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
: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;
font-size: 16px;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.container { max-width: 1600px; margin: 0 auto; padding: 20px 40px; }
/* Tabs */
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.tab {
padding: 10px 20px; border-radius: 6px 6px 0 0; cursor: pointer;
border: 1px solid var(--border); border-bottom: none;
background: var(--bg); color: #8b949e; font-size: 15px;
}
.tab.active { background: var(--panel); color: var(--accent); border-color: var(--border); }
/* Panels */
.panel { display: none; }
.panel.active { display: block; }
/* Status bar */
.status-bar {
display: flex; align-items: center; gap: 12px;
padding: 10px 16px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
font-family: 'Courier New', monospace; font-size: 14px;
}
.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 */
.terminal {
background: #010409; border: 1px solid var(--border); border-top: none;
font-family: 'Courier New', monospace; font-size: 16px;
min-height: 400px; max-height: 70vh; overflow-y: auto;
padding: 12px 16px;
}
.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: #0d1117; border: 1px solid var(--accent); border-top: none;
border-radius: 0 0 6px 6px; padding: 10px 14px;
font-family: 'Courier New', monospace; font-size: 15px;
box-shadow: 0 2px 8px rgba(88,166,255,0.1);
}
.terminal-input {
flex: 1; background: transparent; border: none; outline: none;
color: var(--green); font-family: inherit; font-size: 16px;
}
.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 rgba(0,0,0,0.4);
}
.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 progress */
.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 */
.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; }
/* Buttons */
.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 display */
.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; }
/* Agent avatars */
.agent-avatar {
background: linear-gradient(145deg, rgba(33,38,45,0.4) 0%, rgba(13,17,23,0.8) 100%);
backdrop-filter: blur(12px);
border: 1px solid rgba(240,246,252,0.1);
border-radius: 14px;
padding: 8px 8px 6px;
text-align: center;
width: 90px;
opacity: 0.8;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.agent-avatar:hover {
opacity: 0.85;
transform: translateY(-2px) scale(1.02);
border-color: rgba(240,246,252,0.3);
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
}
.agent-avatar img {
width: 64px; height: 64px; border-radius: 14px;
margin-bottom: 4px; border: 2px solid rgba(240,246,252,0.1);
transition: all 0.4s ease; object-fit: cover;
}
.agent-avatar .avatar-name {
font-size: 11px; color: #8b949e; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.agent-avatar.active {
opacity: 1;
transform: translateY(-8px) scale(1.05);
border-color: var(--accent);
background: linear-gradient(145deg, rgba(88,166,255,0.15) 0%, rgba(13,17,23,0.9) 100%);
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88,166,255,0.3);
z-index: 2;
}
.agent-avatar.active img {
border-color: var(--accent);
box-shadow: 0 0 25px rgba(88,166,255,0.8);
}
/* Settings */
.settings-section {
margin-bottom: 24px; padding: 16px; background: var(--panel);
border: 1px solid var(--border); border-radius: 6px;
}
.settings-title { color: #e6edf3; font-size: 15px; margin-bottom: 4px; }
.settings-desc { color: #8b949e; font-size: 13px; margin-bottom: 12px; }
.settings-label { color: var(--text); font-size: 13px; display: block; margin-bottom: 4px; }
.settings-val { color: var(--accent); font-weight: 600; float: right; }
.settings-hint { color: #8b949e; font-size: 11px; margin-top: 2px; }
.settings-textarea {
width: 100%; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
padding: 8px; font-size: 13px; font-family: 'Courier New', monospace;
resize: vertical;
}
.settings-select {
width: 100%; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
padding: 8px; font-size: 13px;
}
.settings-slider {
width: 100%; accent-color: var(--accent);
}
.settings-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
/* Animations */
@keyframes blink { 0%,100% { opacity:1 } 50% { opacity:0 } }
@keyframes spin { to { transform: rotate(360deg) } }

View File

@@ -0,0 +1 @@
{ "extends": "astro/tsconfigs/strict" }

View File

@@ -1,6 +1,6 @@
[package]
name = "hub"
version = "0.2.0"
version = "0.3.1"
edition = "2024"
[dependencies]
@@ -16,3 +16,4 @@ futures = "0.3"
rusqlite = { version = "0.31", features = ["bundled"] }
chrono = "0.4"
base64 = "0.22"
reqwest = { version = "0.12", features = ["json"] }

Binary file not shown.

View File

@@ -26,6 +26,29 @@ impl NodeDb {
INSERT INTO _schema_version VALUES (2);
");
}
if version < 3 {
let _ = conn.execute_batch("
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
avatar TEXT NOT NULL DEFAULT '/avatars/kipina_notext.png',
role TEXT NOT NULL DEFAULT 'coder',
model TEXT NOT NULL DEFAULT 'qwen2.5-coder:7b',
color TEXT NOT NULL DEFAULT '#3fb950',
docs TEXT,
prompt TEXT NOT NULL DEFAULT '',
temperature REAL DEFAULT 0.7,
top_k INTEGER DEFAULT 40,
max_tokens INTEGER DEFAULT 512,
repetition_penalty REAL DEFAULT 1.15,
is_default BOOLEAN DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
DELETE FROM _schema_version;
INSERT INTO _schema_version VALUES (3);
");
}
conn.execute_batch("
CREATE TABLE IF NOT EXISTS node_sessions (
@@ -279,6 +302,82 @@ impl NodeDb {
})
}
// ── Agents CRUD ──
pub fn upsert_agent(&self, agent: &serde_json::Value) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let now = chrono::Utc::now().to_rfc3339();
let id = agent.get("id").and_then(|v| v.as_str()).ok_or("id puuttuu")?;
let name = agent.get("name").and_then(|v| v.as_str()).ok_or("name puuttuu")?;
conn.execute(
"INSERT INTO agents (id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default, created_at, updated_at)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?14)
ON CONFLICT(id) DO UPDATE SET
name=?2, avatar=?3, role=?4, model=?5, color=?6, docs=?7, prompt=?8,
temperature=?9, top_k=?10, max_tokens=?11, repetition_penalty=?12, updated_at=?14",
params![
id, name,
agent.get("avatar").and_then(|v| v.as_str()).unwrap_or("/avatars/kipina_notext.png"),
agent.get("role").and_then(|v| v.as_str()).unwrap_or("coder"),
agent.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5-coder:7b"),
agent.get("color").and_then(|v| v.as_str()).unwrap_or("#3fb950"),
agent.get("docs").and_then(|v| v.as_str()),
agent.get("prompt").and_then(|v| v.as_str()).unwrap_or(""),
agent.get("temperature").and_then(|v| v.as_f64()).unwrap_or(0.7),
agent.get("top_k").and_then(|v| v.as_u64()).unwrap_or(40) as i64,
agent.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as i64,
agent.get("repetition_penalty").and_then(|v| v.as_f64()).unwrap_or(1.15),
agent.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false),
now,
],
).map_err(|e| format!("Agent upsert: {}", e))?;
Ok(())
}
pub fn get_agents(&self) -> Vec<serde_json::Value> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let mut stmt = conn.prepare(
"SELECT id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default,
created_at, updated_at
FROM agents ORDER BY is_default DESC, name"
).unwrap();
stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"name": row.get::<_, String>(1)?,
"avatar": row.get::<_, String>(2)?,
"role": row.get::<_, String>(3)?,
"model": row.get::<_, String>(4)?,
"color": row.get::<_, String>(5)?,
"docs": row.get::<_, Option<String>>(6)?,
"prompt": row.get::<_, String>(7)?,
"temperature": row.get::<_, f64>(8)?,
"top_k": row.get::<_, i64>(9)?,
"max_tokens": row.get::<_, i64>(10)?,
"repetition_penalty": row.get::<_, f64>(11)?,
"is_default": row.get::<_, bool>(12)?,
"created_at": row.get::<_, String>(13)?,
"updated_at": row.get::<_, String>(14)?,
}))
}).unwrap().filter_map(|r| r.ok()).collect()
}
pub fn delete_agent(&self, id: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let deleted = conn.execute(
"DELETE FROM agents WHERE id = ?1 AND is_default = 0",
params![id],
).map_err(|e| format!("Agent delete: {}", e))?;
if deleted == 0 {
Err("Agenttia ei löydy tai se on oletusagentti".to_string())
} else {
Ok(())
}
}
pub fn insert_pair_result(
&self,
node_id: u64,

View File

@@ -34,7 +34,7 @@ struct AppState {
total_tasks: Mutex<u64>,
stats_tx: broadcast::Sender<String>,
node_channels: tokio::sync::RwLock<HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>>, // Kohdennettu reititys
pending_consensus: tokio::sync::RwLock<HashMap<String, Vec<serde_json::Value>>>, // Proof of Compute -konsensus
_pending_consensus: tokio::sync::RwLock<HashMap<String, Vec<serde_json::Value>>>, // Proof of Compute -konsensus
feature_flags: tokio::sync::RwLock<HashMap<String, bool>>, // Tuntee TODO.md:n ruksit lennosta
ip_connections: Mutex<HashMap<IpAddr, u32>>,
node_ips: Mutex<HashMap<u64, IpAddr>>,
@@ -256,7 +256,7 @@ async fn main() {
total_tasks: Mutex::new(0),
stats_tx: stats_tx.clone(),
node_channels: tokio::sync::RwLock::new(HashMap::new()),
pending_consensus: tokio::sync::RwLock::new(HashMap::new()),
_pending_consensus: tokio::sync::RwLock::new(HashMap::new()),
feature_flags: tokio::sync::RwLock::new(HashMap::new()),
ip_connections: Mutex::new(HashMap::new()),
node_ips: Mutex::new(HashMap::new()),
@@ -330,15 +330,6 @@ async fn main() {
let idx = (rng_state as usize) % pairs.len();
let (en, fi) = pairs[idx];
// Tokenisointiparit
let pair_msg = serde_json::json!({
"type": "pair_task",
"en": en,
"fi": fi,
});
let _ = state_for_task.stats_tx.send(pair_msg.to_string());
// LLM-promptit
let llm_prompts = vec![
"Tell me a short joke.",
"What is WebGPU in one sentence?",
@@ -348,33 +339,39 @@ async fn main() {
];
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
// SmolLM-prompt
let smollm_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "smollm-135m",
});
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
let mut sends = Vec::new();
{
let channels = state_for_task.node_channels.read().await;
let tasks = state_for_task.node_tasks.lock().unwrap();
let mut busy = state_for_task.node_busy.lock().unwrap();
// Qwen-prompt (sama prompti, eri malli-tagi)
let qwen_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "qwen-05b",
});
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
for (node_id, task) in tasks.iter() {
if !busy.contains(node_id) {
// Vapaa node -> lähetetään oikea tehtävä
let msg = match task.as_str() {
"tokenize" => Some(serde_json::json!({ "type": "pair_task", "en": en, "fi": fi })),
"smollm-135m" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "smollm-135m" })),
"qwen-05b" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "qwen-05b" })),
"phi3-mini" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "phi3-mini" })),
_ => None, // Coder ja viewer ei saa auto-tehtäviä
};
// Phi-3 prompt
let phi3_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "phi3-mini",
});
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
if let Some(payload) = msg {
if let Some(ch) = channels.get(node_id) {
sends.push((ch.clone(), payload.to_string()));
busy.insert(*node_id);
}
}
}
}
}
// Coder ei saa automaattisia tehtäviä — vain käyttäjän user_text
for (ch, msg_str) in sends {
let _ = ch.send(msg_str);
}
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3");
// tracing::debug!("Tehtävät lähetetty reititetysti idle-nodeille");
}
});
@@ -386,9 +383,12 @@ async fn main() {
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
.route("/api/v1/model", axum::routing::post(api_change_model))
.route("/api/v1/hardware", get(api_hardware))
.route("/api/v1/ollama/tags", get(api_ollama_tags))
.route("/api/v1/agents", get(api_get_agents).post(api_upsert_agent))
.route("/api/v1/agents/:id", axum::routing::delete(api_delete_agent))
.route("/admin", get(admin_page))
.nest_service("/", {
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".to_string());
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
})
.with_state(state);
@@ -461,6 +461,34 @@ fn admin_unauthorized() -> axum::response::Response {
.unwrap()
}
// ── Agents API ──
async fn api_get_agents(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> axum::response::Response {
axum::Json(state.db.get_agents()).into_response()
}
async fn api_upsert_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::Json(payload): axum::Json<serde_json::Value>,
) -> axum::response::Response {
match state.db.upsert_agent(&payload) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn api_delete_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::extract::Path(id): axum::extract::Path<String>,
) -> axum::response::Response {
match state.db.delete_agent(&id) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
if !check_admin_auth(&headers) { return admin_unauthorized(); }
axum::response::Html(ADMIN_HTML).into_response()
@@ -474,7 +502,12 @@ async fn ws_handler(
) -> impl IntoResponse {
// Origin-tarkistus — estää cross-site WebSocket hijackingin
if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
if !ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed) {
let is_allowed = ALLOWED_ORIGINS.iter().any(|&allowed| origin == allowed)
|| origin.starts_with("http://192.168.")
|| origin.starts_with("http://10.")
|| origin.starts_with("http://172."); // LAN-avaruudet
if !is_allowed {
tracing::warn!("Estetty yhteys väärällä originilla: {}", origin);
return (
axum::http::StatusCode::FORBIDDEN,
@@ -490,16 +523,19 @@ async fn ws_handler(
.and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or_else(|| addr.ip());
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
// Max yhteyttä per IP (ei rajoiteta localhost/127.0.0.1)
{
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 10 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 10 yhteyttä per IP",
).into_response();
let is_local = ip.is_loopback();
if !is_local {
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 20 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 20 yhteyttä per IP",
).into_response();
}
}
}
@@ -660,6 +696,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
// API-avain vaaditaan natiivisolmuilta (ei selaimilta)
if node_type == "native" {
let required_key = std::env::var("NODE_API_KEY").unwrap_or_default();
if !required_key.is_empty() {
let provided_key = json.get("api_key").and_then(|v| v.as_str()).unwrap_or("");
if provided_key != required_key {
tracing::warn!("Solmu {} ({}) hylätty: virheellinen API-avain", node_id, ip);
break; // Suljetaan WebSocket
}
}
}
{
let mut map = state.nodes_vram.lock().unwrap();
map.insert(node_id, allocated);
@@ -740,6 +788,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
}
broadcast_stats(&state).await;
} else if msg_type == "pair_done" {
state.node_busy.lock().unwrap().remove(&node_id);
{
let mut json = json; // Siirretään omistajuus muokkausta varten
if let Some(obj) = json.as_object_mut() {
@@ -960,6 +1009,20 @@ struct ChatCompletionResponse {
tokens_generated: u64,
}
async fn api_ollama_tags() -> axum::response::Response {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
match reqwest::get(format!("{}/api/tags", ollama_url)).await {
Ok(resp) => {
if let Ok(body) = resp.json::<serde_json::Value>().await {
axum::Json(body).into_response()
} else {
axum::Json(serde_json::json!({ "models": [] })).into_response()
}
}
Err(_) => axum::Json(serde_json::json!({ "models": [] })).into_response(),
}
}
async fn api_hardware(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> axum::response::Response {
@@ -969,17 +1032,34 @@ async fn api_hardware(
s.get("node_type").and_then(|v| v.as_str()) == Some("native")
});
let (vram_mb, gpu_name, ram_mb) = if let Some(s) = native {
let (mut vram_mb, mut gpu_name, ram_mb) = if let Some(s) = native {
let gpus = s.get("gpus").and_then(|v| v.as_array());
let gpu = gpus.and_then(|g| g.first());
let vram = gpu.and_then(|g| g.get("vram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("?");
let name = gpu.and_then(|g| g.get("name")).and_then(|v| v.as_str()).unwrap_or("").to_string();
let ram = s.get("system").and_then(|v| v.get("ram_total_mb")).and_then(|v| v.as_u64()).unwrap_or(0);
(vram, name.to_string(), ram)
(vram, name, ram)
} else {
(0, "ei natiivisolmua".to_string(), 0)
(0, String::new(), 0)
};
// Fallback: kysytään Ollamalta onko malleja ladattu (= Ollama on käynnissä)
if vram_mb == 0 {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
if let Ok(resp) = reqwest::get(format!("{}/api/tags", ollama_url)).await {
if let Ok(body) = resp.json::<serde_json::Value>().await {
let models = body["models"].as_array().map(|a| a.len()).unwrap_or(0);
if models > 0 {
gpu_name = "Ollama (GPU/CPU)".to_string();
// Natiivisolmun RAM fallbackina
vram_mb = if ram_mb > 0 { ram_mb } else { 0 };
}
}
}
}
if gpu_name.is_empty() { gpu_name = "ei natiivisolmua".to_string(); }
axum::Json(serde_json::json!({
"gpu_name": gpu_name,
"vram_mb": vram_mb,
@@ -1022,93 +1102,50 @@ async fn api_chat_completions(
}
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
let (target_node_free, target_node_any, total_matching) = {
let (target_node, _total_matching) = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
let _busy = state.node_busy.lock().unwrap();
let node_types = state.node_types.lock().unwrap();
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
if payload.model == "qwen-coder" {
task.starts_with("qwen-coder")
// Eksakti match tai qwen-perheen yhteensopivuus (selain: qwen-coder-05b, natiivi: qwen2.5-coder:7b)
let req_model = payload.model.to_lowercase();
let node_task = task.to_lowercase();
if req_model.starts_with("qwen") {
node_task.starts_with("qwen")
} else if req_model.starts_with("phi") {
node_task.starts_with("phi")
} else {
**task == payload.model
}
}).map(|(k, _)| *k).collect();
// Vapaat solmut: natiivi ensin, sitten selain
let free_native = matching.iter().find(|id| {
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
// Etsitään mikä tahansa matchaava solmu (natiivi priorisoidaan)
let native = matching.iter().find(|id| {
node_types.get(id).map(|t| t == "native").unwrap_or(false)
}).copied();
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
let free = free_native.or(free_any);
let any = matching.first().copied();
(free, any, matching.len())
let any = native.or_else(|| matching.first().copied());
(any, matching.len())
};
// Broadcastataan reititystila UI:lle
let task_id = payload.task_id.clone();
if target_node_any.is_none() {
// Ei yhtään solmua tälle mallille
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
let target_node_id = match target_node {
Some(id) => id,
None => {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
};
let target_node_id;
if let Some(free_id) = target_node_free {
// Vapaa solmu löytyi — reititetään suoraan
target_node_id = free_id;
let node_type = if state.node_tasks.lock().unwrap().get(&free_id).map(|t| t.contains("native")).unwrap_or(false) { "natiivi" } else { "selain" };
// Reititystila UI:lle
{
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": free_id,
"node_type": node_type,
"node_id": target_node_id,
"status": "routed",
"message": format!("Reititetty solmulle #{}", free_id),
"message": format!("Reititetty solmulle #{}", target_node_id),
});
let _ = state.stats_tx.send(routing_msg.to_string());
} else {
// Kaikki solmut varattuja — odotetaan vapautumista (max 30s)
let queue_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"status": "queued",
"message": format!("Kaikki {} solmua varattuja — odotetaan vapautumista...", total_matching),
});
let _ = state.stats_tx.send(queue_msg.to_string());
// Pollaa busy-tilaa 500ms välein, max 30s
let mut waited = 0u32;
loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
waited += 500;
let free = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
tasks.iter().find(|(node_id, task)| {
let model_match = if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder"
} else {
**task == payload.model
};
model_match && !busy.contains(node_id)
}).map(|(k, _)| *k)
};
if let Some(id) = free {
target_node_id = id;
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": id,
"status": "routed",
"message": format!("Solmu #{} vapautui — reititetään ({:.1}s jonossa)", id, waited as f64 / 1000.0),
});
let _ = state.stats_tx.send(routing_msg.to_string());
break;
}
if waited >= 30000 {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Aikakatkaisu: kaikki solmut varattuja 30s ajan").into_response();
}
}
};
}
// Merkitään solmu varatuksi ja task_id jaetuksi
state.node_busy.lock().unwrap().insert(target_node_id);

59
network-poc/install.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Kipinä Agentic Studio — asennusskripti (Debian/Ubuntu)
set -e
echo "=== Kipinä Agentic Studio — Asennus ==="
echo ""
# Tarkistetaan käyttöjärjestelmä
if [ ! -f /etc/debian_version ]; then
echo "⚠ Tämä skripti on suunniteltu Debian/Ubuntu-järjestelmille."
echo " Muilla jakeluilla voit asentaa riippuvuudet manuaalisesti."
read -p " Jatketaanko? (k/e) " -n 1 -r; echo
[[ $REPLY =~ ^[Kk]$ ]] || exit 1
fi
echo "[1/6] Päivitetään pakettilistaus..."
sudo apt-get update -qq
echo "[2/6] Asennetaan peruspaketteja..."
sudo apt-get install -y -qq curl git build-essential pkg-config libssl-dev
# Rust
if command -v rustc &>/dev/null; then
echo "[3/6] Rust löytyi: $(rustc --version)"
else
echo "[3/6] Asennetaan Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
fi
# Node.js (Astro-frontend vaatii)
if command -v node &>/dev/null; then
echo "[4/6] Node.js löytyi: $(node --version)"
else
echo "[4/6] Asennetaan Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y -qq nodejs
fi
# Ollama
if command -v ollama &>/dev/null; then
echo "[5/6] Ollama löytyi"
else
echo "[5/6] Asennetaan Ollama..."
curl -fsSL https://ollama.ai/install.sh | sh
fi
# Malli
echo "[6/6] Ladataan kielimalli (qwen2.5-coder:3b)..."
ollama pull qwen2.5-coder:3b
echo ""
echo "=== Asennus valmis! ==="
echo ""
echo "Käynnistä:"
echo " cd $(pwd)"
echo " ./network-poc/local.sh"
echo ""
echo "Avaa selaimessa: http://localhost:3000"

37
network-poc/local.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Local Development ==="
# Frontend
echo "[1/3] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
[ -d node_modules ] || npm install --silent
npm run build --silent 2>&1 | tail -1
# Hub
echo "[2/3] Käynnistetään hub..."
cd "$SCRIPT_DIR/hub"
cargo run &
HUB_PID=$!
sleep 3
# Native-node (jos Ollama on käynnissä)
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
echo "[3/3] Ollama löytyi — käynnistetään native-node..."
cd "$SCRIPT_DIR/native-node"
HUB_URL=ws://localhost:3000/ws cargo run --no-default-features &
NODE_PID=$!
echo " Native-node PID: $NODE_PID"
else
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
fi
echo ""
echo "=== http://localhost:3000 ==="
echo " Ctrl+C pysäyttää"
# Odotetaan hub-prosessia
wait $HUB_PID

View File

@@ -1,8 +1,12 @@
[package]
name = "native-node"
version = "0.2.0"
version = "0.2.2"
edition = "2024"
[features]
default = ["gpu-detect"]
gpu-detect = ["nvml-wrapper", "wgpu"]
[dependencies]
tokio = { version = "1.36", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
@@ -10,8 +14,8 @@ futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.30"
nvml-wrapper = "0.10"
wgpu = "24"
nvml-wrapper = { version = "0.10", optional = true }
wgpu = { version = "24", optional = true }
reqwest = { version = "0.12", features = ["json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -8,17 +8,47 @@ pub struct LlmEngine {
}
impl LlmEngine {
pub fn load() -> Result<Self, String> {
let ollama_url = std::env::var("OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
pub async fn load() -> Result<Self, String> {
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:3b".to_string());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(3))
.build()
.map_err(|e| format!("HTTP client: {}", e))?;
// Jos OLLAMA_URL on asetettu, käytetään sitä suoraan
let ollama_url = if let Ok(url) = std::env::var("OLLAMA_URL") {
tracing::info!("Ollama backend (env): {}", url);
url
} else {
// Haistellaan Ollamaa tunnetuista osoitteista
let candidates = [
"http://localhost:11434",
"http://127.0.0.1:11434",
"http://ollama:11434",
"http://host.docker.internal:11434",
];
let mut found = None;
for url in &candidates {
let probe = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(2))
.build().unwrap_or(client.clone());
if let Ok(resp) = probe.get(format!("{}/api/version", url)).send().await {
if resp.status().is_success() {
tracing::info!("Ollama löytyi osoitteesta: {}", url);
found = Some(url.to_string());
break;
}
}
}
found.unwrap_or_else(|| {
tracing::warn!("Ollamaa ei löytynyt — käytetään oletusta http://localhost:11434");
"http://localhost:11434".to_string()
})
};
tracing::info!("Ollama backend: {} | malli: {}", ollama_url, model);
Ok(LlmEngine { ollama_url, model: RefCell::new(model), client })
}
@@ -49,7 +79,8 @@ impl LlmEngine {
}
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
let system = "You are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.";
// System prompt tulee agentin konfiguraatiosta (frontend lähettää sen osana promptia).
// Tässä ei yliajeta sitä — Ollama saa vain prompt-kentän.
let model = self.model.borrow().clone();
let start = Instant::now();
@@ -57,14 +88,13 @@ impl LlmEngine {
.json(&serde_json::json!({
"model": model,
"prompt": prompt,
"system": system,
"stream": false,
"options": {
"num_predict": max_tokens,
"temperature": 0.7,
"top_k": 40,
"repeat_penalty": 1.15,
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:", "\nPlease note", "\nThis is", "\n```\n\n", "\n// Example", "\n# Example"]
}
}))
.send()
@@ -79,7 +109,7 @@ impl LlmEngine {
.map_err(|e| format!("Ollama JSON: {}", e))?;
let text = body["response"].as_str().unwrap_or("").to_string();
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
@@ -97,27 +127,40 @@ impl LlmEngine {
}
}
/// Siivoa mahdolliset markdown-koodiblokki-merkit
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
fn strip_code_fences(text: &str) -> String {
let mut result = text.trim().to_string();
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
let lines: Vec<&str> = text.lines().collect();
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
let trimmed = line.trim();
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
if trimmed.starts_with("```") {
return false;
}
true
}).collect();
let mut result = filtered.join("\n").trim().to_string();
// Poista aloittava ```lang
if result.starts_with("```") {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
let lower = result.to_lowercase();
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
if let Some(pos) = lower.find(stop) {
result = result[..pos].trim_end().to_string();
}
}
// Poista sulkeva ```
let trimmed = result.trim_end();
if trimmed.ends_with("```") {
let before = &trimmed[..trimmed.len() - 3];
if before.is_empty() || before.ends_with('\n') {
result = before.trim_end().to_string();
// Poista johdantolauseet alusta
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
break;
}
}
result
result.trim().to_string()
}
pub struct GenerateResult {

View File

@@ -33,6 +33,7 @@ impl GpuInfo {
}
}
#[cfg(feature = "gpu-detect")]
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
gpus
}
#[cfg(feature = "gpu-detect")]
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
@@ -150,10 +153,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(feature = "gpu-detect")]
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
let hwmon_dir = device_path.join("hwmon");
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
None
}
#[cfg(feature = "gpu-detect")]
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
// Arvioidaan system RAM:sta
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
fn collect_all_gpus() -> Vec<GpuInfo> {
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
gpus
#[cfg(feature = "gpu-detect")]
{
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
return gpus;
}
#[cfg(not(feature = "gpu-detect"))]
{
Vec::new()
}
}
/// Kerää järjestelmätiedot (CPU, RAM, OS)
@@ -222,15 +232,21 @@ fn build_auth_message(allocated_gb: u32) -> String {
v
}).collect();
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
let mut msg = json!({
"type": "auth",
"status": "agent_ready",
"node_type": "native",
"allocated_gb": allocated_gb,
"selected_task": "qwen-coder-05b",
"selected_task": "qwen2.5-coder:7b",
"system": sys,
});
if !api_key.is_empty() {
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
}
if !gpu_json.is_empty() {
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
}
@@ -269,6 +285,9 @@ async fn main() {
let gpus = collect_all_gpus();
if gpus.is_empty() {
#[cfg(not(feature = "gpu-detect"))]
tracing::info!("GPU-tunnistus ei käytössä (--no-default-features). Ollama käyttää GPU:ta automaattisesti jos saatavilla.");
#[cfg(feature = "gpu-detect")]
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
} else {
for (i, gpu) in gpus.iter().enumerate() {
@@ -287,7 +306,7 @@ async fn main() {
// Ollama-backend
tracing::info!("Alustetaan Ollama-yhteyttä...");
let llm = match inference::LlmEngine::load() {
let llm = match inference::LlmEngine::load().await {
Ok(engine) => {
// Varmistetaan malli (ollama pull) — odotetaan kunnes valmis
match engine.ensure_model().await {
@@ -315,38 +334,39 @@ async fn main() {
continue;
}
let mut busy = false;
while let Some(Ok(msg)) = read.next().await {
if let Message::Text(text) = msg {
// LLM-promptit
if text.contains("llm_prompt") && !busy {
if text.contains("llm_prompt") {
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder")) {
if let Some(ref engine) = llm {
busy = true;
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize;
let prompt_lines = prompt.lines().count();
let prompt_last: String = prompt.lines().last().unwrap_or("").chars().take(60).collect();
tracing::info!("→ task_id:{} | {}r prompti | \"{}...\"", task_id, prompt_lines, prompt_last);
let model_name = engine.model_name();
match engine.generate(prompt, max_tokens).await {
Ok(result) => {
tracing::info!(
"Tulos: {} tokenia | {:.0}ms | {:.1} tok/s | \"{}\"",
"✓ {} | {} tok | {:.0}ms | {:.1} tok/s",
model_name,
result.tokens_generated,
result.duration_ms,
result.tokens_per_sec,
&result.text[..result.text.len().min(80)]
);
// Lähetetään vain lyhyt prompti-esikatselu (ei koko kontekstia)
let prompt_short: String = prompt.lines().last().unwrap_or("").chars().take(100).collect();
let done = json!({
"type": "llm_done",
"prompt": prompt,
"prompt": prompt_short,
"model": format!("{} (Ollama)", model_name),
"response": result.text,
"tokens_generated": result.tokens_generated,
@@ -361,7 +381,6 @@ async fn main() {
tracing::error!("Inferenssivirhe: {}", e);
}
}
busy = false;
}
}
}

View File

@@ -118,6 +118,27 @@ async fn run_ai_tensor_inference(difficulty: usize) -> String {
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
}
/// JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
/// Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
#[wasm_bindgen]
pub async fn tokenize_js(text: String) -> Result<String, JsValue> {
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
let Some(bytes) = cached_tok else {
// Yritetään ladata verkosta
let resp = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
let bytes = resp.bytes().await
.map_err(|e| JsValue::from_str(&format!("Tokenizer-lataus epäonnistui: {}", e)))?;
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
return Ok(tokenize_text(&tokenizer, &text).to_string());
};
let tokenizer = tokenizers::Tokenizer::from_bytes(&bytes)
.map_err(|e| JsValue::from_str(&format!("Tokenizer-parsinta: {}", e)))?;
Ok(tokenize_text(&tokenizer, &text).to_string())
}
/// Tokenisoi yhden tekstin ja palauttaa metriikat
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
let char_count = text.chars().count();
@@ -327,7 +348,9 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
});
}
}
} else if msg.contains("llm_prompt") && (current_task == 4 || current_task == 5) {
} else if msg.contains("llm_prompt") {
console_log!("[DEBUG] llm_prompt vastaanotettu! current_task={}, busy={}", current_task, LLM_BUSY.load(Ordering::SeqCst));
if current_task == 4 || current_task == 5 {
// Qwen2.5-Coder: 4 = 0.5B, 5 = 3B
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
@@ -355,6 +378,7 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
}
}
}
} // current_task == 4 || 5
} else if msg.contains("ai_task") {
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
let ws_for_async = ws_clone.clone();

View File

@@ -248,14 +248,17 @@ async fn get_or_build_model(use_3b: bool, ws: &Rc<RefCell<WebSocket>>) -> Result
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
console_log!("[Coder] run_coder_inference alkaa! prompt={}", &prompt[..prompt.len().min(50)]);
let size_label = if use_3b { "3B" } else { "0.5B" };
let start_load = crate::perf_now();
console_log!("[Coder] Kutsutaan get_or_build_model...");
if let Err(e) = get_or_build_model(use_3b, &ws).await {
console_log!("[Coder] Mallin lataus: {}", e);
console_log!("[Coder] Mallin lataus epäonnistui: {}", e);
return;
}
console_log!("[Coder] Malli valmis, aloitetaan inferenssi");
let load_time = crate::perf_now() - start_load;
if load_time > 100.0 {
@@ -320,7 +323,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
generated_text.push_str(&text);
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -362,7 +369,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
}
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -391,7 +402,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
"load_time_ms": (load_time * 100.0).round() / 100.0,
});
if let Some(tid) = task_id {
done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid));
if let Some(obj) = done.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&done.to_string());
}

View File

@@ -0,0 +1 @@
{"rustc_fingerprint":15841952146704291179,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/jaakko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}

View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@@ -0,0 +1,5 @@
// @ts-check
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "frontend",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^6.1.5",
"three": "^0.183.2"
}
}

View File

@@ -0,0 +1,34 @@
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
## 1. Luo Animoidut GIF-tiedostot
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
Esimerkkejä:
- Koodari `kipina_notext.png``kipina_notext_puhuva.gif`
- Manageri `karhunpentu.png``karhunpentu_puhuva.gif`
- Asiakas `kettu_notext.png``kettu_notext_puhuva.gif`
## 3. Aktivoi Koodi
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
```javascript
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
window.USE_ANIMATED_GIFS = false;
```
Muuta tuo `false` arvoon `true`:
```javascript
window.USE_ANIMATED_GIFS = true;
```
**Mitä logiikka tekee?**
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.

View File

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 432 KiB

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 389 KiB

View File

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 596 KiB

View File

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Some files were not shown because too many files have changed in this diff Show More