agentic office
This commit is contained in:
@@ -4,4 +4,4 @@ members = [
|
|||||||
"hub",
|
"hub",
|
||||||
"node",
|
"node",
|
||||||
"native-node"
|
"native-node"
|
||||||
]
|
, "cli"]
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ COPY Cargo.lock* ./
|
|||||||
COPY hub/Cargo.toml hub/Cargo.toml
|
COPY hub/Cargo.toml hub/Cargo.toml
|
||||||
COPY node/Cargo.toml node/Cargo.toml
|
COPY node/Cargo.toml node/Cargo.toml
|
||||||
COPY native-node/Cargo.toml native-node/Cargo.toml
|
COPY native-node/Cargo.toml native-node/Cargo.toml
|
||||||
|
COPY cli/Cargo.toml cli/Cargo.toml
|
||||||
|
|
||||||
# Kopioi lähdekoodi
|
# Kopioi lähdekoodi
|
||||||
COPY hub/src hub/src
|
COPY hub/src hub/src
|
||||||
COPY node/src node/src
|
COPY node/src node/src
|
||||||
COPY native-node/src native-node/src
|
COPY native-node/src native-node/src
|
||||||
|
COPY cli/src cli/src
|
||||||
COPY static static
|
COPY static static
|
||||||
|
|
||||||
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
|
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
|
||||||
|
|||||||
15
network-poc/cli/Cargo.toml
Normal file
15
network-poc/cli/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.6.0", features = ["derive"] }
|
||||||
|
console = "0.16.3"
|
||||||
|
indicatif = "0.18.4"
|
||||||
|
reqwest = { version = "0.13.2", features = ["json"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
uuid = { version = "1.23.0", features = ["v4"] }
|
||||||
165
network-poc/cli/src/main.rs
Normal file
165
network-poc/cli/src/main.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "kpn")]
|
||||||
|
#[command(about = "Kipinä Agent Local CLI", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Alustaa uuden Kipinä-agenttikansion nykyiseen projektiin
|
||||||
|
Init {
|
||||||
|
#[arg(short, long, default_value = "kipina-tasks")]
|
||||||
|
dir: String,
|
||||||
|
},
|
||||||
|
/// Ajaa `.md` tiedostossa kuvatun tehtävän Kipinä-verkoston kautta
|
||||||
|
Run {
|
||||||
|
/// Polku `.md` työtiedostoon
|
||||||
|
file: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct Frontmatter {
|
||||||
|
agent: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
context: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CompletionRequest {
|
||||||
|
model: String,
|
||||||
|
prompt: String,
|
||||||
|
task_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CompletionResponse {
|
||||||
|
response: String,
|
||||||
|
model: String,
|
||||||
|
tokens_generated: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
Commands::Init { dir } => {
|
||||||
|
let path = Path::new(dir);
|
||||||
|
if !path.exists() {
|
||||||
|
fs::create_dir_all(path).unwrap();
|
||||||
|
let example = format!("---\nstatus: open\nagent: qwen-coder-3b\ncontext: []\n---\n\n# Tehtävä\nKirjoita tähän mitä haluat verkon koodaavan.");
|
||||||
|
fs::write(path.join("01-esimerkki.md"), example).unwrap();
|
||||||
|
println!("✅ Alustettu lokaali agenttikansio: {}", dir);
|
||||||
|
} else {
|
||||||
|
println!("⚠️ Kansio {} on jo olemassa.", dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::Run { file } => {
|
||||||
|
if let Err(e) = run_workflow(file).await {
|
||||||
|
eprintln!("❌ Virhe: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_workflow(filepath: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let content = fs::read_to_string(filepath)?;
|
||||||
|
|
||||||
|
// Yksinkertainen frontmatter-parseri
|
||||||
|
let mut frontmatter_str = String::new();
|
||||||
|
let mut body = String::new();
|
||||||
|
let mut in_frontmatter = false;
|
||||||
|
let mut fm_found = false;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.trim() == "---" {
|
||||||
|
if !fm_found {
|
||||||
|
in_frontmatter = true;
|
||||||
|
fm_found = true;
|
||||||
|
continue;
|
||||||
|
} else if in_frontmatter {
|
||||||
|
in_frontmatter = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_frontmatter {
|
||||||
|
frontmatter_str.push_str(line);
|
||||||
|
frontmatter_str.push('\n');
|
||||||
|
} else {
|
||||||
|
body.push_str(line);
|
||||||
|
body.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta: Frontmatter = if fm_found {
|
||||||
|
serde_yaml::from_str(&frontmatter_str).unwrap_or(Frontmatter { agent: None, status: None, context: None })
|
||||||
|
} else {
|
||||||
|
Frontmatter { agent: None, status: None, context: None }
|
||||||
|
};
|
||||||
|
|
||||||
|
let model = meta.agent.unwrap_or_else(|| "qwen-coder-05b".to_string());
|
||||||
|
|
||||||
|
// Kerätään kontekstitiedostot
|
||||||
|
let mut mega_prompt = body.trim().to_string();
|
||||||
|
if let Some(ctx_files) = meta.context {
|
||||||
|
mega_prompt.push_str("\n\n=== KONTEKSTI ===\n");
|
||||||
|
for ctx in ctx_files {
|
||||||
|
if let Ok(c) = fs::read_to_string(&ctx) {
|
||||||
|
mega_prompt.push_str(&format!("\n--- Tiedosto: {} ---\n{}\n", ctx, c));
|
||||||
|
} else {
|
||||||
|
println!("⚠️ Varoitus: Kontekstitiedostoa {} ei löytynyt.", ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n🚀 Lähetetään tehtävä Kipinäverkkoon (Malli: {})", model);
|
||||||
|
|
||||||
|
let pb = ProgressBar::new_spinner();
|
||||||
|
pb.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
pb.set_style(
|
||||||
|
ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] {msg}")
|
||||||
|
.unwrap()
|
||||||
|
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
||||||
|
);
|
||||||
|
pb.set_message("Odotetaan verkon solmua ja laskentaa...");
|
||||||
|
|
||||||
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let req = CompletionRequest {
|
||||||
|
model: model.clone(),
|
||||||
|
prompt: mega_prompt.clone(),
|
||||||
|
task_id: task_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = client.post("http://localhost:3000/api/v1/chat/completions")
|
||||||
|
.json(&req)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let completion: CompletionResponse = res.json().await?;
|
||||||
|
pb.finish_with_message(format!("Tulos saapui verkolta! ({} tokenia)", completion.tokens_generated));
|
||||||
|
|
||||||
|
let new_content = format!("{}\n\n## Kipinä Agentin Ratkaisu\n{}\n", content, completion.response);
|
||||||
|
let updated_content = new_content.replace("status: open", "status: done");
|
||||||
|
fs::write(filepath, updated_content)?;
|
||||||
|
println!("✅ Vastaus tallennettu tiedostoon: {}", filepath);
|
||||||
|
} else {
|
||||||
|
pb.finish_with_message("❌ Verkkopyyntö epäonnistui!");
|
||||||
|
println!("Virhekoodi: {}", res.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7.4", features = ["ws", "macros"] }
|
axum = { version = "0.7.4", features = ["ws", "macros"] }
|
||||||
tokio = { version = "1.36.0", features = ["full"] }
|
tokio = { version = "1.36.0", features = ["full", "sync"] }
|
||||||
tower-http = { version = "0.5.2", features = ["fs", "cors", "trace"] }
|
tower-http = { version = "0.5.2", features = ["fs", "cors", "trace"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ async fn main() {
|
|||||||
.route("/api/sessions", get(api_sessions))
|
.route("/api/sessions", get(api_sessions))
|
||||||
.route("/api/pairs", get(api_pairs))
|
.route("/api/pairs", get(api_pairs))
|
||||||
.route("/api/stats", get(api_stats))
|
.route("/api/stats", get(api_stats))
|
||||||
|
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
||||||
.route("/admin", get(admin_page))
|
.route("/admin", get(admin_page))
|
||||||
.nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string())))
|
.nest_service("/", ServeDir::new(std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string())))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -820,3 +821,50 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
|||||||
broadcast_stats(&state).await;
|
broadcast_stats(&state).await;
|
||||||
sender_task.abort();
|
sender_task.abort();
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct ChatCompletionRequest {
|
||||||
|
model: String,
|
||||||
|
prompt: String,
|
||||||
|
task_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ChatCompletionResponse {
|
||||||
|
response: String,
|
||||||
|
model: String,
|
||||||
|
tokens_generated: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_chat_completions(
|
||||||
|
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<ChatCompletionRequest>,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let msg = serde_json::json!({
|
||||||
|
"type": "llm_prompt",
|
||||||
|
"prompt": payload.prompt,
|
||||||
|
"model": payload.model,
|
||||||
|
"task_id": payload.task_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rx = state.stats_tx.subscribe();
|
||||||
|
let _ = state.stats_tx.send(msg.to_string());
|
||||||
|
|
||||||
|
while let Ok(msg_str) = rx.recv().await {
|
||||||
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg_str) {
|
||||||
|
if v["type"].as_str() == Some("llm_done") {
|
||||||
|
if let Some(tid) = v["task_id"].as_str() {
|
||||||
|
if tid == payload.task_id {
|
||||||
|
let res = ChatCompletionResponse {
|
||||||
|
response: v["response"].as_str().unwrap_or("").to_string(),
|
||||||
|
model: v["model"].as_str().unwrap_or("").to_string(),
|
||||||
|
tokens_generated: v["tokens_generated"].as_u64().unwrap_or(0),
|
||||||
|
};
|
||||||
|
return axum::Json(res).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Network Error").into_response()
|
||||||
|
}
|
||||||
|
|||||||
@@ -298,12 +298,13 @@ pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_jso
|
|||||||
if LLM_BUSY.load(Ordering::SeqCst) {
|
if LLM_BUSY.load(Ordering::SeqCst) {
|
||||||
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
} else if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
|
||||||
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||||
if !prompt.is_empty() {
|
if !prompt.is_empty() {
|
||||||
let use_3b = current_task == 5;
|
let use_3b = current_task == 5;
|
||||||
LLM_BUSY.store(true, Ordering::SeqCst);
|
LLM_BUSY.store(true, Ordering::SeqCst);
|
||||||
let ws_for_async = ws_clone.clone();
|
let ws_for_async = ws_clone.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
qwen_coder::run_coder_inference(prompt, ws_for_async, use_3b).await;
|
qwen_coder::run_coder_inference(prompt, ws_for_async, use_3b, task_id).await;
|
||||||
LLM_BUSY.store(false, Ordering::SeqCst);
|
LLM_BUSY.store(false, Ordering::SeqCst);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async fn ensure_cached(key: &str, url: &str, ws: &Rc<RefCell<WebSocket>>) -> Res
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
/// use_3b: false = 0.5B (nopea), true = 3B (laadukas)
|
||||||
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool) {
|
pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use_3b: bool, task_id: Option<String>) {
|
||||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||||
let size_label = if use_3b { "3B" } else { "0.5B" };
|
let size_label = if use_3b { "3B" } else { "0.5B" };
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
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 };
|
||||||
console_log!("[Coder] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
console_log!("[Coder] {} tokenia | {:.0}ms | {:.1} tok/s", tokens_generated, gen_time, tokens_per_sec);
|
||||||
|
|
||||||
let done = serde_json::json!({
|
let mut done = serde_json::json!({
|
||||||
"type": "llm_done",
|
"type": "llm_done",
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"model": format!("Qwen2.5-Coder-{}-Instruct", size_label),
|
"model": format!("Qwen2.5-Coder-{}-Instruct", size_label),
|
||||||
@@ -281,5 +281,8 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
|
|||||||
"tokens_per_sec": (tokens_per_sec * 10.0).round() / 10.0,
|
"tokens_per_sec": (tokens_per_sec * 10.0).round() / 10.0,
|
||||||
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
"load_time_ms": (load_time * 100.0).round() / 100.0,
|
||||||
});
|
});
|
||||||
|
if let Some(tid) = task_id {
|
||||||
|
done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid));
|
||||||
|
}
|
||||||
let _ = ws.borrow().send_with_str(&done.to_string());
|
let _ = ws.borrow().send_with_str(&done.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
network-poc/static/avatars/old/forge_hero.png
Normal file
BIN
network-poc/static/avatars/old/forge_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
network-poc/static/avatars/old/gecko_hero.png
Normal file
BIN
network-poc/static/avatars/old/gecko_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
BIN
network-poc/static/avatars/old/kipina.png
Normal file
BIN
network-poc/static/avatars/old/kipina.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
BIN
network-poc/static/avatars/old/serpent_hero.png
Normal file
BIN
network-poc/static/avatars/old/serpent_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -368,6 +368,53 @@
|
|||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
.terminal-panel {
|
||||||
|
background:#010409;
|
||||||
|
border:1px solid var(--border-color);
|
||||||
|
border-radius:6px;
|
||||||
|
padding:15px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size:14px;
|
||||||
|
color:var(--success-color);
|
||||||
|
height:500px;
|
||||||
|
overflow-y:auto;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
.terminal-line { margin: 4px 0; }
|
||||||
|
.terminal-prompt { color: #d29922; }
|
||||||
|
.avatar-grid {
|
||||||
|
display:flex;
|
||||||
|
gap:15px;
|
||||||
|
justify-content:center;
|
||||||
|
margin-bottom:20px;
|
||||||
|
}
|
||||||
|
.avatar-card {
|
||||||
|
background:var(--panel-bg);
|
||||||
|
border:1px solid var(--border-color);
|
||||||
|
border-radius:8px;
|
||||||
|
padding:10px;
|
||||||
|
text-align:center;
|
||||||
|
width:120px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.avatar-card img {
|
||||||
|
width:80px;
|
||||||
|
height:80px;
|
||||||
|
border-radius:50%;
|
||||||
|
margin-bottom:10px;
|
||||||
|
border:2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.avatar-card.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.avatar-card.active img {
|
||||||
|
border-color:var(--accent-color);
|
||||||
|
box-shadow: 0 0 15px var(--accent-color);
|
||||||
|
}
|
||||||
|
.avatar-name { font-weight: bold; font-size: 13px; color: var(--text-color); }
|
||||||
|
.avatar-role { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -379,6 +426,7 @@
|
|||||||
<div class="main-tabs">
|
<div class="main-tabs">
|
||||||
<div class="main-tab active" onclick="switchMainTab('network')">Laskentaverkko</div>
|
<div class="main-tab active" onclick="switchMainTab('network')">Laskentaverkko</div>
|
||||||
<div class="main-tab" onclick="switchMainTab('codelab')">Koodilaboratorio</div>
|
<div class="main-tab" onclick="switchMainTab('codelab')">Koodilaboratorio</div>
|
||||||
|
<div class="main-tab" onclick="switchMainTab('agents')">Agents & CLI</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PANEELI 1: Laskentaverkko -->
|
<!-- PANEELI 1: Laskentaverkko -->
|
||||||
@@ -663,6 +711,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div><!-- /panel-codelab -->
|
</div><!-- /panel-codelab -->
|
||||||
|
|
||||||
|
<!-- PANEELI 3: Agents & CLI -->
|
||||||
|
<div id="panel-agents" class="main-panel" style="position: relative; border-radius: 6px;">
|
||||||
|
<div style="position: absolute; top:0; left:0; width:100%; height:100%; background: url('/avatars/forge_hero.png') no-repeat center center; background-size: cover; opacity: 0.15; z-index: 0; pointer-events: none; border-radius: 6px;"></div>
|
||||||
|
<div style="background:rgba(13, 17, 23, 0.7); backdrop-filter: blur(4px); border:1px solid var(--border-color); border-radius:6px; padding:16px; margin-bottom:16px; position: relative; z-index: 1;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
|
||||||
|
<span style="font-weight:600;font-size:15px;color:var(--accent-color)">Kipinä Agent Workspace</span>
|
||||||
|
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="avatar-grid">
|
||||||
|
<div class="avatar-card active" id="avatar-kpn">
|
||||||
|
<img src="/avatars/forge_hero.png" alt="Forge">
|
||||||
|
<div class="avatar-name">KPN CLI</div>
|
||||||
|
<div class="avatar-role">Paikallinen Ohjaus</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-coder">
|
||||||
|
<img src="/avatars/gecko_hero.png" alt="Gecko">
|
||||||
|
<div class="avatar-name">Qwen-Coder</div>
|
||||||
|
<div class="avatar-role">Koodiagentti</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-smol">
|
||||||
|
<img src="/avatars/serpent_hero.png" alt="Serpent">
|
||||||
|
<div class="avatar-name">SmolLM</div>
|
||||||
|
<div class="avatar-role">Logiikka</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-discord">
|
||||||
|
<img src="/avatars/discord_1.png" alt="Discord">
|
||||||
|
<div class="avatar-name">Swarm</div>
|
||||||
|
<div class="avatar-role">WebGPU Solmu</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terminal-panel" id="agent-terminal">
|
||||||
|
<div class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div>
|
||||||
|
<div class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /panel-agents -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -794,21 +881,33 @@
|
|||||||
|
|
||||||
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
|
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
|
||||||
const originalLog = console.log;
|
const originalLog = console.log;
|
||||||
console.log = function(...args) {
|
let logQueue = [];
|
||||||
originalLog.apply(console, args);
|
let logFlushPending = false;
|
||||||
// Älä tulosta teknisiä WGPU warningeja suoraan AI:n näytölle jos niitä on
|
function flushLogs() {
|
||||||
let msg = args.join(' ');
|
if (!logQueue.length) return;
|
||||||
if (msg.includes("wgpu") || msg.includes("vastaanotettu")) return; // Siistitään spämmäävät lokit näkymästä, koska niitä tulee nyt sata sekunnissa
|
const frag = document.createDocumentFragment();
|
||||||
|
for (const msg of logQueue) {
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.textContent = '> ' + msg;
|
p.textContent = '> ' + msg;
|
||||||
logBox.appendChild(p);
|
frag.appendChild(p);
|
||||||
|
|
||||||
// Ehkäistään selaimen jumittuminen sadoista tuhansista lokiriveistä pitkässä GPU-ajossa
|
|
||||||
if (logBox.children.length > 30) {
|
|
||||||
logBox.removeChild(logBox.firstChild);
|
|
||||||
}
|
}
|
||||||
|
logBox.appendChild(frag);
|
||||||
|
while (logBox.children.length > 20) logBox.removeChild(logBox.firstChild);
|
||||||
logBox.scrollTop = logBox.scrollHeight;
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
|
logQueue = [];
|
||||||
|
logFlushPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = function(...args) {
|
||||||
|
originalLog.apply(console, args);
|
||||||
|
let msg = args.join(' ');
|
||||||
|
if (msg.includes("wgpu") || msg.includes("vastaanotettu") || msg.includes("Tehtävä vastaanotettu")) return;
|
||||||
|
|
||||||
|
logQueue.push(msg);
|
||||||
|
if (!logFlushPending) {
|
||||||
|
logFlushPending = true;
|
||||||
|
requestAnimationFrame(flushLogs);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
|
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
|
||||||
@@ -923,7 +1022,10 @@
|
|||||||
};
|
};
|
||||||
uiSocket.onmessage = (event) => {
|
uiSocket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const raw = event.data;
|
||||||
|
if (raw.includes('"single_tokenize"') || raw.includes('"download_progress"')) return;
|
||||||
|
|
||||||
|
const data = JSON.parse(raw);
|
||||||
if (data.type === "stats") {
|
if (data.type === "stats") {
|
||||||
statNodes.textContent = data.nodes;
|
statNodes.textContent = data.nodes;
|
||||||
statVram.textContent = data.vram_gb + " GB";
|
statVram.textContent = data.vram_gb + " GB";
|
||||||
@@ -935,13 +1037,6 @@
|
|||||||
}
|
}
|
||||||
} else if (data.type === "node_joined") {
|
} else if (data.type === "node_joined") {
|
||||||
chatBox.classList.remove('hidden');
|
chatBox.classList.remove('hidden');
|
||||||
const msgDiv = document.createElement('div');
|
|
||||||
msgDiv.className = 'chat-msg';
|
|
||||||
msgDiv.style.borderLeftColor = 'var(--success-color)';
|
|
||||||
msgDiv.innerHTML = `<span style="color:var(--success-color)">[Järjestelmä] Uusi solmu (ID: ${data.node_id}) liittyi verkon työjohdon piiriin!</span>`;
|
|
||||||
chatBox.appendChild(msgDiv);
|
|
||||||
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
|
||||||
chatBox.scrollTop = chatBox.scrollHeight;
|
|
||||||
} else if (data.type === "download_progress") {
|
} else if (data.type === "download_progress") {
|
||||||
const dlBar = document.getElementById('download-bar');
|
const dlBar = document.getElementById('download-bar');
|
||||||
if (data.pct < 100) {
|
if (data.pct < 100) {
|
||||||
@@ -987,10 +1082,10 @@
|
|||||||
<strong style="color:#58a6ff;font-size:12px">(${r.token_count || 0})</strong> ${tokHtml}
|
<strong style="color:#58a6ff;font-size:12px">(${r.token_count || 0})</strong> ${tokHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
chatBox.appendChild(msgDiv);
|
chatBox.appendChild(msgDiv);
|
||||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
||||||
chatBox.scrollTop = chatBox.scrollHeight;
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
flashComputing();
|
flashComputing();
|
||||||
} else if (data.type === "pair_task") {
|
} else if (data.type === "pair_task" && selectedTask === 'tokenize') {
|
||||||
chatBox.classList.remove('hidden');
|
chatBox.classList.remove('hidden');
|
||||||
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
|
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
|
||||||
chatBox.innerHTML = '';
|
chatBox.innerHTML = '';
|
||||||
@@ -1003,7 +1098,7 @@
|
|||||||
<div><strong style="color:#d29922">FI</strong> "${data.fi}"</div>
|
<div><strong style="color:#d29922">FI</strong> "${data.fi}"</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
chatBox.appendChild(msgDiv);
|
chatBox.appendChild(msgDiv);
|
||||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
||||||
chatBox.scrollTop = chatBox.scrollHeight;
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
} else if (data.type === "pair_done") {
|
} else if (data.type === "pair_done") {
|
||||||
chatBox.classList.remove('hidden');
|
chatBox.classList.remove('hidden');
|
||||||
@@ -1076,9 +1171,27 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (!msgDiv.parentNode) chatBox.appendChild(msgDiv);
|
if (!msgDiv.parentNode) chatBox.appendChild(msgDiv);
|
||||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
||||||
chatBox.scrollTop = chatBox.scrollHeight;
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
} else if (data.type === "llm_done") {
|
} else if (data.type === "llm_done") {
|
||||||
|
const term = document.getElementById('agent-terminal');
|
||||||
|
if (term) {
|
||||||
|
const model = data.model || 'llm';
|
||||||
|
const tokGen = data.tokens_generated || 0;
|
||||||
|
const durMs = typeof data.duration_ms === 'number' ? data.duration_ms.toFixed(0) : data.duration_ms || '?';
|
||||||
|
const tokS = data.tokens_per_sec || '?';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'terminal-line';
|
||||||
|
div.style.color = '#a5d6ff';
|
||||||
|
div.innerHTML = ` ✓ ${model} <span style="color:#8b949e">${tokGen} tok | ${durMs}ms | ${tokS} tok/s</span>`;
|
||||||
|
term.appendChild(div);
|
||||||
|
while (term.children.length > 50) term.removeChild(term.firstChild);
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
|
||||||
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
|
document.getElementById('avatar-kpn').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
// Poistetaan streaming-kortti
|
// Poistetaan streaming-kortti
|
||||||
chatBox.querySelector('.streaming-card')?.remove();
|
chatBox.querySelector('.streaming-card')?.remove();
|
||||||
chatBox.classList.remove('hidden');
|
chatBox.classList.remove('hidden');
|
||||||
@@ -1107,7 +1220,7 @@
|
|||||||
${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
|
||||||
</div>`;
|
</div>`;
|
||||||
chatBox.appendChild(msgDiv);
|
chatBox.appendChild(msgDiv);
|
||||||
if (chatBox.children.length > 8) chatBox.removeChild(chatBox.firstChild);
|
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
|
||||||
chatBox.scrollTop = chatBox.scrollHeight;
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
|
|
||||||
metrics.tasks++;
|
metrics.tasks++;
|
||||||
@@ -1152,6 +1265,25 @@
|
|||||||
if (counterEl) counterEl.textContent = tokCount + ' tok';
|
if (counterEl) counterEl.textContent = tokCount + ' tok';
|
||||||
targetBox.scrollTop = targetBox.scrollHeight;
|
targetBox.scrollTop = targetBox.scrollHeight;
|
||||||
}
|
}
|
||||||
|
} else if (data.type === "llm_prompt") {
|
||||||
|
if (data.task_id) {
|
||||||
|
const term = document.getElementById('agent-terminal');
|
||||||
|
if (term) {
|
||||||
|
const model = data.model || 'llm';
|
||||||
|
const promptShort = (data.prompt || '').substring(0, 50).replace(/</g,'<');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'terminal-line';
|
||||||
|
div.innerHTML = `<span class="terminal-prompt">$</span> kpn run ${model} <span style="color:#8b949e">"${promptShort}"</span>`;
|
||||||
|
term.appendChild(div);
|
||||||
|
while (term.children.length > 50) term.removeChild(term.firstChild);
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
|
const model = data.model || '';
|
||||||
|
if (model.includes('coder')) document.getElementById('avatar-coder')?.classList.add('active');
|
||||||
|
else document.getElementById('avatar-smol')?.classList.add('active');
|
||||||
|
document.getElementById('avatar-discord')?.classList.add('active');
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user