Compare commits
9 Commits
57c6506f91
...
6f14614af8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f14614af8 | ||
|
|
518c6dc5cb | ||
|
|
b48eeb6f5f | ||
|
|
6bc7d03676 | ||
|
|
13b2911d38 | ||
|
|
38054452e2 | ||
|
|
50ff34cb09 | ||
|
|
949f34833f | ||
|
|
88fd31ca8c |
@@ -972,8 +972,60 @@ 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 busy = state.node_busy.lock().unwrap();
|
||||||
|
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
|
||||||
|
if payload.model == "qwen-coder" {
|
||||||
|
*task == "qwen-coder-05b" || *task == "qwen-coder"
|
||||||
|
} else {
|
||||||
|
**task == payload.model
|
||||||
|
}
|
||||||
|
}).map(|(k, _)| *k).collect();
|
||||||
|
let free = matching.iter().find(|id| !busy.contains(id)).copied();
|
||||||
|
let any = matching.first().copied();
|
||||||
|
(free, any, matching.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcastataan reititystila UI:lle
|
||||||
|
let task_id = payload.task_id.clone();
|
||||||
|
|
||||||
|
if target_node_any.is_none() {
|
||||||
|
// Ei yhtään solmua tälle mallille
|
||||||
|
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_node_id;
|
||||||
|
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 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)| {
|
tasks.iter().find(|(node_id, task)| {
|
||||||
@@ -985,11 +1037,21 @@ async fn api_chat_completions(
|
|||||||
model_match && !busy.contains(node_id)
|
model_match && !busy.contains(node_id)
|
||||||
}).map(|(k, _)| *k)
|
}).map(|(k, _)| *k)
|
||||||
};
|
};
|
||||||
|
if let Some(id) = free {
|
||||||
let target_node_id = match target_node {
|
target_node_id = id;
|
||||||
Some(id) => id,
|
let routing_msg = serde_json::json!({
|
||||||
None => {
|
"type": "task_routed",
|
||||||
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei vapaata solmua tälle mallille (kaikki varattuja tai ei käynnissä)").into_response();
|
"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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1007,14 +1069,14 @@ async fn api_chat_completions(
|
|||||||
// 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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+)%/);
|
||||||
|
|||||||
Reference in New Issue
Block a user