Compare commits
14 Commits
ff3a720b8d
...
agentic-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac15336c9f | ||
|
|
7a15cacebf | ||
|
|
27135a8f14 | ||
|
|
e28a715f32 | ||
|
|
24d29d9ba9 | ||
|
|
7eca426e77 | ||
|
|
7a1352ead7 | ||
|
|
b9017448d8 | ||
|
|
3d1b406e8d | ||
|
|
aa6c4739dd | ||
|
|
cbbf427a93 | ||
|
|
0a216f19e2 | ||
|
|
a2e7ed53ff | ||
|
|
950cae9d96 |
525
network-poc/BUILDING_BLOCKS.md
Normal file
525
network-poc/BUILDING_BLOCKS.md
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
# Kipinä Agentic Studio — Rakennuspalaset
|
||||||
|
|
||||||
|
Tämä dokumentti kuvaa projektin UI-komponentit, arkkitehtuuripatternit ja työnkulut niin, että vastaavan hajautetun AI-laskentaverkon ja agenttipohjaisen käyttöliittymän voi rakentaa alusta asti.
|
||||||
|
|
||||||
|
## Yleiskuva
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Selain (käyttäjä) │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ Verkko- │ │ Koodi- │ │ Agents-näkymä │ │
|
||||||
|
│ │ näkymä │ │ labra │ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ │ │ │ │ Terminaali │ │ │
|
||||||
|
│ │ Stats │ │ Editor │ │ │ Tab-complete │ │ │
|
||||||
|
│ │ Chat │ │ Pipeline │ │ │ Dropdown │ │ │
|
||||||
|
│ │ Tokenit │ │ Tulokset │ │ │ Historia │ │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ │ └───────────────┘ │ │
|
||||||
|
│ │ │ └────────┬──────────┘ │
|
||||||
|
│ └──────────┬───┘ │ │
|
||||||
|
│ UI WebSocket HTTP API │
|
||||||
|
│ │ /api/v1/chat │
|
||||||
|
│ ┌───────────────┴──────────────┐ │ │
|
||||||
|
│ │ Wasm Compute Node │ │ │
|
||||||
|
│ │ (Candle + Burn) │ │ │
|
||||||
|
│ │ ┌─────────┐ ┌────────────┐ │ │ │
|
||||||
|
│ │ │ RAM │ │ IndexedDB │ │ │ │
|
||||||
|
│ │ │ Cache │ │ Cache │ │ │ │
|
||||||
|
│ │ └─────────┘ └────────────┘ │ │ │
|
||||||
|
│ │ ┌─────────────────────────┐ │ │ │
|
||||||
|
│ │ │ Model Cache (QwenModel) │ │ │ │
|
||||||
|
│ │ └─────────────────────────┘ │ │ │
|
||||||
|
│ └──────────────┬───────────────┘ │ │
|
||||||
|
│ │ WS │ │
|
||||||
|
└─────────────────┼──────────────────────┼─────────────┘
|
||||||
|
│ │
|
||||||
|
┌────────┴──────────────────────┴──┐
|
||||||
|
│ Hub (Axum + Tokio) │
|
||||||
|
│ ┌────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Broadcast │ │ Node │ │
|
||||||
|
│ │ Channel │ │ Registry │ │
|
||||||
|
│ └────────────┘ └─────────────┘ │
|
||||||
|
│ ┌────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Busy-State │ │ Rate Limit │ │
|
||||||
|
│ │ Tracker │ │ + Auth │ │
|
||||||
|
│ └────────────┘ └─────────────┘ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ SQLite (sessiot, tulokset) │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. WebSocket-reaaliaikakommunikaatio
|
||||||
|
|
||||||
|
### 1.1 Hub ↔ Node broadcast-kanava
|
||||||
|
|
||||||
|
**Tarkoitus:** Jakaa tehtäviä ja vastaanottaa tuloksia kaikilta laskentasolmuilta.
|
||||||
|
|
||||||
|
**Työnkulku:**
|
||||||
|
1. Hub luo `tokio::sync::broadcast::channel(100)`
|
||||||
|
2. Jokainen solmu saa oman `rx = stats_tx.subscribe()`
|
||||||
|
3. Hub broadcastaa tehtävät: `stats_tx.send(json)`
|
||||||
|
4. Solmut suodattavat viestin tyypin ja `selected_task`:n perusteella
|
||||||
|
|
||||||
|
**Viestityupit:**
|
||||||
|
|
||||||
|
| Tyyppi | Suunta | Sisältö |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `stats` | Hub → kaikki | nodes, vram_gb, tasks |
|
||||||
|
| `pair_task` | Hub → tokenize-solmut | en, fi tekstiparit |
|
||||||
|
| `llm_prompt` | Hub → valittu solmu | prompt, model, task_id |
|
||||||
|
| `llm_chunk` | Solmu → Hub → UI | token (1 kerrallaan) |
|
||||||
|
| `llm_done` | Solmu → Hub → UI | response, tokens_generated, duration_ms |
|
||||||
|
| `llm_error` | Solmu → Hub → UI | error, task_id |
|
||||||
|
| `task_routed` | Hub → UI | status (routed/queued), node_id, message |
|
||||||
|
|
||||||
|
**Lagged-viestien käsittely:**
|
||||||
|
```rust
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(msg) => { /* käsittele */ }
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
// Ohitetaan vanhat viestit, ei katkaista yhteyttä
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => break, // Kanava suljettu
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Kohdennettu reititys (Direct Channel)
|
||||||
|
|
||||||
|
**Tarkoitus:** Lähetä tehtävä yhdelle tietylle solmulle broadcastin sijaan.
|
||||||
|
|
||||||
|
**Työnkulku:**
|
||||||
|
1. Jokainen solmu saa `mpsc::unbounded_channel` yhdistyessään
|
||||||
|
2. Hub tallentaa `node_channels: HashMap<u64, UnboundedSender>`
|
||||||
|
3. API-pyyntö → valitaan vapaa solmu → lähetetään suoraan kanavaan
|
||||||
|
4. Broadcast-kanavaa käytetään vain tuloksen välittämiseen UI:lle
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let channels = state.node_channels.read().await;
|
||||||
|
if let Some(tx) = channels.get(&target_node_id) {
|
||||||
|
tx.send(msg.to_string());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Busy-state ja työjono
|
||||||
|
|
||||||
|
**Tarkoitus:** Estä tehtävien reititys varatuille solmuille.
|
||||||
|
|
||||||
|
**Rakenne:**
|
||||||
|
- `node_busy: HashSet<u64>` — solmut joilla on aktiivinen tehtävä
|
||||||
|
- Asetetaan kun tehtävä reititetään, vapautetaan `llm_done`/`llm_error`:ssa
|
||||||
|
- Jos kaikki solmut varattuja → pollaa 500ms välein, max 30s
|
||||||
|
|
||||||
|
**UI-palaute:**
|
||||||
|
```json
|
||||||
|
{"type": "task_routed", "status": "queued", "message": "Kaikki 2 solmua varattuja — odotetaan..."}
|
||||||
|
{"type": "task_routed", "status": "routed", "node_id": 3, "message": "Solmu #3 vapautui (2.5s jonossa)"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Wasm-laskentasolmu
|
||||||
|
|
||||||
|
### 2.1 Elinkaari
|
||||||
|
|
||||||
|
```
|
||||||
|
init() → start_agent_node(ws_url, has_webgpu, device_info, task_id)
|
||||||
|
│
|
||||||
|
├─ Avaa WebSocket hubiin
|
||||||
|
├─ Lähettää auth-viestin (laitetiedot, selected_task)
|
||||||
|
├─ Rekisteröityy onmessage-käsittelijä
|
||||||
|
│ ├─ pair_task → tokenize
|
||||||
|
│ ├─ llm_prompt → inference
|
||||||
|
│ └─ ai_task → tensor matmul
|
||||||
|
└─ Odottaa tehtäviä loopissa
|
||||||
|
```
|
||||||
|
|
||||||
|
**Globaali tila (atominen, lukitsematon):**
|
||||||
|
```rust
|
||||||
|
static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50);
|
||||||
|
static LLM_BUSY: AtomicBool = AtomicBool::new(false);
|
||||||
|
static SELECTED_TASK: AtomicU32 = AtomicU32::new(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Kolmitasoinen cache
|
||||||
|
|
||||||
|
```
|
||||||
|
Pyyntö → [1] RAM-cache (thread_local HashMap)
|
||||||
|
│ miss
|
||||||
|
▼
|
||||||
|
[2] IndexedDB (selaimen pysyvä tallennus)
|
||||||
|
│ miss
|
||||||
|
▼
|
||||||
|
[3] Verkko (HuggingFace CDN, streaming + 5% progressi)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Tallenna → IndexedDB → RAM-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
| Taso | Nopeus | Koko | Pysyvyys |
|
||||||
|
|------|--------|------|----------|
|
||||||
|
| RAM | ~0ms | Rajaton | Sivulataus |
|
||||||
|
| IndexedDB | ~50ms | ~50GB | Pysyvä |
|
||||||
|
| Verkko | ~10s/100MB | ∞ | — |
|
||||||
|
|
||||||
|
**Malliinstanssin cache (neljäs taso):**
|
||||||
|
```rust
|
||||||
|
thread_local! {
|
||||||
|
static MODEL_CACHE: RefCell<Option<CachedModel>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
// clear_kv_cache() promptien välillä — ei tarvitse rakentaa mallia uusiksi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Warmup-esilataus
|
||||||
|
|
||||||
|
**Tarkoitus:** Lataa malli valmiiksi ennen ensimmäistä oikeaa promptia.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Lähetetään 1 tokenin warmup heti kun WS on auki
|
||||||
|
uiSocket.send(JSON.stringify({
|
||||||
|
type: 'user_text',
|
||||||
|
text: '{"prompt":"warmup","max_tokens":1}',
|
||||||
|
task_type: 'qwen-coder'
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. LLM-inferenssipipeline
|
||||||
|
|
||||||
|
### 3.1 Prompt-formaatti (ChatML + prefill)
|
||||||
|
|
||||||
|
```
|
||||||
|
<|im_start|>system
|
||||||
|
You are a coding assistant. Respond with ONLY code.<|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
hello world in python<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
``` ← PREFILL: pakottaa mallin aloittamaan koodilla
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prefill-tekniikka:** Lisäämällä ` ``` ` assistantin vastauksen alkuun malli jatkaa suoraan koodilla eikä tuota "Sure! Here is..." -johdantoa. Säästää 10-20 tokenia per vastaus.
|
||||||
|
|
||||||
|
### 3.2 Sampling-parametrit
|
||||||
|
|
||||||
|
| Parametri | Arvo | Tarkoitus |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| `temperature` | 0.7 | Pehmentää jakaumaa, vähentää toistoa |
|
||||||
|
| `top_k` | 40 | Rajaa valinnan 40 todennäköisimpään tokeniin |
|
||||||
|
| `repetition_penalty` | 1.15 | Rankaisee jo generoitujen tokenien uudelleenvalintaa |
|
||||||
|
| `max_tokens` | 128 | Oletusraja, JSON-promptilla konfiguroitavissa |
|
||||||
|
|
||||||
|
**Sampling-funktio (top-k + temperature + repetition penalty):**
|
||||||
|
```rust
|
||||||
|
fn sample_top_k_with_penalty(logits, k, temperature, generated_tokens, penalty) -> u32 {
|
||||||
|
// 1. Repetition penalty: vähennä aiempien tokenien logitteja
|
||||||
|
// 2. Temperature scaling: jaa logitit temperaturella
|
||||||
|
// 3. Top-k: ota k suurinta
|
||||||
|
// 4. Softmax top-k:lle
|
||||||
|
// 5. Satunnaisvalinta kumulatiivisella todennäköisyydellä (XorShift RNG)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Stop-sekvenssit
|
||||||
|
|
||||||
|
Generointi katkaistaan ja teksti trimmataan kun malli alkaa selittää:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let stop_patterns = ["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Vastauksen siivous
|
||||||
|
|
||||||
|
```
|
||||||
|
Raakavastaus: "Sure! Here is...\n```python\n# This is a simple program\nprint('hi')\n```"
|
||||||
|
│
|
||||||
|
strip_markdown: "# This is a simple program\nprint('hi')"
|
||||||
|
│
|
||||||
|
strip_preamble: "print('hi')"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tunnistettavat selityskommentit:** `# This is`, `# simple`, `# program that`, `# here is`, `# the following`, `# below`
|
||||||
|
|
||||||
|
### 3.5 Streaming
|
||||||
|
|
||||||
|
Jokainen generoitu token lähetetään heti `llm_chunk`-viestinä:
|
||||||
|
```json
|
||||||
|
{"type": "llm_chunk", "token": "print", "prompt": "...", "model": "Qwen2.5-Coder", "task_id": "uuid"}
|
||||||
|
```
|
||||||
|
|
||||||
|
UI päivittää streaming-korttia reaaliaikaisesti appendaamalla tokeneita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Terminaaliemulaattori
|
||||||
|
|
||||||
|
### 4.1 Rakenne
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="agent-hub-status"> <!-- Status-palkki (Hub + Laskenta) -->
|
||||||
|
<div id="agent-terminal"> <!-- Scrollaava tulosalue, max 100 riviä -->
|
||||||
|
<div> <!-- Input-rivi -->
|
||||||
|
<span>$</span>
|
||||||
|
<input id="term-input">
|
||||||
|
<div id="term-dropdown"> <!-- Autocompletion-valikko -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Komentojen käsittely
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function termExec(cmd) {
|
||||||
|
// Parsitaan: "kpn" + alikomento + argumentit
|
||||||
|
// Tuetut: help, run, pipeline, load, status, models, hello, clear
|
||||||
|
// Agenttinimi → malli-mapping: "coder" → "qwen-coder"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Tab-completion (kolmitasoinen)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const kpnCommands = {
|
||||||
|
'kpn': ['help', 'run', 'pipeline', 'load', ...],
|
||||||
|
'kpn run': ['coder', 'manager', 'qwen-coder', ...],
|
||||||
|
};
|
||||||
|
const kpnExamples = {
|
||||||
|
'kpn run coder': ['"hello world in python"', ...],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Käyttö:**
|
||||||
|
|
||||||
|
| Näppäin | Toiminto |
|
||||||
|
|---------|----------|
|
||||||
|
| TAB | Täydennä seuraava sana tai avaa dropdown |
|
||||||
|
| Shift-TAB | Poista viimeinen sana (lainausmerkit kokonaisuutena) |
|
||||||
|
| ↑ / ↓ | Navigoi dropdownissa (tai komentohistoriassa) |
|
||||||
|
| Enter | Valitse dropdownista tai suorita komento |
|
||||||
|
| Esc | Sulje dropdown |
|
||||||
|
|
||||||
|
### 4.4 Dropdown-valikko
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function showDropdown(items, prefix) {
|
||||||
|
// Luo div.term-dd-item per vaihtoehto
|
||||||
|
// Positio: absolute, bottom: 100% (inputin yläpuolella)
|
||||||
|
// Mouseenter → highlight, click → valinta
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Komentohistoria
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const termHistory = []; // Kaikki ajetut komennot (viimeisin ensin)
|
||||||
|
let termHistIdx = -1; // Nykyinen positio historiassa
|
||||||
|
// ArrowUp: termHistIdx++, ArrowDown: termHistIdx--
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Status-palkit ja tilaindikaattorit
|
||||||
|
|
||||||
|
### 5.1 Hub-yhteyden tila
|
||||||
|
|
||||||
|
| Tila | Väri | Teksti | Tooltip |
|
||||||
|
|------|------|--------|---------|
|
||||||
|
| Yhdistetään | 🟡 | "Yhdistetään..." | WebSocket-yhteys Kipinä Hubiin |
|
||||||
|
| Yhdistetty | 🟢 | "Yhdistetty" | Tehtävien jakelu aktiivinen |
|
||||||
|
| Katkennut | 🔴 | "Yhteys katkennut" | Tarkista verkko, lataa uudelleen |
|
||||||
|
|
||||||
|
### 5.2 Laskentasolmun tila
|
||||||
|
|
||||||
|
| Tila | Väri | Teksti | Nappi |
|
||||||
|
|------|------|--------|-------|
|
||||||
|
| Ei käynnissä | ⚫ | "—" | `[Alusta laskentasolmu]` sininen |
|
||||||
|
| Lataa | 🟡 | "Ladataan..." | `[Peruuta]` punainen |
|
||||||
|
| Valmis | 🟢 | "Qwen2.5-Coder" | `[✓ Valmis]` vihreä |
|
||||||
|
|
||||||
|
### 5.3 Pipeline-tilakone (Codelab)
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: WebAssembly-ytimen lataus [◯ → ◷ → ✓]
|
||||||
|
Step 2: Tokenizer (7 MB) [◯ → ◷ → ✓]
|
||||||
|
Step 3: Mallipainot (990 MB) [◯ → ◷ 45% → ✓ cache]
|
||||||
|
Step 4: Mallin rakentaminen [◯ → ◷ → ✓]
|
||||||
|
Step 5: Valmis generoimaan [◯ → ✓]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Seuranta console.log-viesteistä:**
|
||||||
|
```javascript
|
||||||
|
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
|
||||||
|
// Merkkaa kaikki vaiheet valmiiksi (myös cache-hitillä)
|
||||||
|
setStep('step-wasm', 'done');
|
||||||
|
setStep('step-tokenizer', 'done');
|
||||||
|
setStep('step-model', 'done', 'cache');
|
||||||
|
setStep('step-build', 'done');
|
||||||
|
setStep('step-ready', 'done');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Tietoturva
|
||||||
|
|
||||||
|
### 6.1 XSS-suojaus
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function esc(str) {
|
||||||
|
return String(str).replace(/&/g,'&').replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Käyttöpaikat:** Kaikki `innerHTML`-insertoinnit joissa on käyttäjä- tai backend-dataa.
|
||||||
|
|
||||||
|
### 6.2 System prompt -piilotus
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function stripSystemPrompt(prompt) {
|
||||||
|
const parts = prompt.split('\n\n');
|
||||||
|
return parts[parts.length - 1] || prompt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Viestityyppivalidointi (backend)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const ALLOWED_MSG_TYPES: &[&str] = &[
|
||||||
|
"auth", "result", "pair_done", "llm_chunk", "llm_done",
|
||||||
|
"llm_error", "download_progress", "user_text", "single_tokenize_done"
|
||||||
|
];
|
||||||
|
|
||||||
|
fn validate_message(text: &str) -> Result<Value, &'static str> {
|
||||||
|
// 1. JSON-parsinta
|
||||||
|
// 2. "type"-kenttä pakollinen
|
||||||
|
// 3. Tyyppi sallittujen listalla
|
||||||
|
// 4. Tyyppikohtainen validointi (esim. pair_done: token_count <= 10000)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Rate limiting
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Per-IP liukuva ikkuna: max 10 pyyntöä per 60s
|
||||||
|
let entry = limits.entry(addr.ip()).or_insert((now, 0));
|
||||||
|
if now.duration_since(entry.0).as_secs() >= 60 {
|
||||||
|
*entry = (now, 1);
|
||||||
|
} else {
|
||||||
|
entry.1 += 1;
|
||||||
|
if entry.1 > 10 { return 429 Too Many Requests; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 Gamification-huijauksen esto
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Hub jakaa task_id:n → tallentaa pending_task_ids:hen
|
||||||
|
// Merkkejä jaetaan VAIN jos llm_done sisältää validin task_id:n
|
||||||
|
let valid_task = state.pending_task_ids.lock().unwrap().remove(tid);
|
||||||
|
if active_incentives && valid_task {
|
||||||
|
*balance += 20;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Syntaksikorostus
|
||||||
|
|
||||||
|
### 7.1 Highlight.js-integraatio
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function highlightCode(code) {
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
return hljs.highlightAuto(code).value; // Automaattinen kielentunnistus
|
||||||
|
}
|
||||||
|
return esc(code); // Fallback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Käyttöpaikat:** Codelab-tulokset, agents-terminaalin vastaukset, network-chat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Agenttien orkestrointi
|
||||||
|
|
||||||
|
### 8.1 Multi-agent pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ Manageri │ ──→ │ Koodari │ ──→ │ Testaaja │
|
||||||
|
│ Analysoi │ │ Koodaa │ │ Arvioi │
|
||||||
|
│ tehtävä │ │ ratkaisu │ │ koodi │
|
||||||
|
└──────────┘ └──────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function kpnPipeline(task) {
|
||||||
|
const plan = await kpnRun('qwen-coder', `Analysoi: ${task}`);
|
||||||
|
if (!plan) return;
|
||||||
|
const code = await kpnRun('qwen-coder', `Koodaa: ${plan}`);
|
||||||
|
if (!code) return;
|
||||||
|
await kpnRun('smollm-135m', `Arvioi: ${code}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Agenttien promptien hallinta
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const agentPrompts = {
|
||||||
|
manager: { model: 'qwen-coder', prompt: 'Olet projektipäällikkö...' },
|
||||||
|
coder: { model: 'qwen-coder', prompt: 'Olet ohjelmistokehittäjä...' },
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
// Tallennetaan localStorage:en per agentti
|
||||||
|
localStorage.setItem('kpn-agent-prompt-coder', customPrompt);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Yhteinen promptikonteksti
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function kpnRun(model, prompt) {
|
||||||
|
const parts = [];
|
||||||
|
if (sharedPrompt) parts.push(sharedPrompt); // Kaikille yhteinen
|
||||||
|
if (agent.prompt) parts.push(agent.prompt); // Agenttikohtainen
|
||||||
|
parts.push(prompt); // Käyttäjän pyyntö
|
||||||
|
const fullPrompt = parts.join('\n\n');
|
||||||
|
// → HTTP POST /api/v1/chat/completions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Teknologiapino
|
||||||
|
|
||||||
|
| Kerros | Teknologia | Tarkoitus |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| Frontend | Vanilla JS + HTML + CSS | Ei build-steppiä, toimii suoraan |
|
||||||
|
| Wasm | Rust + wasm-bindgen | Inferenssi selaimessa |
|
||||||
|
| LLM | Candle (Rust) | Transformer-inferenssi CPU:lla |
|
||||||
|
| Tensorit | Burn (Rust) | GPU-tensorilaskenta (WebGPU/NdArray) |
|
||||||
|
| Backend | Axum + Tokio (Rust) | Async WebSocket + HTTP -palvelin |
|
||||||
|
| Tietokanta | SQLite (rusqlite) | Sessiot ja tulokset |
|
||||||
|
| Cache | IndexedDB | Mallipainot selaimen pysyvässä muistissa |
|
||||||
|
| Korostus | Highlight.js (CDN) | Syntaksikorostus, automaattinen kielentunnistus |
|
||||||
|
| Tokenizer | HuggingFace tokenizers | BPE-tokenisaatio Wasmissa |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Jatkokehitysideoita
|
||||||
|
|
||||||
|
Näiden rakennuspalasten pohjalta voi rakentaa:
|
||||||
|
|
||||||
|
- **Oma chat-UI:** WebSocket + streaming + syntaksikorostus
|
||||||
|
- **Hajautettu laskentaverkko:** Hub + node-rekisteri + busy-state + työjono
|
||||||
|
- **Selain-LLM:** Wasm + Candle + IndexedDB-cache + warmup
|
||||||
|
- **Agenttipohjainen työnkulku:** Pipeline + prompt-orkestrointi + reititys
|
||||||
|
- **Terminaaliemulasttori:** Input + historia + tab-completion + dropdown
|
||||||
|
- **Reaaliaikadashboard:** WebSocket broadcast + tilaindikaattorit + metriikat
|
||||||
@@ -207,8 +207,8 @@ impl LlmEngine {
|
|||||||
|
|
||||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
||||||
let lower = generated_text.to_lowercase();
|
let lower = generated_text.to_lowercase();
|
||||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") {
|
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
||||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] {
|
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
||||||
if let Some(pos) = generated_text.find(stop) {
|
if let Some(pos) = generated_text.find(stop) {
|
||||||
generated_text.truncate(pos);
|
generated_text.truncate(pos);
|
||||||
}
|
}
|
||||||
@@ -234,33 +234,50 @@ impl LlmEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin.
|
const LANG_TAGS: &[&str] = &[
|
||||||
|
"python", "py", "rust", "rs", "javascript", "js", "typescript", "ts",
|
||||||
|
"java", "kotlin", "scala", "go", "ruby", "rb", "php", "swift",
|
||||||
|
"c", "cpp", "c++", "c#", "csharp", "r", "sql", "bash", "sh", "zsh",
|
||||||
|
"html", "css", "json", "yaml", "yml", "toml", "xml", "markdown", "md",
|
||||||
|
"lua", "perl", "dart", "elixir", "haskell", "hs", "ocaml", "zig",
|
||||||
|
"plaintext", "text", "txt",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Siivoa mallin tuottama vastaus (prefill-yhteensopiva).
|
||||||
fn strip_markdown_wrapper(text: &str) -> String {
|
fn strip_markdown_wrapper(text: &str) -> String {
|
||||||
let text = text.trim();
|
let mut result = text.trim().to_string();
|
||||||
if let Some(start) = text.find("```") {
|
|
||||||
let after = &text[start + 3..];
|
// 1. Kielitunniste — VAIN tunnettu kieli
|
||||||
let code_start = after.find('\n').map(|i| i + 1).unwrap_or(0);
|
if let Some(nl) = result.find('\n') {
|
||||||
let code = &after[code_start..];
|
let first = result[..nl].trim().to_lowercase();
|
||||||
if let Some(end) = code.find("```") {
|
if LANG_TAGS.contains(&first.as_str()) {
|
||||||
return code[..end].trim().to_string();
|
result = result[nl + 1..].to_string();
|
||||||
}
|
}
|
||||||
return code.trim().to_string();
|
|
||||||
}
|
}
|
||||||
let mut result = text.to_string();
|
|
||||||
let lower = result.to_lowercase();
|
// 2. Sulkeva ``` — VAIN omalla rivillään lopussa
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Johdantolauseet
|
||||||
|
let lower = result.trim().to_lowercase();
|
||||||
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
||||||
if lower.starts_with(prefix) {
|
if lower.starts_with(prefix) {
|
||||||
if let Some(nl) = result.find('\n') {
|
if let Some(nl) = result.find('\n') { result = result[nl + 1..].to_string(); }
|
||||||
result = result[nl + 1..].to_string();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Selityskommentit alusta
|
||||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
let mut lines: Vec<&str> = result.trim().lines().collect();
|
||||||
while !lines.is_empty() {
|
while !lines.is_empty() {
|
||||||
let first = lines[0].trim();
|
let first = lines[0].trim();
|
||||||
let is_preamble = first.starts_with("# ")
|
let is_preamble = first.starts_with("# ") && !first.starts_with("#!")
|
||||||
&& !first.starts_with("#!")
|
|
||||||
&& (first.to_lowercase().contains("this is")
|
&& (first.to_lowercase().contains("this is")
|
||||||
|| first.to_lowercase().contains("simple")
|
|| first.to_lowercase().contains("simple")
|
||||||
|| first.to_lowercase().contains("program that")
|
|| first.to_lowercase().contains("program that")
|
||||||
|
|||||||
@@ -27,26 +27,42 @@ struct CachedModel {
|
|||||||
is_3b: bool,
|
is_3b: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin.
|
/// Tunnetut kielitunnisteet joita malli voi tuottaa prefill-backtickien jälkeen.
|
||||||
/// "Sure! Here is...\n```python\nprint('hi')\n```" → "print('hi')"
|
const LANG_TAGS: &[&str] = &[
|
||||||
|
"python", "py", "rust", "rs", "javascript", "js", "typescript", "ts",
|
||||||
|
"java", "kotlin", "scala", "go", "ruby", "rb", "php", "swift",
|
||||||
|
"c", "cpp", "c++", "c#", "csharp", "r", "sql", "bash", "sh", "zsh",
|
||||||
|
"html", "css", "json", "yaml", "yml", "toml", "xml", "markdown", "md",
|
||||||
|
"lua", "perl", "dart", "elixir", "haskell", "hs", "ocaml", "zig",
|
||||||
|
"plaintext", "text", "txt",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Siivoa mallin tuottama vastaus.
|
||||||
|
/// Prefill-tekniikan vuoksi malli tuottaa: "rust\nfn main() {...}\n```"
|
||||||
|
/// eli kielitunniste alussa + sulkeva ``` lopussa. Molemmat poistetaan.
|
||||||
fn strip_markdown_wrapper(text: &str) -> String {
|
fn strip_markdown_wrapper(text: &str) -> String {
|
||||||
let text = text.trim();
|
let mut result = text.trim().to_string();
|
||||||
// Jos vastaus sisältää ```-koodiblokin, ota vain sen sisältö
|
|
||||||
if let Some(start) = text.find("```") {
|
// 1. Poistetaan kielitunniste ensimmäiseltä riviltä — VAIN jos se on tunnettu kieli
|
||||||
let after_backticks = &text[start + 3..];
|
if let Some(first_newline) = result.find('\n') {
|
||||||
// Ohita mahdollinen kielitunniste (```python, ```rust jne.)
|
let first_line = result[..first_newline].trim().to_lowercase();
|
||||||
let code_start = after_backticks.find('\n').map(|i| i + 1).unwrap_or(0);
|
if LANG_TAGS.contains(&first_line.as_str()) {
|
||||||
let code = &after_backticks[code_start..];
|
result = result[first_newline + 1..].to_string();
|
||||||
// Etsi sulkeva ```
|
|
||||||
if let Some(end) = code.find("```") {
|
|
||||||
return code[..end].trim().to_string();
|
|
||||||
}
|
}
|
||||||
// Ei sulkevaa ``` — ota kaikki loput
|
|
||||||
return code.trim().to_string();
|
|
||||||
}
|
}
|
||||||
// Ei koodiblokkia — poista yleiset johdantolauseet ja selityskommentit alusta
|
|
||||||
let mut result = text.to_string();
|
// 2. Poistetaan sulkeva ``` VAIN jos se on omalla rivillään lopussa
|
||||||
let lower = result.to_lowercase();
|
let trimmed = result.trim_end();
|
||||||
|
if trimmed.ends_with("```") {
|
||||||
|
let before = &trimmed[..trimmed.len() - 3];
|
||||||
|
// Varmistetaan: edellinen merkki on rivinvaihto tai alku (eli ``` on oma rivinsä)
|
||||||
|
if before.is_empty() || before.ends_with('\n') {
|
||||||
|
result = before.trim_end().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Poistetaan johdantolauseet: "Sure! Here is...", "Certainly!" jne.
|
||||||
|
let lower = result.trim().to_lowercase();
|
||||||
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
|
||||||
if lower.starts_with(prefix) {
|
if lower.starts_with(prefix) {
|
||||||
if let Some(newline) = result.find('\n') {
|
if let Some(newline) = result.find('\n') {
|
||||||
@@ -55,12 +71,12 @@ fn strip_markdown_wrapper(text: &str) -> String {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Poistetaan alun selityskommentit: "# This is a simple..." -tyyppiset rivit
|
|
||||||
// jotka eivät ole osa varsinaista koodia (esim. shebangia #! pidetään)
|
// 4. Poistetaan selityskommentit alusta: "# This is a simple program..."
|
||||||
let mut lines: Vec<&str> = result.trim().lines().collect();
|
let mut lines: Vec<&str> = result.trim().lines().collect();
|
||||||
while !lines.is_empty() {
|
while !lines.is_empty() {
|
||||||
let first = lines[0].trim();
|
let first = lines[0].trim();
|
||||||
let is_preamble_comment = first.starts_with("# ")
|
let is_preamble = first.starts_with("# ")
|
||||||
&& !first.starts_with("#!")
|
&& !first.starts_with("#!")
|
||||||
&& (first.to_lowercase().contains("this is")
|
&& (first.to_lowercase().contains("this is")
|
||||||
|| first.to_lowercase().contains("simple")
|
|| first.to_lowercase().contains("simple")
|
||||||
@@ -68,12 +84,9 @@ fn strip_markdown_wrapper(text: &str) -> String {
|
|||||||
|| first.to_lowercase().contains("here is")
|
|| first.to_lowercase().contains("here is")
|
||||||
|| first.to_lowercase().contains("the following")
|
|| first.to_lowercase().contains("the following")
|
||||||
|| first.to_lowercase().contains("below"));
|
|| first.to_lowercase().contains("below"));
|
||||||
if is_preamble_comment {
|
if is_preamble { lines.remove(0); } else { break; }
|
||||||
lines.remove(0);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.join("\n").trim().to_string()
|
lines.join("\n").trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,13 +258,13 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&prompt) {
|
||||||
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
|
||||||
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
let s = json.get("system").and_then(|v| v.as_str()).unwrap_or(default_system).to_string();
|
||||||
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(128) as usize;
|
let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(256) as usize;
|
||||||
(p, s, m)
|
(p, s, m)
|
||||||
} else {
|
} else {
|
||||||
(prompt.clone(), default_system.to_string(), 128)
|
(prompt.clone(), default_system.to_string(), 256)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(prompt.clone(), default_system.to_string(), 128)
|
(prompt.clone(), default_system.to_string(), 256)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
// Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
|
||||||
@@ -329,8 +342,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
|
|
||||||
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
// Stop-sekvenssit: katkaistaan kun malli alkaa selittää
|
||||||
let lower = generated_text.to_lowercase();
|
let lower = generated_text.to_lowercase();
|
||||||
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") {
|
if lower.contains("\n###") || lower.contains("\nexplanation") || lower.contains("\nnote:") || lower.contains("\noutput:") || lower.contains("\n```\n\n") || lower.contains("\n// example") || lower.contains("\n# example") {
|
||||||
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"] {
|
for stop in &["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n", "\n// Example", "\n// example", "\n# Example", "\n# example"] {
|
||||||
if let Some(pos) = generated_text.find(stop) {
|
if let Some(pos) = generated_text.find(stop) {
|
||||||
generated_text.truncate(pos);
|
generated_text.truncate(pos);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1089,17 +1089,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="agent-hub-status" title="WebSocket-yhteys Kipinä Hubiin — hallitsee tehtävien jakelun ja solmujen koordinoinnin" style="margin-top:20px;padding:8px 14px;background:#0d1117;border:1px solid var(--border-color);border-radius:6px 6px 0 0;font-family:'Courier New',monospace;font-size:13px;display:flex;align-items:center;gap:8px;cursor:help">
|
<div id="agent-hub-status" style="margin-top:20px;padding:8px 14px;background:#0d1117;border:1px solid var(--border-color);border-radius:6px 6px 0 0;font-family:'Courier New',monospace;font-size:13px;display:flex;align-items:center;gap:12px;cursor:help" title="WebSocket-yhteys Kipinä Hubiin — hallitsee tehtävien jakelun ja solmujen koordinoinnin">
|
||||||
<span id="agent-hub-dot" style="width:8px;height:8px;border-radius:50%;background:#d29922;display:inline-block"></span>
|
<span style="display:flex;align-items:center;gap:6px" title="Hub-yhteyden tila">
|
||||||
<span style="color:#8b949e">Hub:</span>
|
<span id="agent-hub-dot" style="width:8px;height:8px;border-radius:50%;background:#d29922;display:inline-block"></span>
|
||||||
<span id="agent-hub-label" style="color:#d29922">Yhdistetään...</span>
|
<span style="color:#8b949e">Hub:</span>
|
||||||
|
<span id="agent-hub-label" style="color:#d29922">Yhdistetään...</span>
|
||||||
|
</span>
|
||||||
|
<span style="color:#30363d">│</span>
|
||||||
|
<span style="display:flex;align-items:center;gap:6px" id="agent-compute-wrap">
|
||||||
|
<span id="agent-compute-dot" style="width:8px;height:8px;border-radius:50%;background:#30363d;display:inline-block"></span>
|
||||||
|
<span style="color:#8b949e">Laskenta:</span>
|
||||||
|
<span id="agent-compute-label" style="color:#8b949e">—</span>
|
||||||
|
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
|
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
|
<div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
|
||||||
<span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span>
|
<span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span>
|
||||||
<input id="term-input" type="text" placeholder="kpn run coder "kirjoita hello world"" spellcheck="false"
|
<input id="term-input" type="text" placeholder="kpn run coder "kirjoita hello world"" spellcheck="false" autocomplete="off"
|
||||||
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
|
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
|
||||||
|
<div id="term-dropdown" style="display:none;position:absolute;bottom:100%;left:30px;background:#161b22;border:1px solid #30363d;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)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /panel-agents -->
|
</div><!-- /panel-agents -->
|
||||||
@@ -1448,6 +1458,12 @@
|
|||||||
selected_task: viewTask,
|
selected_task: viewTask,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codelab: käynnistetään oma laskentasolmu automaattisesti
|
||||||
|
// Agents: käyttäjä käynnistää itse "Alusta laskentasolmu" -napista
|
||||||
|
if (tab === 'codelab') {
|
||||||
|
if (typeof ensureCoderNode === 'function') ensureCoderNode();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// URL-hash navigointi
|
// URL-hash navigointi
|
||||||
@@ -1475,12 +1491,19 @@
|
|||||||
let detectedWebGPU = false;
|
let detectedWebGPU = false;
|
||||||
let detectedGpuInfo = null;
|
let detectedGpuInfo = null;
|
||||||
let wasmInitialized = false;
|
let wasmInitialized = false;
|
||||||
let coderSize = '05b'; // '05b' tai '3b'
|
let coderSize = localStorage.getItem('kpn-coder-size') || '05b';
|
||||||
|
|
||||||
// Mallivalinnan radio-napit
|
// Mallivalinnan radio-napit — asetetaan oikea valinta localStoragesta
|
||||||
|
const savedRadio = document.querySelector(`input[name="coder-size"][value="${coderSize}"]`);
|
||||||
|
if (savedRadio) savedRadio.checked = true;
|
||||||
|
if (coderSize === '3b') {
|
||||||
|
document.getElementById('coder-opt-05b')?.style && (document.getElementById('coder-opt-05b').style.borderColor = 'var(--border-color)');
|
||||||
|
document.getElementById('coder-opt-3b')?.style && (document.getElementById('coder-opt-3b').style.borderColor = 'var(--accent-color)');
|
||||||
|
}
|
||||||
document.querySelectorAll('input[name="coder-size"]').forEach(radio => {
|
document.querySelectorAll('input[name="coder-size"]').forEach(radio => {
|
||||||
radio.addEventListener('change', (e) => {
|
radio.addEventListener('change', (e) => {
|
||||||
coderSize = e.target.value;
|
coderSize = e.target.value;
|
||||||
|
localStorage.setItem('kpn-coder-size', coderSize);
|
||||||
// Visuaalinen korostus
|
// Visuaalinen korostus
|
||||||
document.getElementById('coder-opt-05b').style.borderColor = coderSize === '05b' ? 'var(--accent-color)' : 'var(--border-color)';
|
document.getElementById('coder-opt-05b').style.borderColor = coderSize === '05b' ? 'var(--accent-color)' : 'var(--border-color)';
|
||||||
document.getElementById('coder-opt-3b').style.borderColor = coderSize === '3b' ? 'var(--accent-color)' : 'var(--border-color)';
|
document.getElementById('coder-opt-3b').style.borderColor = coderSize === '3b' ? 'var(--accent-color)' : 'var(--border-color)';
|
||||||
@@ -1820,6 +1843,7 @@
|
|||||||
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
|
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
|
||||||
termLog(' kpn run <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff');
|
termLog(' kpn run <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff');
|
||||||
termLog(' kpn pipeline "<tehtävä>" — manageri → koodari → testaaja', '#a5d6ff');
|
termLog(' kpn pipeline "<tehtävä>" — manageri → koodari → testaaja', '#a5d6ff');
|
||||||
|
termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff');
|
||||||
termLog(' kpn status — verkon tila', '#a5d6ff');
|
termLog(' kpn status — verkon tila', '#a5d6ff');
|
||||||
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
|
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
|
||||||
termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff');
|
termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff');
|
||||||
@@ -1831,6 +1855,41 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sub === 'load') {
|
||||||
|
const arg = parts[2];
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
// Mallikatalogista valinta numerolla tai nimellä
|
||||||
|
const loadModels = [
|
||||||
|
{ id: '1', key: '05b', name: 'Qwen2.5-Coder:0.5B', size: '~990 MB', coderSize: '05b' },
|
||||||
|
{ id: '2', key: '3b', name: 'Qwen2.5-Coder:3B', size: '~6.2 GB', coderSize: '3b' },
|
||||||
|
];
|
||||||
|
if (!arg) {
|
||||||
|
// Näytetään lista
|
||||||
|
termLog(' Ladattavat mallit:', '#c9d1d9');
|
||||||
|
for (const m of loadModels) {
|
||||||
|
const active = (btn?.dataset.state === 'ready' && coderSize === m.coderSize) ? ' <span style="color:#3fb950">✓ ladattu</span>' : '';
|
||||||
|
termLog(` <span style="color:#58a6ff">${m.id}</span> ${m.name} <span style="color:#8b949e">(${m.size})</span>${active}`);
|
||||||
|
}
|
||||||
|
termLog(' Käyttö: kpn load <numero>', '#8b949e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = loadModels.find(m => m.id === arg || m.key === arg || m.coderSize === arg);
|
||||||
|
if (!selected) {
|
||||||
|
termLog(` Tuntematon malli "${esc(arg)}". Kokeile: kpn load`, '#f85149');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn?.dataset.state === 'ready' && coderSize === selected.coderSize) {
|
||||||
|
termLog(` ✓ ${selected.name} on jo ladattu ja valmis`, '#3fb950');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coderSize = selected.coderSize;
|
||||||
|
localStorage.setItem('kpn-coder-size', coderSize);
|
||||||
|
termLog(` Alustetaan ${selected.name} (${selected.size})...`, '#d29922');
|
||||||
|
if (btn) btn.click();
|
||||||
|
else ensureCoderNode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sub === 'status') {
|
if (sub === 'status') {
|
||||||
const nodes = statNodes.textContent || '0';
|
const nodes = statNodes.textContent || '0';
|
||||||
const vram = statVram.textContent || '?';
|
const vram = statVram.textContent || '?';
|
||||||
@@ -1839,11 +1898,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'models') {
|
if (sub === 'models') {
|
||||||
termLog(' smollm-135m — SmolLM 135M (kevyt)', '#a5d6ff');
|
termLog(' Käytettävissä olevat mallit:', '#c9d1d9');
|
||||||
termLog(' qwen-05b — Qwen2.5 0.5B', '#a5d6ff');
|
termLog(' <span style="color:#58a6ff">1</span> qwen-coder Qwen2.5-Coder:0.5B <span style="color:#8b949e">~990 MB | koodin generointi</span>');
|
||||||
termLog(' phi3-mini — Phi-3 Mini', '#a5d6ff');
|
termLog(' <span style="color:#58a6ff">2</span> qwen-coder-3b Qwen2.5-Coder:3B <span style="color:#8b949e">~6.2 GB | parempi koodinlaatu</span>');
|
||||||
termLog(' qwen-coder — Qwen2.5-Coder 0.5B', '#a5d6ff');
|
termLog(' <span style="color:#58a6ff">3</span> smollm-135m SmolLM 135M <span style="color:#8b949e">~270 MB | kevyt, nopea</span>');
|
||||||
termLog(' qwen-coder-3b — Qwen2.5-Coder 3B', '#a5d6ff');
|
termLog(' <span style="color:#58a6ff">4</span> qwen-05b Qwen2.5:0.5B <span style="color:#8b949e">~990 MB | yleismalli</span>');
|
||||||
|
termLog(' <span style="color:#58a6ff">5</span> phi3-mini Phi-3 Mini <span style="color:#8b949e">~2.2 GB | Microsoftin malli</span>');
|
||||||
|
termLog(' Käyttö: kpn run <malli> "<prompti>"', '#8b949e');
|
||||||
|
termLog(' Lataus: kpn load <numero>', '#8b949e');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1876,7 +1938,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Jos käyttäjä syötti agentin nimen (esim. "coder"), vaihdetaan se oikeaksi tekoälymalliksi ("qwen-coder")
|
// Jos käyttäjä syötti agentin nimen (esim. "coder"), vaihdetaan se oikeaksi tekoälymalliksi ("qwen-coder")
|
||||||
if (agentPrompts[model]) {
|
if (model === 'coder-3b') {
|
||||||
|
model = 'qwen-coder-3b';
|
||||||
|
} else if (agentPrompts[model]) {
|
||||||
model = agentPrompts[model].model;
|
model = agentPrompts[model].model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1887,18 +1951,203 @@
|
|||||||
termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149');
|
termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab-completion: ennustava komennonsyöttö sana kerrallaan
|
||||||
|
const kpnCommands = {
|
||||||
|
'kpn': ['help', 'run', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
|
||||||
|
'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
|
||||||
|
'kpn load': ['1', '2'],
|
||||||
|
'kpn pipeline': ['"'],
|
||||||
|
};
|
||||||
|
// Esimerkkipromptit malleittain
|
||||||
|
const kpnExamples = {
|
||||||
|
'kpn run coder': ['"hello world in python"', '"fibonacci in rust"', '"quicksort in javascript"'],
|
||||||
|
'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'],
|
||||||
|
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
|
||||||
|
'kpn run tester': ['"testaa login-toiminto"'],
|
||||||
|
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function tabComplete(input) {
|
||||||
|
const val = input.value;
|
||||||
|
const words = val.trimEnd().split(/\s+/);
|
||||||
|
|
||||||
|
// Etsitään sopiva täydennystaso
|
||||||
|
// "kpn" → "kpn " alikomennot, "kpn run" → mallit, "kpn run coder" → prompti
|
||||||
|
for (let depth = words.length; depth >= 1; depth--) {
|
||||||
|
const prefix = words.slice(0, depth).join(' ');
|
||||||
|
const partial = words[depth] || '';
|
||||||
|
|
||||||
|
// Tarkistetaan esimerkkipromptit ensin
|
||||||
|
if (kpnExamples[prefix] && !partial) {
|
||||||
|
const example = kpnExamples[prefix][Math.floor(Math.random() * kpnExamples[prefix].length)];
|
||||||
|
input.value = prefix + ' ' + example;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komentojen täydennys
|
||||||
|
const candidates = kpnCommands[prefix];
|
||||||
|
if (candidates) {
|
||||||
|
const matches = partial
|
||||||
|
? candidates.filter(c => c.startsWith(partial))
|
||||||
|
: candidates;
|
||||||
|
if (matches.length === 1) {
|
||||||
|
words[depth] = matches[0];
|
||||||
|
input.value = words.slice(0, depth + 1).join(' ') + ' ';
|
||||||
|
return true;
|
||||||
|
} else if (matches.length > 1 && !partial) {
|
||||||
|
input.value = prefix + ' ' + matches[0];
|
||||||
|
return true;
|
||||||
|
} else if (matches.length > 1) {
|
||||||
|
// Yhteinen etuliite
|
||||||
|
let common = matches[0];
|
||||||
|
for (const m of matches) {
|
||||||
|
while (!m.startsWith(common)) common = common.slice(0, -1);
|
||||||
|
}
|
||||||
|
if (common.length > partial.length) {
|
||||||
|
words[depth] = common;
|
||||||
|
input.value = words.slice(0, depth + 1).join(' ');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tyhjä input → "kpn "
|
||||||
|
if (!val.trim()) {
|
||||||
|
input.value = 'kpn ';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown-autocompletionin tila
|
||||||
|
const dropdown = document.getElementById('term-dropdown');
|
||||||
|
let dropdownItems = [];
|
||||||
|
let dropdownIdx = -1;
|
||||||
|
let dropdownPrefix = ''; // Inputin alku joka säilyy valinnan yhteydessä
|
||||||
|
|
||||||
|
function getCandidates(val) {
|
||||||
|
const words = val.trimEnd().split(/\s+/);
|
||||||
|
for (let depth = words.length; depth >= 1; depth--) {
|
||||||
|
const prefix = words.slice(0, depth).join(' ');
|
||||||
|
const partial = words[depth] || '';
|
||||||
|
// Esimerkkipromptit
|
||||||
|
if (kpnExamples[prefix] && !partial) {
|
||||||
|
return { items: kpnExamples[prefix], prefix: prefix + ' ' };
|
||||||
|
}
|
||||||
|
// Komennot
|
||||||
|
const candidates = kpnCommands[prefix];
|
||||||
|
if (candidates) {
|
||||||
|
const matches = partial ? candidates.filter(c => c.startsWith(partial)) : candidates;
|
||||||
|
if (matches.length > 0) {
|
||||||
|
return { items: matches, prefix: prefix + ' ' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' };
|
||||||
|
return { items: [], prefix: val };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDropdown(items, prefix) {
|
||||||
|
if (!dropdown || items.length === 0) { hideDropdown(); return; }
|
||||||
|
dropdownItems = items;
|
||||||
|
dropdownPrefix = prefix;
|
||||||
|
dropdownIdx = -1;
|
||||||
|
dropdown.innerHTML = items.map((item, i) =>
|
||||||
|
`<div class="term-dd-item" data-idx="${i}" style="padding:6px 12px;cursor:pointer;color:#c9d1d9;white-space:nowrap;border-bottom:1px solid #21262d">${esc(item)}</div>`
|
||||||
|
).join('');
|
||||||
|
dropdown.style.display = 'block';
|
||||||
|
|
||||||
|
// Klikkaus-handlerit
|
||||||
|
dropdown.querySelectorAll('.term-dd-item').forEach(el => {
|
||||||
|
el.addEventListener('mouseenter', () => highlightDropdown(parseInt(el.dataset.idx)));
|
||||||
|
el.addEventListener('click', () => { selectDropdown(); termInput.focus(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDropdown() {
|
||||||
|
if (dropdown) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; }
|
||||||
|
dropdownItems = [];
|
||||||
|
dropdownIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightDropdown(idx) {
|
||||||
|
dropdownIdx = idx;
|
||||||
|
dropdown.querySelectorAll('.term-dd-item').forEach((el, i) => {
|
||||||
|
el.style.background = i === idx ? '#30363d' : 'transparent';
|
||||||
|
el.style.color = i === idx ? '#58a6ff' : '#c9d1d9';
|
||||||
|
});
|
||||||
|
// Varmistetaan näkyvyys
|
||||||
|
const active = dropdown.children[idx];
|
||||||
|
if (active) active.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDropdown() {
|
||||||
|
if (dropdownIdx >= 0 && dropdownIdx < dropdownItems.length) {
|
||||||
|
termInput.value = dropdownPrefix + dropdownItems[dropdownIdx] + (dropdownItems[dropdownIdx].startsWith('"') ? '' : ' ');
|
||||||
|
}
|
||||||
|
hideDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
termInput?.addEventListener('keydown', (e) => {
|
termInput?.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
// Dropdown auki: nuolet navigoi, Enter/Tab valitsee, Esc sulkee
|
||||||
|
if (dropdown && dropdown.style.display === 'block') {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightDropdown(Math.min(dropdownIdx + 1, dropdownItems.length - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightDropdown(Math.max(dropdownIdx - 1, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((e.key === 'Enter' || e.key === 'Tab') && dropdownIdx >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
hideDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Tab' && e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
hideDropdown();
|
||||||
|
const val = termInput.value.trimEnd();
|
||||||
|
if (!val) return;
|
||||||
|
const quoteMatch = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/);
|
||||||
|
if (quoteMatch) {
|
||||||
|
termInput.value = (quoteMatch[1] || quoteMatch[2]).trimEnd() + ' ';
|
||||||
|
} else {
|
||||||
|
const lastSpace = val.lastIndexOf(' ');
|
||||||
|
termInput.value = lastSpace > 0 ? val.substring(0, lastSpace + 1) : '';
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Näytä dropdown tai täydennä jos vain yksi vaihtoehto
|
||||||
|
const { items, prefix } = getCandidates(termInput.value);
|
||||||
|
if (items.length === 1) {
|
||||||
|
termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' ');
|
||||||
|
hideDropdown();
|
||||||
|
} else if (items.length > 1) {
|
||||||
|
showDropdown(items, prefix);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
hideDropdown();
|
||||||
const cmd = termInput.value.trim();
|
const cmd = termInput.value.trim();
|
||||||
if (cmd) termExec(cmd);
|
if (cmd) termExec(cmd);
|
||||||
termInput.value = '';
|
termInput.value = '';
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp' && !dropdown?.style.display?.includes('block')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (termHistIdx < termHistory.length - 1) {
|
if (termHistIdx < termHistory.length - 1) {
|
||||||
termHistIdx++;
|
termHistIdx++;
|
||||||
termInput.value = termHistory[termHistIdx];
|
termInput.value = termHistory[termHistIdx];
|
||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown' && !dropdown?.style.display?.includes('block')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (termHistIdx > 0) {
|
if (termHistIdx > 0) {
|
||||||
termHistIdx--;
|
termHistIdx--;
|
||||||
@@ -1910,6 +2159,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Suljetaan dropdown kun klikataan muualle
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
// Klikkaa terminaalipaneelia → fokusoi input
|
// Klikkaa terminaalipaneelia → fokusoi input
|
||||||
termPanel?.addEventListener('click', () => termInput?.focus());
|
termPanel?.addEventListener('click', () => termInput?.focus());
|
||||||
|
|
||||||
@@ -2229,7 +2483,8 @@
|
|||||||
: `→ ${msg} — generoidaan...`;
|
: `→ ${msg} — generoidaan...`;
|
||||||
}
|
}
|
||||||
} else if (data.type === "llm_prompt") {
|
} else if (data.type === "llm_prompt") {
|
||||||
if (data.task_id) {
|
// Reagoidaan VAIN agents-pipelinen tehtäviin (task_id + activeStreams)
|
||||||
|
if (data.task_id && activeStreams[data.task_id]) {
|
||||||
const term = document.getElementById('agent-terminal');
|
const term = document.getElementById('agent-terminal');
|
||||||
if (term) {
|
if (term) {
|
||||||
const model = data.model || 'llm';
|
const model = data.model || 'llm';
|
||||||
@@ -2241,26 +2496,26 @@
|
|||||||
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
|
while (term.children.length > 50 && !term.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
|
||||||
term.scrollTop = term.scrollHeight;
|
term.scrollTop = term.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avatar-aktivointi vain omille tehtäville
|
||||||
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
|
const model = data.model || '';
|
||||||
|
const p = data.prompt ? data.prompt.toLowerCase() : '';
|
||||||
|
|
||||||
|
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
|
||||||
|
document.getElementById('avatar-kpn')?.classList.add('active');
|
||||||
|
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
|
||||||
|
document.getElementById('avatar-tester')?.classList.add('active');
|
||||||
|
} else if (p.includes('tervehdi')) {
|
||||||
|
document.getElementById('avatar-client')?.classList.add('active');
|
||||||
|
} else if (p.includes('test')) {
|
||||||
|
document.getElementById('avatar-qa')?.classList.add('active');
|
||||||
|
} else if (model.includes('coder') || model.includes('Coder')) {
|
||||||
|
document.getElementById('avatar-coder')?.classList.add('active');
|
||||||
|
} else if (model.includes('deepseek') || model.includes('r1')) {
|
||||||
|
document.getElementById('avatar-observer')?.classList.add('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
|
||||||
const model = data.model || '';
|
|
||||||
const p = data.prompt ? data.prompt.toLowerCase() : '';
|
|
||||||
|
|
||||||
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
|
|
||||||
document.getElementById('avatar-kpn')?.classList.add('active');
|
|
||||||
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
|
|
||||||
document.getElementById('avatar-tester')?.classList.add('active');
|
|
||||||
} else if (p.includes('tervehdi')) {
|
|
||||||
document.getElementById('avatar-client')?.classList.add('active');
|
|
||||||
} else if (p.includes('test')) {
|
|
||||||
document.getElementById('avatar-qa')?.classList.add('active');
|
|
||||||
} else if (model.includes('coder') || model.includes('Coder')) {
|
|
||||||
// Koodari aktivoituu, jos kyse on suoraan koodarille osoitetusta mallitehtävästä (esim. network task)
|
|
||||||
document.getElementById('avatar-coder')?.classList.add('active');
|
|
||||||
} else if (model.includes('deepseek') || model.includes('r1')) {
|
|
||||||
document.getElementById('avatar-observer')?.classList.add('active');
|
|
||||||
}
|
|
||||||
// Emme enää aseta oletusagenttia, jottei tuntemattomissa verkkopyynnöissä mikään turhaan hypi silmille.
|
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
@@ -2494,20 +2749,42 @@
|
|||||||
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('löytyi')) { setStep('step-model', 'done', 'cache'); }
|
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('löytyi')) { setStep('step-model', 'done', 'cache'); }
|
||||||
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('tallennettu')) { setStep('step-model', 'done', '100%'); }
|
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('tallennettu')) { setStep('step-model', 'done', '100%'); }
|
||||||
if (msg.includes('[Coder]') && msg.includes('Rakennetaan')) { setStep('step-build', 'active'); }
|
if (msg.includes('[Coder]') && msg.includes('Rakennetaan')) { setStep('step-build', 'active'); }
|
||||||
|
if (msg.includes('Agent Node käynnistyy') || msg.includes('Rakennetaan')) {
|
||||||
|
const cd = document.getElementById('agent-compute-dot');
|
||||||
|
const cl = document.getElementById('agent-compute-label');
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
if (cd) cd.style.background = '#d29922';
|
||||||
|
if (cl) { cl.textContent = 'Ladataan...'; cl.style.color = '#d29922'; }
|
||||||
|
if (btn && btn.dataset.state !== 'ready') {
|
||||||
|
btn.dataset.state = 'loading';
|
||||||
|
btn.textContent = 'Peruuta';
|
||||||
|
btn.style.borderColor = '#f85149';
|
||||||
|
btn.style.color = '#f85149';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
|
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
|
||||||
// Malli on valmis — merkataan kaikki vaiheet valmiiksi
|
// Malli on valmis — merkataan kaikki vaiheet valmiiksi
|
||||||
setStep('step-wasm', 'done');
|
setStep('step-wasm', 'done');
|
||||||
setStep('step-tokenizer', 'done');
|
setStep('step-tokenizer', 'done');
|
||||||
|
|
||||||
const pctSpan = document.getElementById('step-model-pct');
|
const pctSpan = document.getElementById('step-model-pct');
|
||||||
if (pctSpan && pctSpan.textContent.includes('100%')) {
|
if (pctSpan && pctSpan.textContent.includes('100%')) {
|
||||||
setStep('step-model', 'done', '100%');
|
setStep('step-model', 'done', '100%');
|
||||||
} else {
|
} else {
|
||||||
setStep('step-model', 'done', 'cache');
|
setStep('step-model', 'done', 'cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
setStep('step-build', 'done');
|
setStep('step-build', 'done');
|
||||||
setStep('step-ready', 'done');
|
setStep('step-ready', 'done');
|
||||||
|
|
||||||
|
// Agents-sivun compute-status: valmis
|
||||||
|
const cd = document.getElementById('agent-compute-dot');
|
||||||
|
const cl = document.getElementById('agent-compute-label');
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
if (cd) cd.style.background = '#3fb950';
|
||||||
|
if (cl) { cl.textContent = 'Qwen2.5-Coder'; cl.style.color = '#3fb950'; }
|
||||||
|
if (btn) { btn.dataset.state = 'ready'; btn.textContent = '✓ Valmis'; btn.style.borderColor = '#3fb950'; btn.style.color = '#3fb950'; btn.style.cursor = 'default'; btn.title = 'Kielimalli ladattu — oma kone on valmis laskentaan'; }
|
||||||
|
localStorage.setItem('kpn-coder-loaded', 'true');
|
||||||
}
|
}
|
||||||
if (msg.includes('[Coder]') && msg.includes('Syöte:')) {
|
if (msg.includes('[Coder]') && msg.includes('Syöte:')) {
|
||||||
// Pipeline piiloon kun generointi alkaa
|
// Pipeline piiloon kun generointi alkaa
|
||||||
@@ -2594,6 +2871,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automaattinen uudelleenkäynnistys: jos malli oli ladattu ennen refreshiä, ladataan se uudelleen cachesta
|
||||||
|
if (localStorage.getItem('kpn-coder-loaded') === 'true') {
|
||||||
|
// Pieni viive jotta UI ehtii piirtyä
|
||||||
|
setTimeout(() => ensureCoderNode(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laskentasolmun käynnistys/pysäytys -nappi
|
||||||
|
let computeAbortController = null;
|
||||||
|
document.getElementById('agent-compute-btn')?.addEventListener('click', () => {
|
||||||
|
const btn = document.getElementById('agent-compute-btn');
|
||||||
|
const cl = document.getElementById('agent-compute-label');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
if (btn.dataset.state === 'ready') return; // Jo valmis, ei tehdä mitään
|
||||||
|
|
||||||
|
if (btn.dataset.state === 'loading') {
|
||||||
|
// Cancel — ladataan sivua uudelleen koska Wasm-latausta ei voi pysäyttää
|
||||||
|
btn.textContent = 'Peruutetaan...';
|
||||||
|
btn.disabled = true;
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Käynnistetään
|
||||||
|
btn.dataset.state = 'loading';
|
||||||
|
btn.textContent = 'Peruuta';
|
||||||
|
btn.style.borderColor = '#f85149';
|
||||||
|
btn.style.color = '#f85149';
|
||||||
|
btn.title = 'Peruuta kielimallin lataus';
|
||||||
|
ensureCoderNode();
|
||||||
|
});
|
||||||
|
|
||||||
// JSON mode toggle
|
// JSON mode toggle
|
||||||
const jsonToggle = document.getElementById('json-mode-toggle');
|
const jsonToggle = document.getElementById('json-mode-toggle');
|
||||||
const jsonHelp = document.getElementById('json-help');
|
const jsonHelp = document.getElementById('json-help');
|
||||||
|
|||||||
Reference in New Issue
Block a user