hyvä siitä tulee
This commit is contained in:
@@ -152,6 +152,24 @@ impl NodeDb {
|
||||
conn.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub fn update_session_task(&self, node_id: u64, task: &str) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET selected_task = ?1 WHERE node_id = ?2 AND disconnected_at IS NULL",
|
||||
params![task, node_id as i64],
|
||||
);
|
||||
}
|
||||
|
||||
/// Sulkee saman IP:n viewer-sessiot kun aktiivinen node liittyy
|
||||
pub fn close_viewers_by_ip(&self, ip: &str) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"UPDATE node_sessions SET disconnected_at = ?1 WHERE ip = ?2 AND disconnected_at IS NULL AND (selected_task = 'viewer' OR selected_task = 'codelab-viewer')",
|
||||
params![now, ip],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn close_session(&self, node_id: u64) {
|
||||
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
@@ -25,7 +25,7 @@ const ALLOWED_ORIGINS: &[&str] = &[
|
||||
];
|
||||
|
||||
// Sallitut viestityyypit clientilta
|
||||
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "download_progress", "user_text"];
|
||||
const ALLOWED_MSG_TYPES: &[&str] = &["auth", "result", "pair_done", "llm_chunk", "llm_done", "download_progress", "user_text", "single_tokenize_done"];
|
||||
|
||||
struct AppState {
|
||||
next_node_id: Mutex<u64>,
|
||||
@@ -158,7 +158,7 @@ async function load() {
|
||||
].map(s => `<div class="stat-card"><div class="val">${s.v}</div><div class="label">${s.l}</div></div>`).join('');
|
||||
|
||||
// Sessions — lajittelu: 1) aktiiviset nodet (online + ei viewer), 2) katsojat (online + viewer), 3) offline
|
||||
const taskNames = {'tokenize':'Tokenisaatio','smollm-135m':'SmolLM 135M','qwen-05b':'Qwen2.5 0.5B','phi3-mini':'Phi-3 Mini','qwen-coder-05b':'Coder 0.5B','qwen-coder-3b':'Coder 3B','viewer':'Katsoja'};
|
||||
const taskNames = {'tokenize':'Tokenisaatio','smollm-135m':'SmolLM 135M','qwen-05b':'Qwen2.5 0.5B','phi3-mini':'Phi-3 Mini','qwen-coder-05b':'Coder 0.5B','qwen-coder-3b':'Coder 3B','viewer':'Katsoja','codelab-viewer':'Koodilabra'};
|
||||
sessions.sort((a, b) => {
|
||||
const aOnline = !a.disconnected_at;
|
||||
const bOnline = !b.disconnected_at;
|
||||
@@ -321,28 +321,9 @@ async fn main() {
|
||||
});
|
||||
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
|
||||
|
||||
// Coder-promptit — pieniä Python-tehtäviä
|
||||
let code_prompts = vec![
|
||||
"Write a Python function that checks if a number is prime.",
|
||||
"Write a Python function that reverses a string without using slicing.",
|
||||
"Write a Python function to find the factorial of a number using recursion.",
|
||||
"Write a Python function that returns the Fibonacci sequence up to n numbers.",
|
||||
"Write a Python function to check if a string is a palindrome.",
|
||||
"Write a Python function that sorts a list using bubble sort.",
|
||||
"Write a Python function to count the occurrences of each character in a string.",
|
||||
"Write a Python function that flattens a nested list.",
|
||||
"Write a Python function to find the greatest common divisor of two numbers.",
|
||||
"Write a Python function that converts Celsius to Fahrenheit.",
|
||||
];
|
||||
let code_idx = (rng_state as usize / 13) % code_prompts.len();
|
||||
let coder_msg = serde_json::json!({
|
||||
"type": "llm_prompt",
|
||||
"prompt": code_prompts[code_idx],
|
||||
"model": "qwen-coder",
|
||||
});
|
||||
let _ = state_for_task.stats_tx.send(coder_msg.to_string());
|
||||
// Coder ei saa automaattisia tehtäviä — vain käyttäjän user_text
|
||||
|
||||
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3 + coder");
|
||||
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -617,11 +598,21 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
map.insert(node_id, allocated);
|
||||
}
|
||||
|
||||
// Tallennetaan sessiotieto tietokantaan
|
||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||
|
||||
// Tallennetaan valittu tehtävä muistiin reititystä varten
|
||||
let selected_task = json.get("selected_task").and_then(|v| v.as_str()).unwrap_or("tokenize").to_string();
|
||||
let is_viewer = selected_task == "viewer" || selected_task == "codelab-viewer";
|
||||
let existing = state.node_tasks.lock().unwrap().contains_key(&node_id);
|
||||
|
||||
if existing {
|
||||
// Sama yhteys, eri tehtävä → päivitetään
|
||||
state.db.update_session_task(node_id, &selected_task);
|
||||
tracing::info!("Solmu {} päivitti tehtävän → {}", node_id, selected_task);
|
||||
} else {
|
||||
// Uusi yhteys — suljetaan saman IP:n viewer-sessiot jos tämä on aktiivinen node
|
||||
if !is_viewer {
|
||||
state.db.close_viewers_by_ip(&ip.to_string());
|
||||
}
|
||||
state.db.insert_session(node_id, &ip.to_string(), node_type, &json);
|
||||
}
|
||||
state.node_tasks.lock().unwrap().insert(node_id, selected_task);
|
||||
|
||||
if node_type == "native" {
|
||||
@@ -726,6 +717,14 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
}
|
||||
broadcast_stats(&state).await;
|
||||
}
|
||||
} else if msg_type == "single_tokenize_done" {
|
||||
{
|
||||
let mut json = json.clone();
|
||||
if let Some(obj) = json.as_object_mut() {
|
||||
obj.insert("node_id".to_string(), serde_json::json!(node_id));
|
||||
}
|
||||
let _ = state.stats_tx.send(json.to_string());
|
||||
}
|
||||
} else if msg_type == "llm_chunk" {
|
||||
{
|
||||
let mut json = json;
|
||||
@@ -770,14 +769,11 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
tracing::info!("Solmu {} lähetti oman tekstin ({}): \"{}\"", node_id, task_type, &text[..text.len().min(80)]);
|
||||
match task_type {
|
||||
"tokenize" => {
|
||||
// Tokenisoidaan käyttäjän teksti EN-puolella, FI jätetään tyhjäksi
|
||||
let pair = serde_json::json!({
|
||||
"type": "pair_task",
|
||||
"en": text,
|
||||
"fi": text,
|
||||
"user_submitted": true,
|
||||
let msg = serde_json::json!({
|
||||
"type": "single_tokenize",
|
||||
"text": text,
|
||||
});
|
||||
let _ = state.stats_tx.send(pair.to_string());
|
||||
let _ = state.stats_tx.send(msg.to_string());
|
||||
}
|
||||
_ => {
|
||||
// LLM-prompti
|
||||
|
||||
@@ -26,9 +26,9 @@ web-sys = { version = "0.3.68", features = [
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
burn = { version = "0.21.0-pre.2", default-features = false, features = ["wgpu", "ndarray"] }
|
||||
burn-wgpu = "0.21.0-pre.2"
|
||||
burn-ndarray = "0.21.0-pre.2"
|
||||
burn = { version = "0.14.0", features = ["wgpu", "ndarray"] }
|
||||
burn-wgpu = "0.14.0"
|
||||
burn-ndarray = "0.14.0"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||
|
||||
@@ -22,8 +22,15 @@ macro_rules! console_log {
|
||||
static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50);
|
||||
static HAS_WEBGPU: AtomicBool = AtomicBool::new(true);
|
||||
static SELECTED_TASK: AtomicU32 = AtomicU32::new(0);
|
||||
// Estää rinnakkaiset LLM-inferenssit (vain yksi kerrallaan)
|
||||
static LLM_BUSY: AtomicBool = AtomicBool::new(false);
|
||||
// Käsitelläänkö hubin automaattisia tehtäviä
|
||||
static AUTO_TASKS: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_auto_tasks(enabled: bool) {
|
||||
AUTO_TASKS.store(enabled, Ordering::SeqCst);
|
||||
console_log!("[Wasm] Automaattiset tehtävät: {}", if enabled { "päällä" } else { "pois" });
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_gpu_load(load: u32) {
|
||||
@@ -110,6 +117,30 @@ fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::V
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenisoi yksittäisen tekstin ja lähettää tuloksen hubille
|
||||
async fn run_single_tokenize(text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
|
||||
let Some(bytes) = cached_tok else { return; };
|
||||
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else { return; };
|
||||
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start = perf.now();
|
||||
let result = tokenize_text(&tokenizer, &text);
|
||||
let duration_ms = perf.now() - start;
|
||||
|
||||
let token_count = result["token_count"].as_u64().unwrap_or(0);
|
||||
let cpt = result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
console_log!("Tokenisaatio: \"{}\" → {} tokenia | {:.2} m/t | {:.2}ms",
|
||||
&text[..text.len().min(50)], token_count, cpt, duration_ms);
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"type": "single_tokenize_done",
|
||||
"result": result,
|
||||
"duration_ms": (duration_ms * 100.0).round() / 100.0,
|
||||
});
|
||||
let _ = ws.borrow().send_with_str(&msg.to_string());
|
||||
}
|
||||
|
||||
/// Tokenisoi en/fi-parin, vertaa tehokkuutta ja lähettää tuloksen hubille
|
||||
async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let load_pct = GPU_LOAD_PERCENT.load(Ordering::SeqCst);
|
||||
@@ -194,8 +225,9 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
let msg: String = txt.into();
|
||||
|
||||
let current_task = SELECTED_TASK.load(Ordering::SeqCst);
|
||||
let auto_on = AUTO_TASKS.load(Ordering::SeqCst);
|
||||
|
||||
if msg.contains("pair_task") && current_task == 0 {
|
||||
if msg.contains("pair_task") && current_task == 0 && auto_on {
|
||||
// Vain tokenisaatiosolmut käsittelevät pair_task-viestejä
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let en = task.get("en").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
@@ -207,7 +239,17 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") && current_task == 1 {
|
||||
} else if msg.contains("single_tokenize") && current_task == 0 {
|
||||
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
let text = task.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if !text.is_empty() {
|
||||
let ws_for_async = ws_clone.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
run_single_tokenize(text, ws_for_async).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") && current_task == 1 && auto_on {
|
||||
// Vain SmolLM-solmut, ja vain yksi inferenssi kerrallaan
|
||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||
// Ohitetaan — edellinen inferenssi vielä käynnissä
|
||||
@@ -223,7 +265,7 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") && current_task == 2 {
|
||||
} else if msg.contains("llm_prompt") && current_task == 2 && auto_on {
|
||||
// Qwen2.5-0.5B
|
||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
@@ -237,7 +279,7 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if msg.contains("llm_prompt") && current_task == 3 {
|
||||
} else if msg.contains("llm_prompt") && current_task == 3 && auto_on {
|
||||
// Phi-3 Mini
|
||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||
|
||||
@@ -199,6 +199,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
crate::sleep_ms(0).await;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
|
||||
@@ -262,6 +262,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
||||
let _ = ws.borrow().send_with_str(&chunk.to_string());
|
||||
}
|
||||
tokens_generated += 1;
|
||||
|
||||
// Yield — vapautetaan selaimen event loop joka tokenin jälkeen
|
||||
crate::sleep_ms(0).await;
|
||||
}
|
||||
|
||||
let gen_time = perf.now() - start_gen;
|
||||
|
||||
@@ -118,14 +118,11 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc<RefCell<WebSocket>>) {
|
||||
Err(e) => { console_log!("[SmolLM] Malli-virhe: {}", e); return; }
|
||||
};
|
||||
|
||||
let use_gpu = crate::HAS_WEBGPU.load(std::sync::atomic::Ordering::SeqCst);
|
||||
if use_gpu {
|
||||
console_log!("[SmolLM] Burn WebGPU inferenssi...");
|
||||
run_burn_inference::<burn::backend::Wgpu>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
||||
} else {
|
||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
||||
}
|
||||
// Burn 0.14 wgpu ei yhteensopiva nykyisten selainten kanssa (maxInterStageShaderComponents)
|
||||
// Burn 0.21-pre.2 cubecl-runtime ei käänny Wasmille (println! puuttuu)
|
||||
// → NdArray kunnes Burn 0.21 stable + Wasm-tuki
|
||||
console_log!("[SmolLM] Burn NdArray (CPU) inferenssi...");
|
||||
run_burn_inference::<burn::backend::NdArray>(prompt, model_bytes, tokenizer, ws, perf.clone()).await;
|
||||
}
|
||||
|
||||
async fn run_burn_inference<B: burn::tensor::backend::Backend>(
|
||||
|
||||
@@ -119,6 +119,8 @@
|
||||
.main-panel { display: none; }
|
||||
.main-panel.active { display: block; }
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.code-output {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background: #010409;
|
||||
@@ -500,6 +502,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automaattiset tehtävät -->
|
||||
<div style="margin-bottom:14px;display:flex;align-items:center;gap:10px">
|
||||
<label style="font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="auto-tasks-toggle" checked style="accent-color:var(--accent-color)">
|
||||
Vastaanota automaattisia tehtäviä hubilta
|
||||
</label>
|
||||
<span style="font-size:11px;color:#8b949e">(10s välein)</span>
|
||||
</div>
|
||||
|
||||
<!-- Reaaliaikaiset metriikat -->
|
||||
<div id="metrics-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px">
|
||||
<div class="metric-card">
|
||||
@@ -655,7 +666,7 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, { start_agent_node, set_gpu_load } from './pkg/node.js';
|
||||
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
|
||||
|
||||
// Päävälilehtien vaihto
|
||||
window.switchMainTab = function(tab) {
|
||||
@@ -664,6 +675,21 @@
|
||||
document.getElementById('panel-' + tab).classList.add('active');
|
||||
document.querySelector(`.main-tab[onclick*="${tab}"]`).classList.add('active');
|
||||
window.location.hash = tab;
|
||||
|
||||
// Päivitetään admin-sessio vastaamaan nykyistä välilehteä
|
||||
if (window._uiSocket && window._uiSocket.readyState === 1) {
|
||||
const viewTask = tab === 'codelab' ? 'codelab-viewer' : 'viewer';
|
||||
window._uiSocket.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
status: 'viewer',
|
||||
node_type: 'browser',
|
||||
platform: navigator.platform || '',
|
||||
cpu_cores: navigator.hardwareConcurrency || 0,
|
||||
device_memory_gb: navigator.deviceMemory || 0,
|
||||
allocated_gb: 0,
|
||||
selected_task: viewTask,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// URL-hash navigointi: #codelab tai #network
|
||||
@@ -671,9 +697,24 @@
|
||||
switchMainTab('codelab');
|
||||
}
|
||||
|
||||
// Synkronoi coder-status kun WS on jo auki (suora #codelab navigointi)
|
||||
setTimeout(() => {
|
||||
if (uiSocket && uiSocket.readyState === 1) {
|
||||
const coderEl = document.getElementById('coder-status');
|
||||
if (coderEl && coderEl.textContent === 'Ei yhdistetty') {
|
||||
coderEl.textContent = 'Connected';
|
||||
coderEl.style.color = '#d29922';
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Koodilaboratorion tila
|
||||
const codeMetrics = { tasks: 0, tokens: 0, lastSpeed: 0 };
|
||||
let coderJoined = false;
|
||||
|
||||
// Globaali WebGPU-tila — tunnistetaan kerran viewer-authissa, käytetään kaikkialla
|
||||
let detectedWebGPU = false;
|
||||
let detectedGpuInfo = null;
|
||||
let wasmInitialized = false;
|
||||
let coderSize = '05b'; // '05b' tai '3b'
|
||||
|
||||
@@ -791,6 +832,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Automaattisten tehtävien toggle
|
||||
document.getElementById('auto-tasks-toggle')?.addEventListener('change', (e) => {
|
||||
if (window.wasm_active) {
|
||||
set_auto_tasks(e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Käyttäjän oma tekstisyöte
|
||||
const userInput = document.getElementById('user-text');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
@@ -815,13 +863,41 @@
|
||||
|
||||
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
|
||||
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
|
||||
uiSocket.onopen = () => {
|
||||
window._uiSocket = uiSocket;
|
||||
uiSocket.onopen = async () => {
|
||||
// Päivitetään molemmat statukset
|
||||
const el = document.getElementById('node-status');
|
||||
el.textContent = 'Connected';
|
||||
el.style.color = '#d29922';
|
||||
const coderEl = document.getElementById('coder-status');
|
||||
if (coderEl && !coderJoined) {
|
||||
coderEl.textContent = 'Connected';
|
||||
coderEl.style.color = '#d29922';
|
||||
}
|
||||
|
||||
// Tunnistetaan WebGPU kunnolla (adapter + info) — tallennetaan globaalisti
|
||||
if (navigator.gpu) {
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (adapter) {
|
||||
detectedWebGPU = true;
|
||||
const info = adapter.info || {};
|
||||
const maxBuf = Number(adapter.limits.maxBufferSize || 0);
|
||||
detectedGpuInfo = {
|
||||
vendor: info.vendor || '',
|
||||
description: info.description || '',
|
||||
architecture: info.architecture || '',
|
||||
device: info.device || '',
|
||||
estimated_vram_gb: maxBuf > 0 ? Math.round(maxBuf / 1024 / 1024 / 1024 * 4) : 0,
|
||||
max_buffer_size: maxBuf,
|
||||
max_compute_workgroups: adapter.limits.maxComputeWorkgroupsPerDimension || 0,
|
||||
};
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
const hasGPU = detectedWebGPU;
|
||||
const gpuInfo = detectedGpuInfo;
|
||||
|
||||
// Lähetetään kevyt auth heti — admin näkee kävijän välittömästi
|
||||
const hasGPU = !!navigator.gpu;
|
||||
uiSocket.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
status: 'viewer',
|
||||
@@ -832,12 +908,18 @@
|
||||
allocated_gb: 0,
|
||||
selected_task: 'viewer',
|
||||
has_webgpu: hasGPU,
|
||||
gpu: gpuInfo,
|
||||
}));
|
||||
};
|
||||
uiSocket.onclose = () => {
|
||||
const el = document.getElementById('node-status');
|
||||
el.textContent = 'Disconnected';
|
||||
el.style.color = '#f85149';
|
||||
const coderEl = document.getElementById('coder-status');
|
||||
if (coderEl) {
|
||||
coderEl.textContent = 'Disconnected';
|
||||
coderEl.style.color = '#f85149';
|
||||
}
|
||||
};
|
||||
uiSocket.onmessage = (event) => {
|
||||
try {
|
||||
@@ -871,6 +953,43 @@
|
||||
} else {
|
||||
dlBar.style.display = 'none';
|
||||
}
|
||||
} else if (data.type === "single_tokenize_done") {
|
||||
chatBox.classList.remove('hidden');
|
||||
const r = data.result || {};
|
||||
const ms = data.duration_ms || 0;
|
||||
const nodeId = data.node_id || '?';
|
||||
const cpt = parseFloat((r.chars_per_token || 0).toFixed(2));
|
||||
const cptColor = cpt >= 4 ? "#3fb950" : cpt >= 3 ? "#d29922" : "#f85149";
|
||||
const renderTokens = (tokens) => (tokens || []).map(t =>
|
||||
`<span class="tok tok-en">${t.replace(/</g,'<')}</span>`
|
||||
).join('');
|
||||
const tokHtml = renderTokens(r.tokens);
|
||||
const detailId = 'stok-' + Date.now();
|
||||
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'chat-msg';
|
||||
msgDiv.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="color:var(--accent-color);font-weight:600;font-size:15px">Solmu #${nodeId}</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button class="toggle-tokens" onclick="document.getElementById('${detailId}').classList.toggle('visible')">Tokenit</button>
|
||||
<span style="color:#8b949e;font-size:13px">${typeof ms === 'number' ? ms.toFixed(2) : ms}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:14px;color:#79b8ff;margin-bottom:6px">"${r.text || ''}"</div>
|
||||
<div style="font-size:14px;display:flex;gap:16px">
|
||||
<span style="color:#8b949e">${r.char_count || 0} merkkiä</span>
|
||||
<span style="color:#8b949e">${r.word_count || 0} sanaa</span>
|
||||
<span style="color:var(--accent-color);font-weight:600">${r.token_count || 0} tokenia</span>
|
||||
<span style="color:${cptColor};font-weight:600">${cpt} merkkiä/token</span>
|
||||
</div>
|
||||
<div id="${detailId}" class="token-detail">
|
||||
<strong style="color:#58a6ff;font-size:12px">(${r.token_count || 0})</strong> ${tokHtml}
|
||||
</div>`;
|
||||
chatBox.appendChild(msgDiv);
|
||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
flashComputing();
|
||||
} else if (data.type === "pair_task") {
|
||||
chatBox.classList.remove('hidden');
|
||||
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
|
||||
@@ -960,7 +1079,8 @@
|
||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
} else if (data.type === "llm_done") {
|
||||
// SmolLM / LLM-inferenssin tulos
|
||||
// Poistetaan streaming-kortti
|
||||
chatBox.querySelector('.streaming-card')?.remove();
|
||||
chatBox.classList.remove('hidden');
|
||||
const nodeId = data.node_id || "?";
|
||||
const model = data.model || "LLM";
|
||||
@@ -998,53 +1118,56 @@
|
||||
|
||||
console.log(`[${model}] ${tokGen} tokenia | ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s | "${(data.response || '').substring(0, 60)}..."`);
|
||||
} else if (data.type === "llm_chunk") {
|
||||
// Streaming-token LLM:ltä — näytetään lokissa
|
||||
// (chat-ikkunan streaming tulisi toteuttaa samalla logiikalla kuin pair_task/pair_done)
|
||||
// Streaming: näytetään generointi reaaliaikaisesti
|
||||
const model = data.model || '';
|
||||
const isCoder = model.includes('Coder');
|
||||
const targetBox = isCoder ? codeResults : chatBox;
|
||||
|
||||
if (targetBox) {
|
||||
let streamEl = targetBox.querySelector('.streaming-card');
|
||||
if (!streamEl) {
|
||||
streamEl = document.createElement('div');
|
||||
streamEl.className = isCoder ? 'code-task-card streaming-card' : 'chat-msg streaming-card';
|
||||
streamEl.style.borderLeftColor = '#a371f7';
|
||||
streamEl.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||
<span style="color:#a371f7;font-weight:600">${model}</span>
|
||||
<span class="stream-counter" style="color:var(--accent-color);font-size:12px">0 tok</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:#8b949e;margin-bottom:4px">Prompt: "${data.prompt || ''}"</div>
|
||||
<div class="stream-text" style="font-size:14px;color:var(--text-color);line-height:1.5;${isCoder ? 'font-family:Courier New,monospace;background:#010409;padding:8px;border-radius:4px;white-space:pre-wrap;font-size:12px;color:#3fb950' : ''}"></div>
|
||||
<div style="margin-top:6px;font-size:11px;color:#d29922">
|
||||
<span class="spinner" style="display:inline-block;animation:spin 1s linear infinite">◠</span> Generating...
|
||||
</div>`;
|
||||
if (isCoder) {
|
||||
targetBox.insertBefore(streamEl, targetBox.firstChild);
|
||||
} else {
|
||||
targetBox.appendChild(streamEl);
|
||||
}
|
||||
}
|
||||
const textEl = streamEl.querySelector('.stream-text');
|
||||
const counterEl = streamEl.querySelector('.stream-counter');
|
||||
if (textEl) textEl.textContent += data.token || '';
|
||||
const tokCount = (textEl.textContent || '').split('').length;
|
||||
if (counterEl) counterEl.textContent = tokCount + ' tok';
|
||||
targetBox.scrollTop = targetBox.scrollHeight;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
// Kerätään laitteistotiedot
|
||||
let hasWebGPU = false;
|
||||
// Käytetään viewer-authissa jo tunnistettua WebGPU-tilaa
|
||||
let hasWebGPU = detectedWebGPU;
|
||||
const deviceInfo = {
|
||||
allocated_gb: 4,
|
||||
cpu_cores: navigator.hardwareConcurrency || 0,
|
||||
device_memory_gb: navigator.deviceMemory || 0,
|
||||
platform: navigator.platform || "",
|
||||
gpu: null,
|
||||
gpu: detectedGpuInfo,
|
||||
selected_task: selectedTask
|
||||
};
|
||||
|
||||
if (navigator.gpu) {
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (adapter) {
|
||||
try {
|
||||
const testDevice = await adapter.requestDevice({ requiredLimits: { maxInterStageShaderComponents: 60 } });
|
||||
hasWebGPU = true;
|
||||
testDevice.destroy();
|
||||
} catch(e) {
|
||||
console.log("[WebGPU] Legacy-tuki rajoittaa WebGPU:n: " + e.message);
|
||||
hasWebGPU = false; // Fallback to CPU NdArray avoiding WASM panic
|
||||
}
|
||||
const info = adapter.info || {};
|
||||
const maxBuf = Number(adapter.limits.maxBufferSize || 0);
|
||||
// maxBufferSize antaa arvion VRAM:sta — tyypillisesti ~25% todellisesta
|
||||
const estimatedVramGb = maxBuf > 0 ? Math.round(maxBuf / 1024 / 1024 / 1024 * 4) : 0;
|
||||
deviceInfo.gpu = {
|
||||
vendor: info.vendor || "",
|
||||
architecture: info.architecture || "",
|
||||
device: info.device || "",
|
||||
description: info.description || "",
|
||||
max_buffer_size: maxBuf,
|
||||
max_compute_workgroups: adapter.limits.maxComputeWorkgroupsPerDimension || 0,
|
||||
estimated_vram_gb: estimatedVramGb
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const gpuStr = hasWebGPU ? (deviceInfo.gpu?.description || deviceInfo.gpu?.vendor || "WebGPU") : "ei GPU:ta";
|
||||
// Laskenta käyttää aina CPU:ta (Candle), WebGPU on vain tensorilaskennassa (Burn)
|
||||
const computeBackend = (selectedTask === 'tokenize')
|
||||
@@ -1184,6 +1307,9 @@
|
||||
}
|
||||
|
||||
function addCodeResult(data) {
|
||||
// Poistetaan streaming-kortti
|
||||
codeResults.querySelector('.streaming-card')?.remove();
|
||||
|
||||
const model = data.model || 'Coder';
|
||||
const tokGen = data.tokens_generated || 0;
|
||||
const durMs = data.duration_ms || 0;
|
||||
|
||||
Reference in New Issue
Block a user