9 Commits

Author SHA1 Message Date
Jaakko Vanhala
6f14614af8 Syntaksikorostus agents-terminaalin ja network-näkymän LLM-vastauksiin
Highlight.js:n automaattinen kielentunnistus nyt myös agents-terminaalin
koodivastauksissa ja network-näkymän chatBoxissa (aiemmin vain codelabissa).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:28:24 +03:00
Jaakko Vanhala
518c6dc5cb Prefill-tekniikka: pakotetaan LLM-vastaus alkamaan suoraan koodilla
Assistantin vastauksen alkuun syötetään valmiiksi backtick-koodiblokki,
jolloin malli jatkaa suoraan koodilla eikä tuota "Sure! Here is..."
-johdantotekstejä. Säästää tokeneita ja vastausaikaa.
strip_markdown_wrapper poistaa ``` -merkit jälkikäteen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:27:28 +03:00
Jaakko Vanhala
b48eeb6f5f Poistetaan selityskommentit LLM-vastauksista: "# This is a simple program..." -tyyppiset rivit
Malli tuottaa toisinaan selityskommentin koodin alkuun ilman markdown-wrapperia.
Stripperi tunnistaa ja poistaa nämä avainsanojen perusteella (this is, simple,
program that, jne.) mutta säilyttää oikeat koodikommentit ja shebangin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:26:26 +03:00
Jaakko Vanhala
6bc7d03676 Reitityksen tilatieto UI:ssa: task_routed-viestit terminaaliin ja codelab-latausindikaattoriin
Näyttää "Reititetty solmulle #N" tai "Kaikki N solmua varattuja — odotetaan..."
sekä agents-terminaalissa että koodilaboratorion lataustekstissä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:25:43 +03:00
Jaakko Vanhala
13b2911d38 Tehtävien reitityksen tilatieto ja työjono: task_routed-viesti UI:lle, 30s jono kun solmut varattuja
Hub broadcastaa task_routed-viestin joka kertoo reitityksen tilan:
- "routed": vapaa solmu löytyi, tehtävä reititetty suoraan
- "queued": kaikki solmut varattuja, odotetaan vapautumista (max 30s poll)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:25:18 +03:00
Jaakko Vanhala
38054452e2 Pipeline-tilakone: matchaa myös [Storage]-prefiksin tokenizer-logit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:19:03 +03:00
Jaakko Vanhala
50ff34cb09 Highlight.js koodin syntaksikorostukseen (automaattinen kielentunnistus) + markdown-strippaus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 08:43:20 +03:00
Jaakko Vanhala
949f34833f Markdown-wrapper strippaus LLM-vastauksista + hub-status tooltip
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 08:41:23 +03:00
Jaakko Vanhala
88fd31ca8c Hub-yhteyden tila omaksi status-palkiksi terminaalin yläpuolelle (agents-näkymä)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 08:39:48 +03:00
4 changed files with 246 additions and 47 deletions

View File

@@ -972,24 +972,86 @@ async fn api_chat_completions(
} }
} }
// Etsitään ensimmäinen VAPAA solmu, joka vastaa pyydettyä mallia // Etsitään vapaa tai varattu solmu, joka vastaa pyydettyä mallia
let target_node = { let (target_node_free, target_node_any, total_matching) = {
let tasks = state.node_tasks.lock().unwrap(); let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap(); let busy = state.node_busy.lock().unwrap();
tasks.iter().find(|(node_id, task)| { let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
let model_match = if payload.model == "qwen-coder" { if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder" *task == "qwen-coder-05b" || *task == "qwen-coder"
} else { } else {
**task == payload.model **task == payload.model
}; }
model_match && !busy.contains(node_id) }).map(|(k, _)| *k).collect();
}).map(|(k, _)| *k) let free = matching.iter().find(|id| !busy.contains(id)).copied();
let any = matching.first().copied();
(free, any, matching.len())
}; };
let target_node_id = match target_node { // Broadcastataan reititystila UI:lle
Some(id) => id, let task_id = payload.task_id.clone();
None => {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei vapaata solmua tälle mallille (kaikki varattuja tai ei käynnissä)").into_response(); if target_node_any.is_none() {
// Ei yhtään solmua tälle mallille
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
let target_node_id;
if let Some(free_id) = target_node_free {
// Vapaa solmu löytyi — reititetään suoraan
target_node_id = free_id;
let node_type = if state.node_tasks.lock().unwrap().get(&free_id).map(|t| t.contains("native")).unwrap_or(false) { "natiivi" } else { "selain" };
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": free_id,
"node_type": node_type,
"status": "routed",
"message": format!("Reititetty solmulle #{}", free_id),
});
let _ = state.stats_tx.send(routing_msg.to_string());
} else {
// Kaikki solmut varattuja — odotetaan vapautumista (max 30s)
let queue_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"status": "queued",
"message": format!("Kaikki {} solmua varattuja — odotetaan vapautumista...", total_matching),
});
let _ = state.stats_tx.send(queue_msg.to_string());
// Pollaa busy-tilaa 500ms välein, max 30s
let mut waited = 0u32;
loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
waited += 500;
let free = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
tasks.iter().find(|(node_id, task)| {
let model_match = if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder"
} else {
**task == payload.model
};
model_match && !busy.contains(node_id)
}).map(|(k, _)| *k)
};
if let Some(id) = free {
target_node_id = id;
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": id,
"status": "routed",
"message": format!("Solmu #{} vapautui — reititetään ({:.1}s jonossa)", id, waited as f64 / 1000.0),
});
let _ = state.stats_tx.send(routing_msg.to_string());
break;
}
if waited >= 30000 {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Aikakatkaisu: kaikki solmut varattuja 30s ajan").into_response();
}
} }
}; };
@@ -1003,18 +1065,18 @@ async fn api_chat_completions(
"model": payload.model, "model": payload.model,
"task_id": payload.task_id, "task_id": payload.task_id,
}); });
// Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta) // Odotuskanava valmiiksi (solmu palauttaa tuloksen stats_tx kautta)
let mut rx = state.stats_tx.subscribe(); let mut rx = state.stats_tx.subscribe();
// Kohdennettu reititys: lähetetään AI-tehtävä suoraan VAIN valitulle solmulle (Reititysarkkitehtuuri) // Kohdennettu reititys: lähetetään AI-tehtävä suoraan VAIN valitulle solmulle
{ {
let channels = state.node_channels.read().await; let channels = state.node_channels.read().await;
if let Some(tx) = channels.get(&target_node_id) { if let Some(tx) = channels.get(&target_node_id) {
let _ = tx.send(msg.to_string()); let _ = tx.send(msg.to_string());
tracing::info!("Reititettiin API-pyyntö solmulle {} (Malli: {})", target_node_id, payload.model); tracing::info!("Reititettiin API-pyyntö solmulle {} (Malli: {})", target_node_id, payload.model);
} else { } else {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Verkkovirhe: solmun yhteys katkesi pyynnön aikana").into_response(); return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Verkkovirhe: solmun yhteys katkesi reitityksen aikana").into_response();
} }
} }

View File

@@ -124,7 +124,8 @@ impl LlmEngine {
} }
pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> { pub fn generate(&mut self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
let formatted = format!("<|im_start|>system\nYou are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt); // Prefill: aloitetaan vastaus ```-koodiblokkilla → malli jatkaa suoraan koodilla
let formatted = format!("<|im_start|>system\nYou are a coding assistant. Respond with ONLY code. No explanations, no markdown, no comments unless asked.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", prompt);
let encoding = self.tokenizer.encode(formatted.as_str(), true) let encoding = self.tokenizer.encode(formatted.as_str(), true)
.map_err(|e| format!("Encode: {}", e))?; .map_err(|e| format!("Encode: {}", e))?;
@@ -225,7 +226,7 @@ impl LlmEngine {
} else { 0.0 }; } else { 0.0 };
Ok(GenerateResult { Ok(GenerateResult {
text: generated_text, text: strip_markdown_wrapper(&generated_text),
tokens_generated, tokens_generated,
duration_ms: gen_time.as_millis() as f64, duration_ms: gen_time.as_millis() as f64,
tokens_per_sec, tokens_per_sec,
@@ -233,6 +234,44 @@ impl LlmEngine {
} }
} }
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin.
fn strip_markdown_wrapper(text: &str) -> String {
let text = text.trim();
if let Some(start) = text.find("```") {
let after = &text[start + 3..];
let code_start = after.find('\n').map(|i| i + 1).unwrap_or(0);
let code = &after[code_start..];
if let Some(end) = code.find("```") {
return code[..end].trim().to_string();
}
return code.trim().to_string();
}
let mut result = text.to_string();
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
break;
}
}
let mut lines: Vec<&str> = result.trim().lines().collect();
while !lines.is_empty() {
let first = lines[0].trim();
let is_preamble = first.starts_with("# ")
&& !first.starts_with("#!")
&& (first.to_lowercase().contains("this is")
|| first.to_lowercase().contains("simple")
|| first.to_lowercase().contains("program that")
|| first.to_lowercase().contains("here is")
|| first.to_lowercase().contains("the following")
|| first.to_lowercase().contains("below"));
if is_preamble { lines.remove(0); } else { break; }
}
lines.join("\n").trim().to_string()
}
pub struct GenerateResult { pub struct GenerateResult {
pub text: String, pub text: String,
pub tokens_generated: usize, pub tokens_generated: usize,

View File

@@ -27,6 +27,56 @@ struct CachedModel {
is_3b: bool, is_3b: bool,
} }
/// Poistaa mallin tuottaman markdown-wrapperin ja johdantotekstin.
/// "Sure! Here is...\n```python\nprint('hi')\n```" → "print('hi')"
fn strip_markdown_wrapper(text: &str) -> String {
let text = text.trim();
// Jos vastaus sisältää ```-koodiblokin, ota vain sen sisältö
if let Some(start) = text.find("```") {
let after_backticks = &text[start + 3..];
// Ohita mahdollinen kielitunniste (```python, ```rust jne.)
let code_start = after_backticks.find('\n').map(|i| i + 1).unwrap_or(0);
let code = &after_backticks[code_start..];
// 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();
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(newline) = result.find('\n') {
result = result[newline + 1..].to_string();
}
break;
}
}
// Poistetaan alun selityskommentit: "# This is a simple..." -tyyppiset rivit
// jotka eivät ole osa varsinaista koodia (esim. shebangia #! pidetään)
let mut lines: Vec<&str> = result.trim().lines().collect();
while !lines.is_empty() {
let first = lines[0].trim();
let is_preamble_comment = first.starts_with("# ")
&& !first.starts_with("#!")
&& (first.to_lowercase().contains("this is")
|| first.to_lowercase().contains("simple")
|| first.to_lowercase().contains("program that")
|| first.to_lowercase().contains("here is")
|| first.to_lowercase().contains("the following")
|| first.to_lowercase().contains("below"));
if is_preamble_comment {
lines.remove(0);
} else {
break;
}
}
lines.join("\n").trim().to_string()
}
thread_local! { thread_local! {
static RAM_CACHE: RefCell<std::collections::HashMap<String, Rc<Vec<u8>>>> = RefCell::new(std::collections::HashMap::new()); static RAM_CACHE: RefCell<std::collections::HashMap<String, Rc<Vec<u8>>>> = RefCell::new(std::collections::HashMap::new());
static MODEL_CACHE: RefCell<Option<CachedModel>> = RefCell::new(None); static MODEL_CACHE: RefCell<Option<CachedModel>> = RefCell::new(None);
@@ -204,7 +254,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
(prompt.clone(), default_system.to_string(), 128) (prompt.clone(), default_system.to_string(), 128)
}; };
let formatted = format!("<|im_start|>system\n{}<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", system_msg, actual_prompt); // Prefill: aloitetaan vastaus ```-koodiblokkilla, jolloin malli jatkaa suoraan koodilla
// eikä tuota "Sure! Here is..." -johdantoa. strip_markdown_wrapper poistaa ``` jälkikäteen.
let formatted = format!("<|im_start|>system\n{}<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n```\n", system_msg, actual_prompt);
// Inferenssi: käytetään välimuistissa olevaa mallia // Inferenssi: käytetään välimuistissa olevaa mallia
let (generated_text, tokens_generated, gen_time) = MODEL_CACHE.with(|cache| { let (generated_text, tokens_generated, gen_time) = MODEL_CACHE.with(|cache| {
@@ -295,7 +347,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
} }
let gen_time = perf.now() - start_gen; let gen_time = perf.now() - start_gen;
(generated_text, tokens_generated, gen_time)
// Siivotaan vastaus: poista markdown-koodiblokit ja johdantotekstit
let cleaned = strip_markdown_wrapper(&generated_text);
(cleaned, tokens_generated, gen_time)
}); });
let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 }; let tokens_per_sec = if gen_time > 0.0 { (tokens_generated as f64 / gen_time) * 1000.0 } else { 0.0 };

View File

@@ -4,6 +4,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kipinä Agentic Playground</title> <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 src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<style> <style>
:root { :root {
--bg-color: #0d1117; --bg-color: #0d1117;
@@ -134,15 +136,12 @@
padding: 14px; padding: 14px;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
color: var(--success-color);
white-space: pre-wrap; white-space: pre-wrap;
overflow-x: auto; overflow-x: auto;
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
} }
.code-output .keyword { color: #ff7b72; } .code-output .hljs { background: transparent; padding: 0; }
.code-output .string { color: #a5d6ff; }
.code-output .comment { color: #8b949e; }
.code-task-card { .code-task-card {
background: #0d1117; background: #0d1117;
@@ -1090,9 +1089,12 @@
</div> </div>
</div> </div>
<div class="terminal-panel" id="agent-terminal" style="margin-top: 20px;"> <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 class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div> <span id="agent-hub-dot" style="width:8px;height:8px;border-radius:50%;background:#d29922;display:inline-block"></span>
<div class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div> <span style="color:#8b949e">Hub:</span>
<span id="agent-hub-label" style="color:#d29922">Yhdistetään...</span>
</div>
<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="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>
@@ -1634,6 +1636,14 @@
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`); const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
window._uiSocket = uiSocket; window._uiSocket = uiSocket;
uiSocket.onopen = async () => { uiSocket.onopen = async () => {
// Päivitetään agents-näkymän hub-status
const hubDot = document.getElementById('agent-hub-dot');
const hubLabel = document.getElementById('agent-hub-label');
const hubStatus = document.getElementById('agent-hub-status');
if (hubDot) hubDot.style.background = '#3fb950';
if (hubLabel) { hubLabel.textContent = 'Yhdistetty'; hubLabel.style.color = '#3fb950'; }
if (hubStatus) hubStatus.title = 'Yhdistetty Kipinä Hubiin — tehtävien jakelu ja solmujen koordinointi aktiivinen';
// Päivitetään molemmat statukset // Päivitetään molemmat statukset
const el = document.getElementById('node-status'); const el = document.getElementById('node-status');
el.textContent = 'Connected'; el.textContent = 'Connected';
@@ -1681,6 +1691,13 @@
})); }));
}; };
uiSocket.onclose = () => { uiSocket.onclose = () => {
const hubDot = document.getElementById('agent-hub-dot');
const hubLabel = document.getElementById('agent-hub-label');
const hubStatus2 = document.getElementById('agent-hub-status');
if (hubDot) hubDot.style.background = '#f85149';
if (hubLabel) { hubLabel.textContent = 'Yhteys katkennut'; hubLabel.style.color = '#f85149'; }
if (hubStatus2) hubStatus2.title = 'WebSocket-yhteys hubiin katkesi — tarkista verkkoyhteytesi tai hubin tila. Lataa sivu uudelleen yhdistääksesi.';
const el = document.getElementById('node-status'); const el = document.getElementById('node-status');
el.textContent = 'Disconnected'; el.textContent = 'Disconnected';
el.style.color = '#f85149'; el.style.color = '#f85149';
@@ -1748,7 +1765,8 @@
const tokGen = data.tokens_generated || 0; const tokGen = data.tokens_generated || 0;
termLog(` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${data.model || model}</span> <span style="color:#8b949e">(${tokGen} tok)</span>`); termLog(` <span style="color:#3fb950">✓</span> <span style="color:#58a6ff">${data.model || model}</span> <span style="color:#8b949e">(${tokGen} tok)</span>`);
if (!silent) { if (!silent) {
termLog(` ${esc(response).replace(/\n/g,'\n ')}`, '#c9d1d9'); const highlighted = highlightCode(response).replace(/\n/g, '\n ');
termLog(` <pre style="margin:0;font:inherit;white-space:pre-wrap">${highlighted}</pre>`);
} }
return response; return response;
} catch (e) { } catch (e) {
@@ -2099,7 +2117,7 @@
Prompt: <span style="color:#d29922">"${esc(stripSystemPrompt(data.prompt))}"</span> Prompt: <span style="color:#d29922">"${esc(stripSystemPrompt(data.prompt))}"</span>
</div> </div>
<div style="font-size:14px;color:var(--text-color);line-height:1.5;${(model.includes('Coder') || (data.response||'').includes('def ')) ? 'font-family:Courier New,monospace;background:#010409;padding:10px;border-radius:4px;white-space:pre-wrap;font-size:12px' : ''}"> <div style="font-size:14px;color:var(--text-color);line-height:1.5;${(model.includes('Coder') || (data.response||'').includes('def ')) ? 'font-family:Courier New,monospace;background:#010409;padding:10px;border-radius:4px;white-space:pre-wrap;font-size:12px' : ''}">
${data.response ? esc(data.response) : '<em>tyhjä vastaus</em>'} ${data.response ? highlightCode(data.response) : '<em>tyhjä vastaus</em>'}
</div> </div>
<div style="margin-top:8px;font-size:12px;color:#8b949e"> <div style="margin-top:8px;font-size:12px;color:#8b949e">
${tokGen} tokenia generoitu | malli ladattu: ${typeof loadMs === 'number' ? loadMs.toFixed(0) : loadMs}ms ${tokGen} tokenia generoitu | malli ladattu: ${typeof loadMs === 'number' ? loadMs.toFixed(0) : loadMs}ms
@@ -2182,6 +2200,34 @@
targetBox.scrollTop = targetBox.scrollHeight; targetBox.scrollTop = targetBox.scrollHeight;
} }
} }
} else if (data.type === "task_routed") {
const term = document.getElementById('agent-terminal');
const isQueued = data.status === 'queued';
const color = isQueued ? '#d29922' : '#58a6ff';
const icon = isQueued ? '⏳' : '→';
const msg = esc(data.message || '');
// Agents-terminaali
if (term && data.task_id && activeStreams[data.task_id]) {
const div = document.createElement('div');
div.className = 'terminal-line';
div.style.color = color;
div.innerHTML = ` ${icon} ${msg}`;
if (isQueued) div.id = 'routing-' + data.task_id;
// Päivitetään olemassaoleva jonorivi jos löytyy
const existing = document.getElementById('routing-' + data.task_id);
if (existing) { existing.innerHTML = ` ${icon} ${msg}`; existing.style.color = color; }
else term.appendChild(div);
term.scrollTop = term.scrollHeight;
}
// Codelab-loading-teksti
const codeLoading = document.getElementById('code-loading');
if (codeLoading && codeLoading.style.display !== 'none') {
codeLoading.textContent = isQueued
? `${msg}`
: `${msg} — generoidaan...`;
}
} else if (data.type === "llm_prompt") { } else if (data.type === "llm_prompt") {
if (data.task_id) { if (data.task_id) {
const term = document.getElementById('agent-terminal'); const term = document.getElementById('agent-terminal');
@@ -2353,20 +2399,14 @@
let pendingCodePrompt = null; let pendingCodePrompt = null;
// Yksinkertainen Python-syntaksikorostus // Yksinkertainen Python-syntaksikorostus
function highlightPython(code) { function highlightCode(code) {
return code if (typeof hljs !== 'undefined') {
// Kommentit try {
.replace(/(#.*)/g, '<span style="color:#8b949e">$1</span>') const result = hljs.highlightAuto(code);
// Merkkijonot (f-stringit, tavalliset) return result.value;
.replace(/(f?"[^"]*"|f?'[^']*')/g, '<span style="color:#a5d6ff">$1</span>') } catch(e) {}
// Avainsanat }
.replace(/\b(def|return|if|elif|else|for|while|in|not|and|or|is|import|from|class|try|except|with|as|lambda|yield|True|False|None|raise|pass|break|continue)\b/g, '<span style="color:#ff7b72">$1</span>') return esc(code);
// Sisäänrakennetut funktiot
.replace(/\b(print|len|range|int|str|float|list|dict|set|tuple|type|isinstance|enumerate|zip|map|filter|sorted|reversed|sum|min|max|abs|round|input|open)\b/g, '<span style="color:#d2a8ff">$1</span>')
// Numerot
.replace(/\b(\d+\.?\d*)\b/g, '<span style="color:#79c0ff">$1</span>')
// Dekoraattorit
.replace(/(@\w+)/g, '<span style="color:#d2a8ff">$1</span>');
} }
function addCodeResult(data) { function addCodeResult(data) {
@@ -2399,7 +2439,7 @@
card.className = 'code-task-card'; card.className = 'code-task-card';
card.innerHTML = ` card.innerHTML = `
<div class="prompt">${esc(stripSystemPrompt(data.prompt))}</div> <div class="prompt">${esc(stripSystemPrompt(data.prompt))}</div>
<div class="code-output">${highlightPython(response)}</div> <div class="code-output">${highlightCode(response)}</div>
<div class="meta"> <div class="meta">
${model} · ${tokGen} tokenia · ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms · ${tokS} tok/s ${model} · ${tokGen} tokenia · ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms · ${tokS} tok/s
</div>`; </div>`;
@@ -2438,12 +2478,14 @@
const origCodeLog = console.log; const origCodeLog = console.log;
const codeLogListener = (...args) => { const codeLogListener = (...args) => {
const msg = args.join(' '); const msg = args.join(' ');
if (msg.includes('[Coder]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) { if (msg.includes('[Coder]') || msg.includes('[Storage]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) {
if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active'); if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active');
if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); } if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); }
if (msg.includes('[Coder]') && msg.includes('tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); } // Tokenizer: [Coder] tai [Storage] -prefiksi
if (msg.includes('[Coder]') && msg.includes('Ladataan') && msg.includes('tokenizer')) { setStep('step-tokenizer', 'active'); } if (msg.includes('Tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('[Coder]') && msg.includes('tokenizer') && msg.includes('tallennettu')) { setStep('step-tokenizer', 'done'); } if (msg.includes('tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); }
if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('Ladataan') && msg.includes('tokenizer')) { setStep('step-tokenizer', 'active'); }
if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('tokenizer') && msg.includes('tallennettu')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('lataus:')) { if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('lataus:')) {
setStep('step-model', 'active'); setStep('step-model', 'active');
const match = msg.match(/lataus: (\d+)%/); const match = msg.match(/lataus: (\d+)%/);