Frontend uudelleenrakennettu: Astro-komponentit, Wasm pääsäikeessä, ei Workeria
Vanha frontend siirretty temp/. Uusi rakenne: - StatusBar.astro, Terminal.astro, Editor.astro, Guide.astro - global.css erillinen - Wasm pääsäikeessä (ei Worker — yksinkertainen, debugattava) - Tab-completion, dropdown, projektikortti, Monaco, GUIDE.md - Ei tokenisointia eikä koodilaboratoriota Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
413
network-poc/frontend/dist/GUIDE.md
vendored
Normal file
413
network-poc/frontend/dist/GUIDE.md
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
# Kipinä Agentic Studio — Opas
|
||||
|
||||
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
|
||||
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
|
||||
tuloksia voi parantaa.
|
||||
|
||||
---
|
||||
|
||||
## Kielimallit ja niiden koot
|
||||
|
||||
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
|
||||
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
|
||||
|
||||
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|
||||
|-------|-----------|-------------|-------------------|-------------|
|
||||
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
|
||||
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
|
||||
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
|
||||
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
|
||||
|
||||
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
|
||||
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
|
||||
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
|
||||
ja oppikirjan välillä.
|
||||
|
||||
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
|
||||
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
|
||||
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
|
||||
|
||||
---
|
||||
|
||||
## Tokenit — kielimallin "sanat"
|
||||
|
||||
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
|
||||
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
|
||||
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
|
||||
merkkijonot harjoitusdatasta.
|
||||
|
||||
### Esimerkki: suomi vs. englanti
|
||||
|
||||
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
|
||||
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
|
||||
tokeneita saman merkityksen välittämiseen:
|
||||
|
||||

|
||||
|
||||
**Huomaa miten:**
|
||||
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
|
||||
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
|
||||
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
|
||||
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
|
||||
|
||||
### Miksi tämä merkitsee?
|
||||
|
||||
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
|
||||
|
||||
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
|
||||
→ 50% pidempi odotusaika
|
||||
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
|
||||
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
|
||||
tunnista → hallusinaatiot lisääntyvät
|
||||
|
||||
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
|
||||
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
|
||||
|
||||
**Token-budjetti tässä järjestelmässä:**
|
||||
|
||||
| Osa | Tokeneita | Osuus |
|
||||
|-----|-----------|-------|
|
||||
| System prompt | ~30 | kiinteä |
|
||||
| Agent prompt | ~25 | kiinteä |
|
||||
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
|
||||
| Käyttäjän prompti | ~20-50 | vaihtelee |
|
||||
| **Syöte yhteensä** | **~75-400** | |
|
||||
| Generoitu vastaus (max) | 512 | raja |
|
||||
| **Yhteensä** | **~600-900** | /32 768 |
|
||||
|
||||
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
|
||||
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
|
||||
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
|
||||
|
||||
---
|
||||
|
||||
## Promptit — miten mallia ohjataan
|
||||
|
||||
### Kolmitasoinen prompttirakenne
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
|
||||
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
|
||||
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
|
||||
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
|
||||
S --> A --> U --> P
|
||||
P -->|malli jatkaa| R["Generoitu koodi"]
|
||||
|
||||
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
|
||||
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
|
||||
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
|
||||
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
|
||||
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
|
||||
```
|
||||
|
||||
### Miksi promptit ovat englanniksi?
|
||||
|
||||
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
|
||||
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
|
||||
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
|
||||
suomeksi, mutta tekniset ohjeet mallille englanniksi.
|
||||
|
||||
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
|
||||
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
|
||||
kuin tekninen ohje.
|
||||
|
||||
---
|
||||
|
||||
## Prefill-tekniikka
|
||||
|
||||
Normaalisti malli päättää vapaasti miten vastaa:
|
||||
|
||||
```
|
||||
Ilman prefilliä:
|
||||
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
|
||||
→ 25 tokenia, joista 15 turhia
|
||||
|
||||
Prefillin kanssa:
|
||||
Me syötämme: ```
|
||||
Malli jatkaa: python\nprint('Hello')\n```
|
||||
→ 5 tokenia, kaikki hyödyllisiä
|
||||
```
|
||||
|
||||
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
|
||||
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
|
||||
|
||||
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
|
||||
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
|
||||
|
||||
---
|
||||
|
||||
## Sampling — miten malli valitsee seuraavan tokenin
|
||||
|
||||
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
|
||||
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
|
||||
ohjataan kolmella parametrilla:
|
||||
|
||||
### Temperature (0.7)
|
||||
|
||||
Kontrolloi "luovuutta" vs. "varmuutta":
|
||||
|
||||
```
|
||||
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
|
||||
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
|
||||
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
|
||||
```
|
||||
|
||||
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
|
||||
mutta tarpeeksi vaihtelevaa välttämään toistoa.
|
||||
|
||||
### Top-k (40)
|
||||
|
||||
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
|
||||
täysin absurdeja vaihtoehtoja:
|
||||
|
||||
```
|
||||
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
|
||||
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
|
||||
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
|
||||
```
|
||||
|
||||
### Repetition penalty (1.15)
|
||||
|
||||
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
|
||||
juuttumasta luuppiin:
|
||||
|
||||
```
|
||||
Ilman rangaistusta: "print print print print print..."
|
||||
Penalty 1.15: "print('Hello')\nprint('World')"
|
||||
```
|
||||
|
||||
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
|
||||
saman avainsanan (esim. `return`) esiintymisen useasti.
|
||||
|
||||
---
|
||||
|
||||
## Stop-sekvenssit — milloin generointi loppuu
|
||||
|
||||
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
|
||||
|
||||
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
|
||||
2. **Max tokens** (512): Kovakoodattu raja
|
||||
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
|
||||
|
||||
```
|
||||
fn fibonacci(n: usize) -> usize {
|
||||
if n <= 1 { return n; }
|
||||
fibonacci(n-1) + fibonacci(n-2)
|
||||
}
|
||||
← Tähän asti koodia, ok
|
||||
// Example usage: ← Stop! Tämä ei ole enää vastausta
|
||||
let result = fibonacci(10); ← Ei generoida
|
||||
```
|
||||
|
||||
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
|
||||
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
|
||||
stop-kohtaan.
|
||||
|
||||
---
|
||||
|
||||
## Projekti-pipeline — miten agenttitiimi toimii
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
|
||||
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
|
||||
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
|
||||
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
|
||||
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
|
||||
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
|
||||
T1 -->|LGTM| Done["✅ Projekti valmis"]
|
||||
C4["🟡 Koodari: Korjaukset"] --> T2
|
||||
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
|
||||
```
|
||||
|
||||
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
|
||||
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
|
||||
mitä luokkia importata.
|
||||
|
||||
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
|
||||
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
|
||||
|
||||
---
|
||||
|
||||
## Laadun parantaminen
|
||||
|
||||
### 1. Isompi malli (suurin vaikutus)
|
||||
|
||||
| | 0.5B | 3B | Pilvi-API |
|
||||
|---|---|---|---|
|
||||
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
|
||||
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
|
||||
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
|
||||
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
|
||||
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
|
||||
|
||||
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
|
||||
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
|
||||
|
||||
### 2. Paremmat promptit (ilmaista)
|
||||
|
||||
**Huono:** `"tee fibonacci"`
|
||||
- Malli ei tiedä kieltä, formaattia tai kontekstia
|
||||
|
||||
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
|
||||
- Kieli, palautustyyppi ja rakenne määritelty
|
||||
|
||||
**Promptin säännöt:**
|
||||
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
|
||||
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
|
||||
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
|
||||
- Positiivinen ("Write X" ei "Don't write Y")
|
||||
|
||||
### 3. Kontekstin hallinta (pipeline-taso)
|
||||
|
||||
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
|
||||
|
||||
**Ratkaisu:** Pienet, kohdennetut promptit:
|
||||
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
|
||||
- Vain relevantit aiemmat tiedostot kontekstina
|
||||
- Max 4 tiedostoa per projekti
|
||||
|
||||
### 4. Iterointi (review-luuppi)
|
||||
|
||||
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
|
||||
Pipeline-arkkitehtuuri mahdollistaa:
|
||||
|
||||
1. **Generointi** — ensimmäinen versio
|
||||
2. **Review** — testaaja löytää ongelmat
|
||||
3. **Korjaus** — koodari saa palautteen ja korjaa
|
||||
4. **Uusi review** — tarkistetaan korjaukset
|
||||
|
||||
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
|
||||
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
|
||||
|
||||
### 5. Erikoistetut system promptit
|
||||
|
||||
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
|
||||
parantavat laatua merkittävästi:
|
||||
|
||||
```
|
||||
Oletus: "Olet kokenut ohjelmistokehittäjä."
|
||||
|
||||
Parempi: "You are a Python backend developer specializing in FastAPI.
|
||||
Always use Pydantic models for request/response schemas.
|
||||
Always use dependency injection for database sessions.
|
||||
Follow the repository pattern."
|
||||
```
|
||||
|
||||
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
|
||||
|
||||
### 6. Few-shot esimerkit
|
||||
|
||||
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
|
||||
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
|
||||
|
||||
```
|
||||
Write a GET endpoint like this example:
|
||||
|
||||
@app.get("/items")
|
||||
def list_items():
|
||||
db = SessionLocal()
|
||||
return db.query(Item).all()
|
||||
|
||||
Now write a similar endpoint for /users.
|
||||
```
|
||||
|
||||
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
|
||||
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
|
||||
|
||||
### 7. Temperature-säätö tehtävän mukaan
|
||||
|
||||
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
|
||||
|
||||
| Tehtävä | Paras temperature | Miksi |
|
||||
|---------|-------------------|-------|
|
||||
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
|
||||
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
|
||||
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
|
||||
|
||||
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
|
||||
|
||||
### 8. Ensemble — sama prompti usealle mallille
|
||||
|
||||
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
|
||||
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
|
||||
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
|
||||
|
||||
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
|
||||
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
|
||||
koska sampling on stokastinen.
|
||||
|
||||
### 9. Post-processing (nykyinen)
|
||||
|
||||
Mallin raakavastaus siivotaan:
|
||||
1. Kielitunniste poistetaan (`python`, `rust`, ...)
|
||||
2. Sulkeva ` ``` ` poistetaan
|
||||
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
|
||||
4. Selityskommentit poistetaan ("# This is a simple...")
|
||||
5. Stop-sekvenssit katkaisevat generoinnin
|
||||
|
||||
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
|
||||
|
||||
### 10. Mallin hienosäätö (fine-tuning)
|
||||
|
||||
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
|
||||
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
|
||||
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
|
||||
|
||||
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
|
||||
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
|
||||
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
|
||||
|
||||
---
|
||||
|
||||
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
|
||||
|
||||
```
|
||||
Ensimmäinen lataus (hidas):
|
||||
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
|
||||
~990 MB lataus, ~30-60s
|
||||
|
||||
Toinen lataus samalla sivulatauksella (nopea):
|
||||
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
|
||||
~0ms
|
||||
|
||||
Refresh jälkeen (keskitaso):
|
||||
IndexedDB → RAM → Mallin rakennus
|
||||
~0 MB lataus, ~2-5s rakennus
|
||||
|
||||
Uusi selain/laite (hidas):
|
||||
Verkko → IndexedDB → RAM → Mallin rakennus
|
||||
Kuten ensimmäinen lataus
|
||||
```
|
||||
|
||||
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
|
||||
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
|
||||
välillä jotta edellinen vastaus ei vuoda seuraavaan.
|
||||
|
||||
---
|
||||
|
||||
## Lukuja käytännöstä
|
||||
|
||||
**Yksittäinen funktio** (esim. fibonacci):
|
||||
- Input: ~80 tokenia
|
||||
- Output: ~50-100 tokenia
|
||||
- Aika: ~10-20s (0.5B, selain)
|
||||
- Laatu: Yleensä toimiva, joskus loogisia virheitä
|
||||
|
||||
**3 tiedoston projekti** (esim. FastAPI CRUD):
|
||||
- Manageri: ~30 tok out
|
||||
- Koodari (3x): ~100-150 tok out per tiedosto
|
||||
- Testeri: ~50 tok out
|
||||
- Korjaukset: ~100 tok out (jos tarpeen)
|
||||
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
|
||||
- Laatu: Rakenne oikein, yksittäisiä bugeja
|
||||
|
||||
**Token-kustannus vs. pilvipalvelu:**
|
||||
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
|
||||
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
|
||||
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
|
||||
|
||||
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
|
||||
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
|
||||
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.
|
||||
1
network-poc/frontend/dist/_astro/index.astro_astro_type_script_index_0_lang.B57BFWqC.js
vendored
Normal file
1
network-poc/frontend/dist/_astro/index.astro_astro_type_script_index_0_lang.B57BFWqC.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js";
|
||||
519
network-poc/frontend/dist/index.html
vendored
Normal file
519
network-poc/frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,519 @@
|
||||
<!DOCTYPE html><html lang="fi"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Kipinä Agentic Playground</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css"><script type="module" src="/_astro/index.astro_astro_type_script_index_0_lang.B57BFWqC.js"></script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css"><style>:root{--bg: #0d1117;--panel: #161b22;--text: #c9d1d9;--accent: #58a6ff;--green: #3fb950;--yellow: #d29922;--red: #f85149;--purple: #a371f7;--border: #30363d}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}.container{max-width:1200px;margin:0 auto;padding:20px}.tabs{display:flex;gap:4px;margin-bottom:16px}.tab{padding:8px 16px;border-radius:6px 6px 0 0;cursor:pointer;border:1px solid var(--border);border-bottom:none;background:var(--bg);color:#8b949e;font-size:14px}.tab.active{background:var(--panel);color:var(--accent);border-color:var(--border)}.panel{display:none}.panel.active{display:block}.status-bar{display:flex;align-items:center;gap:12px;padding:8px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px 6px 0 0;font-family:Courier New,monospace;font-size:13px}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.status-group{display:flex;align-items:center;gap:6px}.status-separator{color:var(--border)}.terminal{background:#010409;border:1px solid var(--border);border-top:none;font-family:Courier New,monospace;font-size:14px;min-height:300px;max-height:60vh;overflow-y:auto;padding:8px 12px}.terminal-line{padding:1px 0;white-space:pre-wrap;word-break:break-word}.terminal-prompt{color:var(--yellow);margin-right:8px}.terminal-input-row{display:flex;align-items:center;position:relative;background:#010409;border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:Courier New,monospace;font-size:14px}.terminal-input{flex:1;background:transparent;border:none;outline:none;color:var(--green);font-family:inherit;font-size:inherit}.terminal-dropdown{display:none;position:absolute;bottom:100%;left:30px;background:var(--panel);border:1px solid var(--border);border-radius:6px;max-height:200px;overflow-y:auto;font-size:13px;min-width:200px;z-index:100;box-shadow:0 4px 12px #0006}.dd-item{padding:6px 12px;cursor:pointer;color:var(--text);white-space:nowrap;border-bottom:1px solid #21262d}.dd-item:hover,.dd-item.active{background:var(--border);color:var(--accent)}.pipeline-bar{display:none;padding:8px 14px;background:var(--bg);border:1px solid var(--border);border-top:none;font-family:Courier New,monospace;font-size:12px;overflow-x:auto;white-space:nowrap}.project-card{margin:8px 0;border:1px solid var(--border);border-radius:6px;background:var(--panel);overflow:hidden}.project-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg);border-bottom:1px solid var(--border)}.project-tabs{display:flex;gap:2px;padding:6px 8px 0;background:var(--bg)}.project-tab{padding:4px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:12px;color:#8b949e}.project-tab.active{background:var(--panel);color:var(--accent);border:1px solid var(--border);border-bottom:none}.btn{padding:2px 10px;border-radius:4px;border:1px solid var(--border);background:var(--panel);font-size:12px;font-family:inherit;cursor:pointer}.btn-accent{color:var(--accent)}.btn-green{color:var(--green);border-color:var(--green)}.btn-red{color:var(--red);border-color:var(--red)}.btn-muted{color:#8b949e;background:none}.code-block{font-family:Courier New,monospace;background:#010409;border:1px solid var(--border);border-radius:6px;padding:14px;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-x:auto;max-height:400px;overflow-y:auto}.code-block .hljs{background:transparent;padding:0}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}@keyframes spin{to{transform:rotate(360deg)}}
|
||||
</style></head> <body> <div class="container"> <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px"> <div> <h1 style="margin-bottom:0"><span style="color:#ff6b00">Kipinä</span> Agentic Playground</h1> <p style="color:#8b949e;margin:0">AI-ohjelmistokehitystiimi · <span id="hub-version">-</span></p> </div> </div> <!-- Välilehdet --> <div class="tabs"> <div class="tab active" onclick="switchTab('agents')">Agentit</div> <div class="tab" onclick="switchTab('editor')">Editor</div> <div class="tab" onclick="switchTab('guide')">Opas</div> </div> <!-- Agents-paneeli --> <div id="panel-agents" class="panel active"> <!-- Hub-yhteys + laskentasolmun tila --><div class="status-bar"> <span class="status-group" title="Hub-yhteyden tila"> <span id="hub-dot" class="status-dot" style="background:#d29922"></span> <span style="color:#8b949e">Hub:</span> <span id="hub-label" style="color:#d29922">Yhdistetään...</span> </span> <span class="status-separator">│</span> <span class="status-group"> <span id="compute-dot" class="status-dot" style="background:#30363d"></span> <span style="color:#8b949e">Laskenta:</span> <span id="compute-label" style="color:#8b949e">—</span> <button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli">Alusta</button> </span> </div> <!-- Pipeline-palkki + Terminaali + Input --><div id="pipeline-bar" class="pipeline-bar"></div> <div id="terminal" class="terminal"></div> <div class="terminal-input-row"> <span class="terminal-prompt">$</span> <input id="term-input" class="terminal-input" type="text" placeholder="kpn run coder "hello world in python"" spellcheck="false" autocomplete="off"> <div id="term-dropdown" class="terminal-dropdown"></div> </div> </div> <!-- Monaco Editor paneeli --><div id="panel-editor" class="panel"> <div style="display:flex;height:calc(100vh - 200px);gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden"> <div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow-y:auto;font-family:'Courier New',monospace;font-size:13px"> <div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</div> <div id="editor-file-list" style="padding:4px 0"> <div style="padding:8px 16px;color:#8b949e;font-size:12px">Generoi projekti:<br><code style="color:var(--accent)">kpn project "..."</code></div> </div> </div> <div style="flex:1;display:flex;flex-direction:column"> <div id="editor-tabs" style="display:flex;background:var(--bg);border-bottom:1px solid var(--border);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div> <div id="monaco-container" style="flex:1"></div> </div> </div> </div> <!-- Opas-paneeli: ladataan GUIDE.md fetchillä --><div id="panel-guide" class="panel"> <div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;line-height:1.7;font-size:15px"> <p style="color:#8b949e">Ladataan opasta...</p> </div> </div> </div> <script>
|
||||
// === Helpers ===
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function highlightCode(code) {
|
||||
if (typeof hljs !== 'undefined') {
|
||||
try { return hljs.highlightAuto(code).value; } catch(e) {}
|
||||
}
|
||||
return esc(code);
|
||||
}
|
||||
|
||||
// === Tab switching ===
|
||||
window.switchTab = function(tab) {
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('panel-' + tab)?.classList.add('active');
|
||||
document.querySelector(`.tab[onclick*="${tab}"]`)?.classList.add('active');
|
||||
window.location.hash = tab;
|
||||
if (tab === 'editor') initMonaco();
|
||||
};
|
||||
const initHash = window.location.hash.replace('#','');
|
||||
if (['editor','guide'].includes(initHash)) switchTab(initHash);
|
||||
|
||||
// === WebSocket ===
|
||||
const wsUrl = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`;
|
||||
const uiSocket = new WebSocket(wsUrl);
|
||||
window._uiSocket = uiSocket;
|
||||
|
||||
uiSocket.onopen = () => {
|
||||
document.getElementById('hub-dot').style.background = '#3fb950';
|
||||
document.getElementById('hub-label').textContent = 'Yhdistetty';
|
||||
document.getElementById('hub-label').style.color = '#3fb950';
|
||||
// Rekisteröidy viewerina
|
||||
uiSocket.send(JSON.stringify({
|
||||
type: 'auth', status: 'viewer', node_type: 'browser',
|
||||
platform: navigator.platform || '', cpu_cores: navigator.hardwareConcurrency || 0,
|
||||
device_memory_gb: navigator.deviceMemory || 0, allocated_gb: 0, selected_task: 'viewer',
|
||||
}));
|
||||
};
|
||||
uiSocket.onclose = () => {
|
||||
document.getElementById('hub-dot').style.background = '#f85149';
|
||||
document.getElementById('hub-label').textContent = 'Yhteys katkennut';
|
||||
document.getElementById('hub-label').style.color = '#f85149';
|
||||
};
|
||||
|
||||
// === Terminal ===
|
||||
const termPanel = document.getElementById('terminal');
|
||||
const termInput = document.getElementById('term-input');
|
||||
const termHistory = [];
|
||||
let termHistIdx = -1;
|
||||
|
||||
function termLog(html, color) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'terminal-line';
|
||||
if (color) div.style.color = color;
|
||||
div.innerHTML = html;
|
||||
termPanel.appendChild(div);
|
||||
while (termPanel.children.length > 100 && !termPanel.firstChild.querySelector('.stream-content')) termPanel.removeChild(termPanel.firstChild);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
|
||||
// === Wasm inference (main thread) ===
|
||||
let wasmReady = false;
|
||||
let wasmNodeStarted = false;
|
||||
let llmReady = false;
|
||||
|
||||
async function ensureWasm() {
|
||||
if (wasmReady) return;
|
||||
const { default: init, start_agent_node, set_gpu_load } = await import('/pkg/node.js');
|
||||
window._wasmExports = { init, start_agent_node, set_gpu_load };
|
||||
termLog(' Ladataan WebAssembly...', '#d29922');
|
||||
await init();
|
||||
wasmReady = true;
|
||||
termLog(' <span style="color:#3fb950">✓</span> WebAssembly valmis');
|
||||
}
|
||||
|
||||
async function ensureNode() {
|
||||
if (wasmNodeStarted) return;
|
||||
await ensureWasm();
|
||||
const { start_agent_node } = window._wasmExports;
|
||||
const deviceInfo = JSON.stringify({
|
||||
allocated_gb: 4,
|
||||
cpu_cores: navigator.hardwareConcurrency || 0,
|
||||
device_memory_gb: navigator.deviceMemory || 0,
|
||||
platform: navigator.platform || '',
|
||||
gpu: null,
|
||||
selected_task: 'qwen-coder-05b'
|
||||
});
|
||||
termLog(' Yhdistetään laskentasolmuna...', '#d29922');
|
||||
await start_agent_node(wsUrl, false, deviceInfo, 4);
|
||||
wasmNodeStarted = true;
|
||||
|
||||
// Odotetaan WS-yhteyden avautumista (kuunnellaan console.log)
|
||||
await new Promise(resolve => {
|
||||
const origLog = console.log;
|
||||
const check = (...args) => {
|
||||
const msg = args.join(' ');
|
||||
if (msg.includes('Yhteys Hubiin avattu')) {
|
||||
console.log = origLog;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
console.log = function(...args) { origLog.apply(console, args); check(...args); };
|
||||
// Timeout 15s
|
||||
setTimeout(() => { console.log = origLog; resolve(); }, 15000);
|
||||
});
|
||||
|
||||
document.getElementById('compute-dot').style.background = '#d29922';
|
||||
document.getElementById('compute-label').textContent = 'Yhdistetty';
|
||||
document.getElementById('compute-label').style.color = '#d29922';
|
||||
termLog(' <span style="color:#3fb950">✓</span> Laskentasolmu yhdistetty hubiin');
|
||||
}
|
||||
|
||||
// Kuunnellaan console.log mallin latauksen etenemiselle
|
||||
const _origLog = console.log;
|
||||
console.log = function(...args) {
|
||||
_origLog.apply(console, args);
|
||||
const msg = args.join(' ');
|
||||
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
|
||||
llmReady = true;
|
||||
document.getElementById('compute-dot').style.background = '#3fb950';
|
||||
document.getElementById('compute-label').textContent = 'Qwen2.5-Coder:0.5B';
|
||||
document.getElementById('compute-label').style.color = '#3fb950';
|
||||
const btn = document.getElementById('compute-btn');
|
||||
if (btn) { btn.textContent = '✓ Valmis'; btn.className = 'btn btn-green'; }
|
||||
localStorage.setItem('kpn-coder-loaded', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
// Compute-nappi
|
||||
document.getElementById('compute-btn')?.addEventListener('click', () => {
|
||||
const btn = document.getElementById('compute-btn');
|
||||
if (btn.textContent.includes('Valmis')) return;
|
||||
btn.textContent = 'Ladataan...';
|
||||
btn.className = 'btn btn-muted';
|
||||
ensureNode();
|
||||
});
|
||||
|
||||
// Autostart
|
||||
if (localStorage.getItem('kpn-coder-loaded') === 'true') {
|
||||
setTimeout(() => ensureNode(), 300);
|
||||
}
|
||||
|
||||
// === kpnRun: lähettää promptin mallille ===
|
||||
const activeStreams = {};
|
||||
|
||||
async function kpnRun(model, prompt, silent) {
|
||||
const taskId = crypto.randomUUID();
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = 'terminal-line';
|
||||
statusDiv.id = 'status-' + taskId;
|
||||
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> käsittelee...</span>`;
|
||||
termPanel.appendChild(statusDiv);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
|
||||
try {
|
||||
// Varmistetaan solmu
|
||||
if (!wasmNodeStarted) {
|
||||
statusDiv.innerHTML = ' <span style="color:#d29922">→ Käynnistetään laskentasolmua...</span>';
|
||||
await ensureNode();
|
||||
// Odotetaan kunnes malli on latautunut tai 60s
|
||||
for (let i = 0; i < 120 && !llmReady; i++) await new Promise(r => setTimeout(r, 500));
|
||||
if (!llmReady) {
|
||||
statusDiv.innerHTML = ' <span style="color:#f85149">✗ Mallin lataus aikakatkaistiin</span>';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
const streamDiv = document.createElement('div');
|
||||
streamDiv.className = 'terminal-line';
|
||||
streamDiv.innerHTML = ' <span class="stream-content"></span><span style="color:#8b949e;animation:blink 1s infinite">▌</span>';
|
||||
termPanel.appendChild(streamDiv);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
activeStreams[taskId] = streamDiv;
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = ` <span style="color:#8b949e">→ <span style="color:var(--accent)">${model}</span> käsittelee...</span>`;
|
||||
|
||||
const res = await fetch('/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt, task_id: taskId }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text().catch(() => res.statusText);
|
||||
statusDiv.innerHTML = ` <span style="color:#f85149">✗ ${esc(err)}</span>`;
|
||||
return null;
|
||||
}
|
||||
const data = await res.json();
|
||||
const response = (data.response || '').trim();
|
||||
const tokGen = data.tokens_generated || 0;
|
||||
const durS = data.duration_ms ? (data.duration_ms / 1000).toFixed(1) + 's' : '';
|
||||
const tokS = data.tokens_per_sec ? data.tokens_per_sec.toFixed(1) + ' tok/s' : '';
|
||||
statusDiv.innerHTML = ` <span style="color:#3fb950">✓</span> <span style="color:var(--accent)">${esc(data.model || model)}</span> <span style="color:#8b949e">${tokGen} tok · ${durS} · ${tokS}</span>`;
|
||||
|
||||
if (!silent && response) {
|
||||
const firstLine = response.split('\n').find(l => l.trim()) || response;
|
||||
const lineCount = response.split('\n').filter(l => l.trim()).length;
|
||||
const uid = 'code-' + Date.now();
|
||||
termLog(
|
||||
` <span style="color:#3fb950;cursor:pointer" onclick="document.getElementById('${uid}').style.display=document.getElementById('${uid}').style.display==='none'?'block':'none'">`
|
||||
+ `<span style="color:#8b949e">▶</span> ${esc(firstLine.trim())} <span style="color:#8b949e">${lineCount > 1 ? '(+' + (lineCount-1) + ' riviä)' : ''}</span></span>`
|
||||
+ `<pre id="${uid}" style="display:none;margin:4px 0 0 16px;font:inherit;white-space:pre-wrap;border-left:2px solid var(--border);padding-left:10px">${highlightCode(response)}</pre>`
|
||||
);
|
||||
}
|
||||
return response;
|
||||
} catch(e) {
|
||||
statusDiv.innerHTML = ` <span style="color:#f85149">✗ ${esc(e.message)}</span>`;
|
||||
return null;
|
||||
} finally {
|
||||
if (activeStreams[taskId]) { activeStreams[taskId].remove(); delete activeStreams[taskId]; }
|
||||
}
|
||||
}
|
||||
|
||||
// === WebSocket message handler ===
|
||||
uiSocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'stats') {
|
||||
document.getElementById('hub-version').textContent = 'v' + (data.version || '?');
|
||||
} else if (data.type === 'task_routed') {
|
||||
const statusDiv = document.getElementById('status-' + data.task_id);
|
||||
if (statusDiv) {
|
||||
const color = data.status === 'queued' ? '#d29922' : '#8b949e';
|
||||
statusDiv.innerHTML = ` <span style="color:${color}">${data.status === 'queued' ? '⏳' : '→'} ${esc(data.message)}</span>`;
|
||||
}
|
||||
} else if (data.type === 'llm_chunk' && data.task_id && activeStreams[data.task_id]) {
|
||||
const el = activeStreams[data.task_id].querySelector('.stream-content');
|
||||
if (el) { el.textContent += data.token || ''; termPanel.scrollTop = termPanel.scrollHeight; }
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
// === Terminal commands ===
|
||||
const kpnCommands = {
|
||||
'kpn': ['help','run','project','pipeline','load','status','models','clear'],
|
||||
'kpn run': ['coder','coder-3b','manager','tester','qa','qwen-coder','smollm-135m'],
|
||||
'kpn load': ['1','2'],
|
||||
'kpn project': ['"'],
|
||||
'kpn pipeline': ['"'],
|
||||
};
|
||||
const kpnExamples = {
|
||||
'kpn run coder': ['"hello world in python"','"fibonacci in rust"','"quicksort in javascript"'],
|
||||
'kpn run coder-3b': ['"REST API with Flask"','"binary search tree"'],
|
||||
'kpn project': ['"FastAPI + SQLite REST API"','"CLI tool for CSV processing"'],
|
||||
'kpn pipeline': ['"todo-sovellus"','"laskin pythonilla"'],
|
||||
};
|
||||
|
||||
// Tab completion
|
||||
function getCompletions(val) {
|
||||
const words = val.trimEnd().split(/\s+/);
|
||||
for (let d = words.length; d >= 1; d--) {
|
||||
const prefix = words.slice(0,d).join(' ');
|
||||
const partial = words[d] || '';
|
||||
if (kpnExamples[prefix] && !partial) return { items: kpnExamples[prefix], prefix: prefix + ' ' };
|
||||
const cands = kpnCommands[prefix];
|
||||
if (cands) {
|
||||
const m = partial ? cands.filter(c => c.startsWith(partial)) : cands;
|
||||
if (m.length > 0) return { items: m, prefix: prefix + ' ' };
|
||||
}
|
||||
}
|
||||
if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' };
|
||||
return { items: [], prefix: val };
|
||||
}
|
||||
|
||||
// Dropdown
|
||||
const dropdown = document.getElementById('term-dropdown');
|
||||
let ddItems = [], ddIdx = -1, ddPrefix = '';
|
||||
|
||||
function showDD(items, prefix) {
|
||||
if (!items.length) { hideDD(); return; }
|
||||
ddItems = items; ddPrefix = prefix; ddIdx = -1;
|
||||
dropdown.innerHTML = items.map((item,i) =>
|
||||
`<div class="dd-item" data-i="${i}" onclick="selectDD()">${esc(item)}</div>`
|
||||
).join('');
|
||||
dropdown.style.display = 'block';
|
||||
dropdown.querySelectorAll('.dd-item').forEach(el => {
|
||||
el.addEventListener('mouseenter', () => highlightDD(+el.dataset.i));
|
||||
});
|
||||
}
|
||||
function hideDD() { dropdown.style.display = 'none'; ddItems = []; ddIdx = -1; }
|
||||
function highlightDD(i) {
|
||||
ddIdx = i;
|
||||
dropdown.querySelectorAll('.dd-item').forEach((el,j) => el.classList.toggle('active', j===i));
|
||||
dropdown.children[i]?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
window.selectDD = function() {
|
||||
if (ddIdx >= 0 && ddIdx < ddItems.length)
|
||||
termInput.value = ddPrefix + ddItems[ddIdx] + (ddItems[ddIdx].startsWith('"') ? '' : ' ');
|
||||
hideDD(); termInput.focus();
|
||||
};
|
||||
|
||||
// Agenttiprompti-mapping
|
||||
const agentModels = { coder: 'qwen-coder', 'coder-3b': 'qwen-coder-3b', manager: 'qwen-coder', tester: 'smollm-135m', qa: 'smollm-135m' };
|
||||
|
||||
function termExec(cmd) {
|
||||
termLog(`<span class="terminal-prompt">$</span> ${esc(cmd)}`);
|
||||
termHistory.unshift(cmd); termHistIdx = -1;
|
||||
const parts = cmd.trim().split(/\s+/);
|
||||
if (parts[0] !== 'kpn') { termLog(' Tuntematon komento. Kokeile: kpn help', '#f85149'); return; }
|
||||
const sub = parts[1];
|
||||
|
||||
if (sub === 'help' || !sub) {
|
||||
termLog(' kpn run <malli> "prompti" — aja tehtävä', '#a5d6ff');
|
||||
termLog(' kpn project "kuvaus" — monivaiheinen projekti', '#a5d6ff');
|
||||
termLog(' kpn pipeline "tehtävä" — nopea: manageri→koodari→testaaja', '#a5d6ff');
|
||||
termLog(' kpn load — lataa kielimalli', '#a5d6ff');
|
||||
termLog(' kpn models — mallit', '#a5d6ff');
|
||||
termLog(' kpn status — verkon tila', '#a5d6ff');
|
||||
termLog(' kpn clear — tyhjennä', '#a5d6ff');
|
||||
} else if (sub === 'clear') { termPanel.innerHTML = '';
|
||||
} else if (sub === 'load') {
|
||||
const btn = document.getElementById('compute-btn');
|
||||
if (btn && btn.textContent.includes('Valmis')) { termLog(' ✓ Malli jo ladattu', '#3fb950'); }
|
||||
else { btn?.click(); }
|
||||
} else if (sub === 'models') {
|
||||
termLog(' <span style="color:var(--accent)">1</span> qwen-coder Qwen2.5-Coder:0.5B <span style="color:#8b949e">~990 MB</span>');
|
||||
termLog(' <span style="color:var(--accent)">2</span> qwen-coder-3b Qwen2.5-Coder:3B <span style="color:#8b949e">~6.2 GB</span>');
|
||||
termLog(' <span style="color:var(--accent)">3</span> smollm-135m SmolLM 135M <span style="color:#8b949e">~270 MB</span>');
|
||||
} else if (sub === 'status') {
|
||||
termLog(` Hub: ${document.getElementById('hub-label').textContent} | Laskenta: ${document.getElementById('compute-label').textContent}`, '#a5d6ff');
|
||||
} else if (sub === 'run') {
|
||||
let model = parts[2];
|
||||
const after = cmd.replace(/^kpn\s+run\s+\S+\s*/, '');
|
||||
const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const prompt = (m && (m[1]||m[2]||m[3]||'')).trim();
|
||||
if (!model || !prompt) { termLog(' Käyttö: kpn run <malli> "prompti"', '#f85149'); return; }
|
||||
if (agentModels[model]) model = agentModels[model];
|
||||
kpnRun(model, prompt);
|
||||
} else if (sub === 'project') {
|
||||
const after = cmd.replace(/^kpn\s+project\s*/, '');
|
||||
const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const task = (m && (m[1]||m[2]||m[3]||'')).trim();
|
||||
if (!task) { termLog(' Käyttö: kpn project "kuvaus"', '#f85149'); return; }
|
||||
kpnProject(task);
|
||||
} else if (sub === 'pipeline') {
|
||||
const after = cmd.replace(/^kpn\s+pipeline\s*/, '');
|
||||
const m = after.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||
const task = (m && (m[1]||m[2]||m[3]||'')).trim();
|
||||
if (!task) { termLog(' Käyttö: kpn pipeline "tehtävä"', '#f85149'); return; }
|
||||
kpnPipelineSimple(task);
|
||||
} else { termLog(` Tuntematon: ${sub}. Kokeile: kpn help`, '#f85149'); }
|
||||
}
|
||||
|
||||
// Input handler
|
||||
termInput?.addEventListener('keydown', (e) => {
|
||||
if (dropdown.style.display === 'block') {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); highlightDD(Math.min(ddIdx+1, ddItems.length-1)); return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); highlightDD(Math.max(ddIdx-1, 0)); return; }
|
||||
if ((e.key === 'Enter' || e.key === 'Tab') && ddIdx >= 0) { e.preventDefault(); selectDD(); return; }
|
||||
if (e.key === 'Escape') { e.preventDefault(); hideDD(); return; }
|
||||
}
|
||||
if (e.key === 'Tab' && e.shiftKey) {
|
||||
e.preventDefault(); hideDD();
|
||||
const val = termInput.value.trimEnd();
|
||||
if (!val) return;
|
||||
const qm = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/);
|
||||
if (qm) termInput.value = (qm[1]||qm[2]).trimEnd() + ' ';
|
||||
else { const ls = val.lastIndexOf(' '); termInput.value = ls > 0 ? val.substring(0, ls+1) : ''; }
|
||||
} else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const { items, prefix } = getCompletions(termInput.value);
|
||||
if (items.length === 1) { termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' '); hideDD(); }
|
||||
else if (items.length > 1) showDD(items, prefix);
|
||||
} else if (e.key === 'Enter') {
|
||||
hideDD();
|
||||
const cmd = termInput.value.trim();
|
||||
if (cmd) termExec(cmd);
|
||||
termInput.value = '';
|
||||
} else if (e.key === 'ArrowUp') { e.preventDefault(); if (termHistIdx < termHistory.length-1) { termHistIdx++; termInput.value = termHistory[termHistIdx]; }
|
||||
} else if (e.key === 'ArrowDown') { e.preventDefault(); if (termHistIdx > 0) { termHistIdx--; termInput.value = termHistory[termHistIdx]; } else { termHistIdx=-1; termInput.value=''; }
|
||||
}
|
||||
});
|
||||
termPanel?.addEventListener('click', () => termInput?.focus());
|
||||
document.addEventListener('click', (e) => { if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDD(); });
|
||||
|
||||
// === Project pipeline ===
|
||||
async function kpnProject(task) {
|
||||
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Projekti käynnistyy ━━━</span>`);
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — suunnittelu`);
|
||||
const plan = await kpnRun('qwen-coder', `List the source files needed for this project. One file per line, format:\nfilename.py: what this file contains\n\nRules:\n- Max 4 files\n- Only .py, .toml, .json, .html files\n- No directories, just filenames\n- Dependencies first (models.py before main.py)\n- Use pyproject.toml for deps\n\nProject: ${task}`);
|
||||
if (!plan) { termLog(' ✗ Keskeytyi', '#f85149'); return; }
|
||||
|
||||
const fileList = plan.split('\n').map(l => l.trim().replace(/^[\d\.\-\*\s]+/,'').replace(/\*+/g,'').replace(/`/g,'')).map(l => {
|
||||
if (l.includes(':')) { const [n,...d] = l.split(':'); return { name: n.trim(), desc: d.join(':').trim() }; }
|
||||
return { name: l.trim(), desc: '' };
|
||||
}).filter(f => f.name.length > 0 && f.name.length < 40 && !f.name.includes('/') && !f.name.includes(' ') && /\.\w{1,5}$/.test(f.name));
|
||||
|
||||
if (!fileList.length) {
|
||||
termLog(' Ei tiedostojakoa — generoidaan yhtenä', '#8b949e');
|
||||
await kpnRun('qwen-coder', `Project: ${task}\n\nWrite all the code.`);
|
||||
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
|
||||
return;
|
||||
}
|
||||
|
||||
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f=>f.name).join(', ')}</span>`);
|
||||
const files = {};
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const f = fileList[i];
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i+2}] Koodari</span> — ${esc(f.name)}`);
|
||||
let ctx = '';
|
||||
const prev = Object.entries(files);
|
||||
if (prev.length) ctx = 'Already written:\n' + prev.map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n') + '\n\n';
|
||||
let extra = '';
|
||||
if (f.name === 'pyproject.toml') extra = '\nUse format: [project]\\nname="proj"\\nversion="0.1.0"\\nrequires-python=">=3.11"\\ndependencies=["fastapi","uvicorn"]';
|
||||
const code = await kpnRun('qwen-coder', `${ctx}Project: ${task}\nWrite ONLY "${f.name}"${f.desc ? ': '+f.desc : ''}.${extra}\nUse exact libraries from project description.`);
|
||||
if (!code) { termLog(` ✗ Keskeytyi (${f.name})`, '#f85149'); return; }
|
||||
files[f.name] = code;
|
||||
}
|
||||
|
||||
// Review
|
||||
const allCode = Object.entries(files).map(([n,c]) => `--- ${n} ---\n${c}`).join('\n\n');
|
||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[${fileList.length+2}] Testaaja</span> — review`);
|
||||
const review = await kpnRun('smollm-135m', `Review briefly. Say LGTM if ok.\n${allCode}`);
|
||||
if (review && !review.toLowerCase().includes('lgtm')) {
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length+3}] Korjaukset</span>`);
|
||||
await kpnRun('qwen-coder', `Fix issues:\n${review}\n\nCode:\n${allCode}`);
|
||||
}
|
||||
|
||||
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis (${Object.keys(files).length} tiedostoa) ━━━</span>`);
|
||||
renderProjectCard(files, task);
|
||||
}
|
||||
|
||||
async function kpnPipelineSimple(task) {
|
||||
termLog(`<span style="color:var(--purple);font-weight:bold">━━━ Pipeline ━━━</span>`);
|
||||
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span>`);
|
||||
const plan = await kpnRun('qwen-coder', `Analyse briefly, write a spec:\n${task}`);
|
||||
if (!plan) return;
|
||||
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span>`);
|
||||
const code = await kpnRun('qwen-coder', `${plan}\n\nWrite the code.`);
|
||||
if (!code) return;
|
||||
termLog(`\n<span style="color:var(--accent);font-weight:bold">[3/3] Testaaja</span>`);
|
||||
await kpnRun('smollm-135m', `Review briefly:\n${code}`);
|
||||
termLog(`\n<span style="color:var(--purple);font-weight:bold">━━━ Valmis ━━━</span>`);
|
||||
}
|
||||
|
||||
// === Project card ===
|
||||
function renderProjectCard(files, name) {
|
||||
const entries = Object.entries(files);
|
||||
if (!entries.length) return;
|
||||
const id = 'proj-' + Date.now();
|
||||
const tabs = entries.map(([n],i) => `<div class="project-tab${i===0?' active':''}" data-card="${id}" data-i="${i}" onclick="switchProjTab('${id}',${i})">${esc(n)}</div>`).join('');
|
||||
const panels = entries.map(([n,c],i) => `<div class="proj-panel" data-card="${id}" data-i="${i}" style="${i>0?'display:none':''}"><div style="text-align:right;padding:4px 8px;background:var(--bg);border-bottom:1px solid #21262d"><button class="btn btn-muted" onclick="navigator.clipboard.writeText(JSON.parse(document.getElementById('${id}').dataset.files)['${n}'])">Kopioi</button></div><pre class="code-block">${highlightCode(c)}</pre></div>`).join('');
|
||||
const html = `<div id="${id}" class="project-card" data-files='${esc(JSON.stringify(files))}'><div class="project-header"><span style="color:var(--purple);font-weight:600">${esc(name||'Projekti')} <span style="color:#8b949e;font-weight:normal">(${entries.length})</span></span><span style="display:flex;gap:6px"><button class="btn btn-muted" onclick="navigator.clipboard.writeText(Object.entries(JSON.parse(document.getElementById('${id}').dataset.files)).map(([n,c])=>'# --- '+n+' ---\\n'+c).join('\\n\\n'))">Kopioi kaikki</button><button class="btn btn-green" onclick="openInEditor(JSON.parse(document.getElementById('${id}').dataset.files))">Avaa editorissa</button></span></div><div class="project-tabs">${tabs}</div>${panels}</div>`;
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
termPanel.appendChild(div.firstElementChild);
|
||||
termPanel.scrollTop = termPanel.scrollHeight;
|
||||
}
|
||||
window.switchProjTab = function(id,i) {
|
||||
document.querySelectorAll(`.project-tab[data-card="${id}"]`).forEach((t,j) => t.classList.toggle('active', j===i));
|
||||
document.querySelectorAll(`.proj-panel[data-card="${id}"]`).forEach((p,j) => p.style.display = j===i ? '' : 'none');
|
||||
};
|
||||
|
||||
// === Monaco Editor (lazy load) ===
|
||||
let monacoLoaded = false;
|
||||
window.MonacoEnvironment = { getWorkerUrl: () => `data:text/javascript,self.MonacoEnvironment={baseUrl:'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/'};importScripts('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/worker/workerMain.js')` };
|
||||
|
||||
async function initMonaco() {
|
||||
if (monacoLoaded) return;
|
||||
monacoLoaded = true;
|
||||
await new Promise(r => { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js'; s.onload = () => { require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs'}}); require(['vs/editor/editor.main'],()=>r()); }; document.head.appendChild(s); });
|
||||
window._monaco = monaco.editor.create(document.getElementById('monaco-container'), { value: '// Valitse tiedosto tai generoi projekti\n', language: 'plaintext', theme: 'vs-dark', fontSize: 14, minimap:{enabled:false}, automaticLayout: true, padding:{top:10} });
|
||||
}
|
||||
|
||||
window._editorModels = {};
|
||||
const langMap = {py:'python',rs:'rust',js:'javascript',ts:'typescript',toml:'toml',json:'json',html:'html',css:'css',md:'markdown',txt:'plaintext'};
|
||||
|
||||
window.openInEditor = function(files) {
|
||||
switchTab('editor');
|
||||
initMonaco().then(() => {
|
||||
for (const [name,code] of Object.entries(files)) {
|
||||
const ext = name.split('.').pop().toLowerCase();
|
||||
if (window._editorModels[name]) window._editorModels[name].setValue(code);
|
||||
else window._editorModels[name] = monaco.editor.createModel(code, langMap[ext]||'plaintext');
|
||||
}
|
||||
document.getElementById('editor-file-list').innerHTML = Object.keys(files).map(n => `<div class="dd-item" onclick="openFile('${n}')">${n}</div>`).join('');
|
||||
document.getElementById('editor-tabs').innerHTML = Object.keys(files).map(n => `<div class="project-tab" onclick="openFile('${n}')">${n}</div>`).join('');
|
||||
openFile(Object.keys(files)[0]);
|
||||
});
|
||||
};
|
||||
window.openFile = function(name) {
|
||||
if (!window._editorModels[name] || !window._monaco) return;
|
||||
window._monaco.setModel(window._editorModels[name]);
|
||||
document.querySelectorAll('#editor-file-list .dd-item').forEach(el => el.style.background = el.textContent===name ? 'var(--border)' : '');
|
||||
document.querySelectorAll('#editor-tabs .project-tab').forEach(el => el.classList.toggle('active', el.textContent===name));
|
||||
};
|
||||
|
||||
// === Guide loader ===
|
||||
(async () => {
|
||||
const el = document.getElementById('guide-content');
|
||||
try {
|
||||
const r = await fetch('/GUIDE.md');
|
||||
if (r.ok) el.innerHTML = renderMd(await r.text());
|
||||
el.querySelectorAll('pre code').forEach(b => { if (typeof hljs !== 'undefined') hljs.highlightElement(b); });
|
||||
} catch(e) { el.textContent = 'Virhe: ' + e.message; }
|
||||
})();
|
||||
|
||||
function renderMd(md) {
|
||||
let html = '', inCode = false, lang = '', buf = '';
|
||||
for (const line of md.split('\n')) {
|
||||
if (line.startsWith('```')) { if (inCode) { html += `<pre class="code-block"><code class="language-${lang}">${buf.replace(/</g,'<')}</code></pre>`; inCode=false; buf=''; } else { inCode=true; lang=line.slice(3).trim()||'plaintext'; } continue; }
|
||||
if (inCode) { buf += (buf?'\n':'') + line; continue; }
|
||||
if (!line.trim()) { html += '<br>'; continue; }
|
||||
if (line.startsWith('# ')) html += `<h1 style="color:#e6edf3;font-size:24px;margin:24px 0 8px;border-bottom:1px solid var(--border);padding-bottom:6px">${line.slice(2)}</h1>`;
|
||||
else if (line.startsWith('## ')) html += `<h2 style="color:#e6edf3;font-size:20px;margin:20px 0 8px">${line.slice(3)}</h2>`;
|
||||
else if (line.startsWith('### ')) html += `<h3 style="color:#e6edf3;font-size:16px;margin:16px 0 6px">${line.slice(4)}</h3>`;
|
||||
else if (line.startsWith('---')) html += '<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';
|
||||
else if (line.match(/^[\-\*] /)) html += `<div style="padding:2px 0 2px 20px">${line.replace(/^[\-\*] /,'• ').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`(.+?)`/g,'<code style="background:var(--panel);padding:1px 4px;border-radius:3px;font-size:13px">$1</code>')}</div>`;
|
||||
else html += `<p style="margin:4px 0">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`(.+?)`/g,'<code style="background:var(--panel);padding:1px 4px;border-radius:3px;font-size:13px">$1</code>')}</p>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
</script> </body> </html>
|
||||
63
network-poc/frontend/dist/pkg/node.d.ts
vendored
Normal file
63
network-poc/frontend/dist/pkg/node.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function set_auto_tasks(enabled: boolean): void;
|
||||
|
||||
export function set_gpu_load(load: number): void;
|
||||
|
||||
export function start_agent_node(hub_url: string, has_webgpu: boolean, device_info_json: string, task_id: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
|
||||
* Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
|
||||
*/
|
||||
export function tokenize_js(text: string): Promise<string>;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly set_auto_tasks: (a: number) => void;
|
||||
readonly set_gpu_load: (a: number) => void;
|
||||
readonly start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
||||
readonly tokenize_js: (a: number, b: number) => any;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
1741
network-poc/frontend/dist/pkg/node.js
vendored
Normal file
1741
network-poc/frontend/dist/pkg/node.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
network-poc/frontend/dist/pkg/node_bg.wasm
vendored
Normal file
BIN
network-poc/frontend/dist/pkg/node_bg.wasm
vendored
Normal file
Binary file not shown.
24
network-poc/frontend/dist/pkg/node_bg.wasm.d.ts
vendored
Normal file
24
network-poc/frontend/dist/pkg/node_bg.wasm.d.ts
vendored
Normal 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;
|
||||
15
network-poc/frontend/dist/pkg/package.json
vendored
Normal file
15
network-poc/frontend/dist/pkg/package.json
vendored
Normal 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/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user