lisätty pikku pörriäiset
@@ -10,7 +10,7 @@ use std::collections::HashMap;
|
|||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
@@ -341,7 +341,10 @@ async fn main() {
|
|||||||
.route("/api/stats", get(api_stats))
|
.route("/api/stats", get(api_stats))
|
||||||
.route("/api/v1/chat/completions", axum::routing::post(api_chat_completions))
|
.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("/", {
|
||||||
|
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
|
||||||
|
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
|
||||||
|
})
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
||||||
|
|||||||
BIN
network-poc/nodes.db
Normal file
BIN
network-poc/static/avatars/aikuinen_susi.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
44
network-poc/static/avatars/forge_hero.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#2a3f5a"/>
|
||||||
|
<stop offset="100%" stop-color="#0a121d"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="neonGold" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#ffca28" flood-opacity="0.8"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="neonBlue" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#4fc3f7" flood-opacity="0.9"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Base -->
|
||||||
|
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
|
||||||
|
|
||||||
|
<!-- Swarm Connectivity Lines -->
|
||||||
|
<g stroke="#4fc3f7" stroke-width="1.5" opacity="0.4" filter="url(#neonBlue)">
|
||||||
|
<line x1="100" y1="100" x2="30" y2="40" />
|
||||||
|
<line x1="100" y1="100" x2="170" y2="40" />
|
||||||
|
<line x1="100" y1="100" x2="40" y2="160" />
|
||||||
|
<line x1="100" y1="100" x2="160" y2="160" />
|
||||||
|
<line x1="100" y1="100" x2="190" y2="100" />
|
||||||
|
<line x1="100" y1="100" x2="10" y2="100" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Gentle Crystal Core -->
|
||||||
|
<path d="M 100 40 L 130 100 L 100 160 L 70 100 Z" fill="#1b2a4a" stroke="#ffca28" stroke-width="3" filter="url(#neonGold)"/>
|
||||||
|
<path d="M 100 40 L 130 100 L 100 120 Z" fill="#2a3f5a" opacity="0.8"/>
|
||||||
|
<path d="M 100 40 L 70 100 L 100 120 Z" fill="#0a121d" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Floating Swarm Nodes -->
|
||||||
|
<circle cx="30" cy="40" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
|
||||||
|
<circle cx="170" cy="40" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
|
||||||
|
<circle cx="40" cy="160" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
|
||||||
|
<circle cx="160" cy="160" r="5" fill="#4fc3f7" filter="url(#neonBlue)"/>
|
||||||
|
<circle cx="190" cy="100" r="4" fill="#ffca28" filter="url(#neonGold)"/>
|
||||||
|
<circle cx="10" cy="100" r="4" fill="#ffca28" filter="url(#neonGold)"/>
|
||||||
|
|
||||||
|
<circle cx="100" cy="100" r="10" fill="#ffca28" filter="url(#neonGold)"/>
|
||||||
|
<circle cx="100" cy="100" r="4" fill="#ffffff"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
network-poc/static/avatars/karhunpentu.png
Normal file
|
After Width: | Height: | Size: 432 KiB |
BIN
network-poc/static/avatars/kettu_notext.png
Normal file
|
After Width: | Height: | Size: 650 KiB |
BIN
network-poc/static/avatars/kipina_notext.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
network-poc/static/avatars/laiskiainen.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
BIN
network-poc/static/avatars/laiskiainen_notext.png
Normal file
|
After Width: | Height: | Size: 496 KiB |
54
network-poc/static/avatars/lizard_ai.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#0a2e23"/>
|
||||||
|
<stop offset="100%" stop-color="#090e10"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="neonMint" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#0fffa3" flood-opacity="0.8"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="neonSun" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#ffb142" flood-opacity="0.9"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Base -->
|
||||||
|
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
|
||||||
|
|
||||||
|
<!-- Cyberpunk soft coding runic rings in background -->
|
||||||
|
<circle cx="100" cy="100" r="80" fill="none" stroke="#0fffa3" stroke-width="1" opacity="0.1" filter="url(#neonMint)"/>
|
||||||
|
<circle cx="100" cy="100" r="85" fill="none" stroke="#0fffa3" stroke-width="0.5" stroke-dasharray="4 8" opacity="0.2"/>
|
||||||
|
|
||||||
|
<!-- Lizard Gentle Head Shape -->
|
||||||
|
<path d="M 100 30 C 160 30, 180 80, 150 140 C 120 180, 80 180, 50 140 C 20 80, 40 30, 100 30 Z" fill="#1b2a26" />
|
||||||
|
<path d="M 100 40 C 150 40, 160 80, 140 130 C 120 160, 80 160, 60 130 C 40 80, 50 40, 100 40 Z" fill="#243831" />
|
||||||
|
|
||||||
|
<!-- Code-scale back-ridges (Cyber implants but cute) -->
|
||||||
|
<path d="M 60 15 L 75 35 M 100 10 L 100 30 M 140 15 L 125 35" stroke="#0fffa3" stroke-width="4" stroke-linecap="round" filter="url(#neonMint)"/>
|
||||||
|
|
||||||
|
<!-- Big Lovely Lizard Eyes mapped to sides -->
|
||||||
|
<ellipse cx="45" cy="85" rx="20" ry="30" fill="#090e10" transform="rotate(-15 45 85)"/>
|
||||||
|
<ellipse cx="155" cy="85" rx="20" ry="30" fill="#090e10" transform="rotate(15 155 85)"/>
|
||||||
|
|
||||||
|
<!-- Neon Glowing Pupils -->
|
||||||
|
<circle cx="45" cy="85" r="10" fill="#0fffa3" filter="url(#neonMint)"/>
|
||||||
|
<circle cx="155" cy="85" r="10" fill="#0fffa3" filter="url(#neonMint)"/>
|
||||||
|
<circle cx="42" cy="78" r="4" fill="#ffffff" opacity="0.9"/>
|
||||||
|
<circle cx="152" cy="78" r="4" fill="#ffffff" opacity="0.9"/>
|
||||||
|
|
||||||
|
<!-- Adorable Snout & Neural Nose-bridge -->
|
||||||
|
<path d="M 100 45 L 100 100" stroke="#ffb142" stroke-width="2" opacity="0.5" filter="url(#neonSun)"/>
|
||||||
|
<circle cx="100" cy="100" r="4" fill="#ffb142" filter="url(#neonSun)"/>
|
||||||
|
|
||||||
|
<path d="M 80 145 C 90 155, 110 155, 120 145" fill="none" stroke="#0fffa3" stroke-width="3" stroke-linecap="round" filter="url(#neonMint)"/>
|
||||||
|
|
||||||
|
<!-- Cute little cheek blushes in neon -->
|
||||||
|
<ellipse cx="65" cy="115" rx="8" ry="4" fill="#ffb142" opacity="0.6" filter="url(#neonSun)"/>
|
||||||
|
<ellipse cx="135" cy="115" rx="8" ry="4" fill="#ffb142" opacity="0.6" filter="url(#neonSun)"/>
|
||||||
|
|
||||||
|
<!-- Floating Brackets (Coder vibe) -->
|
||||||
|
<text x="15" y="150" fill="#0fffa3" font-size="24" font-family="monospace" opacity="0.5" filter="url(#neonMint)">{</text>
|
||||||
|
<text x="170" y="60" fill="#0fffa3" font-size="24" font-family="monospace" opacity="0.5" filter="url(#neonMint)">}</text>
|
||||||
|
<text x="160" y="160" fill="#ffb142" font-size="18" font-family="monospace" opacity="0.5" filter="url(#neonSun)"></></text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
BIN
network-poc/static/avatars/pesukarhu.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
network-poc/static/avatars/pesukarhu_notext.png
Normal file
|
After Width: | Height: | Size: 563 KiB |
66
network-poc/static/avatars/raccoon_ai.svg
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#3d155f"/>
|
||||||
|
<stop offset="100%" stop-color="#090a0f"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="eyeReflection" cx="40%" cy="40%" r="40%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="neonPink" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#ff71ce" flood-opacity="0.8"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="neonCyan" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#05d9e8" flood-opacity="0.9"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Base -->
|
||||||
|
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
|
||||||
|
|
||||||
|
<!-- Cyberpunk background grid elements -->
|
||||||
|
<g stroke="#05d9e8" stroke-width="0.5" opacity="0.3">
|
||||||
|
<path d="M 20 40 L 180 40 M 20 80 L 180 80 M 20 120 L 180 120 M 20 160 L 180 160"/>
|
||||||
|
<path d="M 40 20 L 40 180 M 80 20 L 80 180 M 120 20 L 120 180 M 160 20 L 160 180"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Fluffy Gentle Raccoon Base -->
|
||||||
|
<path d="M 100 40 C 150 40, 170 80, 160 120 C 145 160, 55 160, 40 120 C 30 80, 50 40, 100 40 Z" fill="#2d3340" />
|
||||||
|
|
||||||
|
<!-- Ears -->
|
||||||
|
<path d="M 45 60 C 25 35, 30 15, 60 30 Z" fill="#1b212b" stroke="#ff71ce" stroke-width="2" filter="url(#neonPink)"/>
|
||||||
|
<path d="M 155 60 C 175 35, 170 15, 140 30 Z" fill="#1b212b" stroke="#05d9e8" stroke-width="2" filter="url(#neonCyan)"/>
|
||||||
|
|
||||||
|
<path d="M 50 55 C 35 40, 40 25, 55 35 Z" fill="#ff71ce" opacity="0.6" filter="url(#neonPink)"/>
|
||||||
|
<path d="M 150 55 C 165 40, 160 25, 145 35 Z" fill="#05d9e8" opacity="0.6" filter="url(#neonCyan)"/>
|
||||||
|
|
||||||
|
<!-- Bandit Mask (Cyber-goggles vibe but soft) -->
|
||||||
|
<path d="M 30 95 C 60 70, 140 70, 170 95 C 180 105, 140 140, 100 130 C 60 140, 20 105, 30 95 Z" fill="#0b0e14" opacity="0.8"/>
|
||||||
|
<path d="M 35 95 C 65 75, 135 75, 165 95" fill="none" stroke="#ff71ce" stroke-width="2" filter="url(#neonPink)" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Big Gentle Anime-style Eyes -->
|
||||||
|
<ellipse cx="65" cy="100" rx="16" ry="22" fill="#05d9e8" filter="url(#neonCyan)"/>
|
||||||
|
<ellipse cx="135" cy="100" rx="16" ry="22" fill="#ff71ce" filter="url(#neonPink)"/>
|
||||||
|
<ellipse cx="65" cy="100" rx="12" ry="18" fill="#111"/>
|
||||||
|
<ellipse cx="135" cy="100" rx="12" ry="18" fill="#111"/>
|
||||||
|
<!-- Glints -->
|
||||||
|
<circle cx="58" cy="92" r="6" fill="url(#eyeReflection)"/>
|
||||||
|
<circle cx="128" cy="92" r="6" fill="url(#eyeReflection)"/>
|
||||||
|
<circle cx="72" cy="108" r="3" fill="#fff" opacity="0.6"/>
|
||||||
|
<circle cx="142" cy="108" r="3" fill="#fff" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Cute Muzzle & Nose -->
|
||||||
|
<path d="M 80 120 C 100 150, 120 120, 100 140 Z" fill="#e5e5e5"/>
|
||||||
|
<path d="M 90 140 C 90 135, 110 135, 110 140 C 110 148, 90 148, 90 140 Z" fill="#ff71ce" filter="url(#neonPink)"/>
|
||||||
|
<path d="M 95 145 C 100 152, 105 145, 105 145" fill="none" stroke="#2d3340" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Cyberpunk Gentle Neck Ring/Scarf -->
|
||||||
|
<path d="M 50 150 C 80 170, 120 170, 150 150" fill="none" stroke="#05d9e8" stroke-width="6" stroke-linecap="round" filter="url(#neonCyan)"/>
|
||||||
|
|
||||||
|
<!-- Little Floating Data Motes -->
|
||||||
|
<circle cx="30" cy="50" r="3" fill="#ff71ce" filter="url(#neonPink)"/>
|
||||||
|
<circle cx="170" cy="130" r="2" fill="#05d9e8" filter="url(#neonCyan)"/>
|
||||||
|
<circle cx="150" cy="40" r="4" fill="#ff71ce" filter="url(#neonPink)"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
49
network-poc/static/avatars/sloth_ai.svg
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bgGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#140722"/>
|
||||||
|
<stop offset="100%" stop-color="#0a0512"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="neonViolet" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="6" flood-color="#b829ea" flood-opacity="0.8"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="neonTeal" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#1cf0eb" flood-opacity="0.9"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background Base -->
|
||||||
|
<rect width="200" height="200" rx="30" fill="url(#bgGlow)"/>
|
||||||
|
|
||||||
|
<!-- Dreamy Cyber-waves -->
|
||||||
|
<path d="M 0 160 Q 50 180, 100 150 T 200 160 M 0 180 Q 50 160, 100 190 T 200 170" fill="none" stroke="#b829ea" stroke-width="2" opacity="0.2" filter="url(#neonViolet)"/>
|
||||||
|
|
||||||
|
<!-- Sloth Head Fluffy Contour -->
|
||||||
|
<path d="M 50 60 C 20 90, 40 160, 100 160 C 160 160, 180 90, 150 60 C 130 30, 70 30, 50 60 Z" fill="#231a31" />
|
||||||
|
|
||||||
|
<!-- Face Mask (Gentle Heart-like shape) -->
|
||||||
|
<path d="M 70 80 C 60 120, 90 140, 100 140 C 110 140, 140 120, 130 80 C 120 50, 80 50, 70 80 Z" fill="#3c304d" />
|
||||||
|
|
||||||
|
<!-- Cozy VR Sleep-Mask / Cyber-visor -->
|
||||||
|
<path d="M 50 80 Q 100 100, 150 80 Q 140 110, 100 115 Q 60 110, 50 80 Z" fill="#0d0914" stroke="#b829ea" stroke-width="2" filter="url(#neonViolet)"/>
|
||||||
|
|
||||||
|
<!-- Sweet Dreamy Digital Lines inside Visor -->
|
||||||
|
<path d="M 70 88 C 80 95, 90 95, 95 88 M 130 88 C 120 95, 110 95, 105 88" fill="none" stroke="#1cf0eb" stroke-width="3" stroke-linecap="round" filter="url(#neonTeal)"/>
|
||||||
|
|
||||||
|
<!-- Cute Sleepy Snout -->
|
||||||
|
<circle cx="100" cy="130" r="6" fill="#140722"/>
|
||||||
|
<path d="M 90 140 C 95 145, 105 145, 110 140" fill="none" stroke="#1cf0eb" stroke-width="2.5" stroke-linecap="round" filter="url(#neonTeal)"/>
|
||||||
|
|
||||||
|
<!-- Sleepy Zzzs and Star nodes -->
|
||||||
|
<text x="140" y="40" fill="#b829ea" font-size="20" font-family="sans-serif" font-weight="bold" filter="url(#neonViolet)">Z</text>
|
||||||
|
<text x="160" y="25" fill="#1cf0eb" font-size="14" font-family="sans-serif" font-weight="bold" filter="url(#neonTeal)">z</text>
|
||||||
|
<text x="175" y="15" fill="#b829ea" font-size="10" font-family="sans-serif" font-weight="bold" filter="url(#neonViolet)">z</text>
|
||||||
|
|
||||||
|
<circle cx="40" cy="50" r="2" fill="#1cf0eb" filter="url(#neonTeal)"/>
|
||||||
|
<circle cx="50" cy="30" r="1.5" fill="#b829ea" filter="url(#neonViolet)"/>
|
||||||
|
|
||||||
|
<!-- Cybernetic headset nodes -->
|
||||||
|
<circle cx="45" cy="85" r="8" fill="#140722" stroke="#1cf0eb" stroke-width="2" filter="url(#neonTeal)"/>
|
||||||
|
<circle cx="155" cy="85" r="8" fill="#140722" stroke="#1cf0eb" stroke-width="2" filter="url(#neonTeal)"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
network-poc/static/avatars/susi_notext.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
@@ -382,39 +382,115 @@
|
|||||||
}
|
}
|
||||||
.terminal-line { margin: 4px 0; }
|
.terminal-line { margin: 4px 0; }
|
||||||
.terminal-prompt { color: #d29922; }
|
.terminal-prompt { color: #d29922; }
|
||||||
.avatar-grid {
|
.org-chart {
|
||||||
display:flex;
|
display: flex;
|
||||||
gap:15px;
|
flex-direction: column;
|
||||||
justify-content:center;
|
align-items: center;
|
||||||
margin-bottom:20px;
|
margin-bottom: 40px;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
.org-level {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.org-connector {
|
||||||
|
width: 2px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(to bottom, rgba(88, 166, 255, 0.8), rgba(88, 166, 255, 0.2));
|
||||||
|
margin: 0px auto;
|
||||||
|
box-shadow: 0 0 10px rgba(88, 166, 255, 0.5);
|
||||||
|
}
|
||||||
|
.org-branch {
|
||||||
|
width: 420px;
|
||||||
|
height: 40px;
|
||||||
|
border-top: 2px solid rgba(88, 166, 255, 0.5);
|
||||||
|
border-left: 2px solid rgba(88, 166, 255, 0.5);
|
||||||
|
border-right: 2px solid rgba(88, 166, 255, 0.5);
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
box-shadow: inset 0 3px 6px -3px rgba(88, 166, 255, 0.4);
|
||||||
}
|
}
|
||||||
.avatar-card {
|
.avatar-card {
|
||||||
background:var(--panel-bg);
|
background: linear-gradient(145deg, rgba(33, 38, 45, 0.4) 0%, rgba(13, 17, 23, 0.8) 100%);
|
||||||
border:1px solid var(--border-color);
|
backdrop-filter: blur(12px);
|
||||||
border-radius:8px;
|
border: 1px solid rgba(240, 246, 252, 0.1);
|
||||||
padding:10px;
|
border-radius: 16px;
|
||||||
text-align:center;
|
padding: 12px 10px;
|
||||||
width:120px;
|
text-align: center;
|
||||||
opacity: 0.6;
|
width: 120px;
|
||||||
transition: all 0.3s;
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.avatar-card:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
border-color: rgba(240, 246, 252, 0.3);
|
||||||
|
box-shadow: 0 12px 20px rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
.avatar-card img {
|
.avatar-card img {
|
||||||
width:80px;
|
width: 80px;
|
||||||
height:80px;
|
height: 80px;
|
||||||
border-radius:50%;
|
border-radius: 18px;
|
||||||
margin-bottom:10px;
|
margin-bottom: 8px;
|
||||||
border:2px solid var(--border-color);
|
border: 2px solid rgba(240, 246, 252, 0.1);
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #010409;
|
||||||
}
|
}
|
||||||
.avatar-card.active {
|
.avatar-card.active, .avatar-card.selected {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-5px);
|
transform: translateY(-8px) scale(1.05);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: linear-gradient(145deg, rgba(88, 166, 255, 0.15) 0%, rgba(13, 17, 23, 0.9) 100%);
|
||||||
|
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88, 166, 255, 0.3);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.avatar-card.active img {
|
.avatar-card.active img, .avatar-card.selected img {
|
||||||
border-color:var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
box-shadow: 0 0 15px var(--accent-color);
|
box-shadow: 0 0 25px rgba(88, 166, 255, 0.5);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
.avatar-name { font-weight: bold; font-size: 13px; color: var(--text-color); }
|
.avatar-name { font-weight: 700; font-size: 14px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; }
|
||||||
.avatar-role { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
.avatar-role { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
|
||||||
|
.agent-prompt-editor {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.agent-prompt-editor.visible { display: block; }
|
||||||
|
.agent-prompt-editor textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: #010409;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
padding: 8px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.agent-prompt-editor textarea:focus { border-color: var(--accent-color); }
|
||||||
|
.agent-prompt-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.agent-prompt-label strong { color: var(--text-color); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -713,33 +789,79 @@
|
|||||||
|
|
||||||
<!-- PANEELI 3: Agents & CLI -->
|
<!-- PANEELI 3: Agents & CLI -->
|
||||||
<div id="panel-agents" class="main-panel" style="position: relative; border-radius: 6px;">
|
<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="position: absolute; top:0; left:0; width:100%; height:100%; background: url('/avatars/forge_hero.svg') 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="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">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
|
||||||
|
<div style="display:flex;align-items:center;gap:16px;">
|
||||||
<span style="font-weight:600;font-size:15px;color:var(--accent-color)">Kipinä Agent Workspace</span>
|
<span style="font-weight:600;font-size:15px;color:var(--accent-color)">Kipinä Agent Workspace</span>
|
||||||
|
<button id="btn-toggle-all" onclick="toggleAllAgents()" style="background:rgba(33, 38, 45, 0.8);border:1px solid var(--border-color);color:#c9d1d9;font-size:11px;padding:4px 12px;border-radius:4px;cursor:pointer;">Valitse kaikki</button>
|
||||||
|
</div>
|
||||||
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
|
<span id="agent-status" style="font-size:12px;color:var(--success-color)">Monitoring Active</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="avatar-grid">
|
<div class="org-chart">
|
||||||
<div class="avatar-card active" id="avatar-kpn">
|
<!-- Taso 1 -->
|
||||||
<img src="/avatars/forge_hero.png" alt="Forge">
|
<div class="org-level">
|
||||||
<div class="avatar-name">KPN CLI</div>
|
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')">
|
||||||
<div class="avatar-role">Paikallinen Ohjaus</div>
|
<img src="/avatars/kettu_notext.png" alt="Asiakas (Kettu)">
|
||||||
|
<div class="avatar-name">Asiakas</div>
|
||||||
|
<div class="avatar-role">Tuoteomistaja</div>
|
||||||
</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>
|
||||||
<div class="avatar-card" id="avatar-smol">
|
|
||||||
<img src="/avatars/serpent_hero.png" alt="Serpent">
|
<div class="org-connector"></div>
|
||||||
<div class="avatar-name">SmolLM</div>
|
|
||||||
<div class="avatar-role">Logiikka</div>
|
<!-- Taso 2 -->
|
||||||
|
<div class="org-level" style="position: relative;">
|
||||||
|
<!-- Tarkkailija laitetaan erilleen kauemmas sivuun jotta se näyttää itsenäiseltä valvojalta -->
|
||||||
|
<div class="avatar-card" id="avatar-observer" data-agent="observer" onclick="selectAgent('observer')" style="position: absolute; right: calc(50% + 240px); top: 0;">
|
||||||
|
<img src="/avatars/aikuinen_susi.png" alt="Tarkkailija (Aikuinen Susi)">
|
||||||
|
<div class="avatar-name">Tarkkailija</div>
|
||||||
|
<div class="avatar-role">Laadunvalvonta</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-card" id="avatar-discord">
|
|
||||||
<img src="/avatars/discord_1.png" alt="Discord">
|
<div class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager')">
|
||||||
<div class="avatar-name">Swarm</div>
|
<img src="/avatars/pesukarhu_notext.png" alt="Manageri (Pesukarhu)">
|
||||||
<div class="avatar-role">WebGPU Solmu</div>
|
<div class="avatar-name">Manageri</div>
|
||||||
|
<div class="avatar-role">KPN CLI</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="org-branch"></div>
|
||||||
|
|
||||||
|
<!-- Taso 3 -->
|
||||||
|
<div class="org-level" style="gap: 20px;">
|
||||||
|
<div class="avatar-card" id="avatar-coder" data-agent="coder" onclick="selectAgent('coder')">
|
||||||
|
<img src="/avatars/kipina_notext.png" alt="Koodari (Salamanteri)">
|
||||||
|
<div class="avatar-name">Koodari</div>
|
||||||
|
<div class="avatar-role">Ohjelmistokehitys</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
|
||||||
|
<img src="/avatars/karhunpentu.png" alt="Data-Agentti (Karhunpentu)">
|
||||||
|
<div class="avatar-name">Data</div>
|
||||||
|
<div class="avatar-role">Tietokannat</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-qa" data-agent="qa" onclick="selectAgent('qa')">
|
||||||
|
<img src="/avatars/susi_notext.png" alt="QA (Pikkususi)">
|
||||||
|
<div class="avatar-name">QA</div>
|
||||||
|
<div class="avatar-role">Testaus</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-card" id="avatar-tester" data-agent="tester" onclick="selectAgent('tester')">
|
||||||
|
<img src="/avatars/laiskiainen_notext.png" alt="DevOps (Laiskiainen)">
|
||||||
|
<div class="avatar-name">DevOps</div>
|
||||||
|
<div class="avatar-role">Käyttöönotto</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agent-prompt-editor" id="agent-prompt-editor">
|
||||||
|
<div class="agent-prompt-label">
|
||||||
|
<strong id="agent-prompt-name">—</strong>
|
||||||
|
<span id="agent-prompt-saved" style="color:var(--success-color);opacity:0;transition:opacity 0.3s">Tallennettu</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="agent-prompt-text" placeholder="Kirjoita system prompt..."></textarea>
|
||||||
|
<div id="shared-prompt-section" style="display:none;margin-top:8px;font-size:12px;color:#8b949e">
|
||||||
|
Yhteinen konteksti liitetään jokaisen valitun agentin oman promptin alkuun.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -747,6 +869,11 @@
|
|||||||
<div class="terminal-line"><span class="terminal-prompt">$</span> kpn hub connect wss://localhost</div>
|
<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 class="terminal-line" style="color:#a5d6ff"> ✓ Yhdistetty Kipinä Hubiin</div>
|
||||||
</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">
|
||||||
|
<span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span>
|
||||||
|
<input id="term-input" type="text" placeholder="kpn run coder "kirjoita hello world"" spellcheck="false"
|
||||||
|
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /panel-agents -->
|
</div><!-- /panel-agents -->
|
||||||
|
|
||||||
@@ -756,6 +883,114 @@
|
|||||||
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
|
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
|
||||||
|
|
||||||
// Päävälilehtien vaihto
|
// Päävälilehtien vaihto
|
||||||
|
// Agenttien system promptit
|
||||||
|
const agentPrompts = {
|
||||||
|
client: { name: 'Asiakas — Projektin vaatimukset', model: 'user-input', default: 'Kirjoita tähän asiakkaan toiveet ja projektin vaatimukset. Orkestraattori (Manageri) purkaa ja delegoi nämä työt asiantuntijoille.' },
|
||||||
|
observer: { name: 'Tarkkailija — System Prompt', model: 'deepseek-r1', default: 'Olet ohjelmistoprojektin riippumaton valvoja. Sinulla on täysi pääsy kaikkiin projektin tietoihin ja muiden agenttien keskusteluihin. Valvo tiimin (Manageri, Koodari, Data, QA, DevOps) toimintaa asiantuntijana kokonaisuutena ja huomauta välittömästi visio- tai turvallisuusriskeistä.' },
|
||||||
|
manager: { name: 'Manageri — System Prompt', model: 'qwen-coder', default: 'Olet projektipäällikkö. Jaa tehtävät osiin, priorisoi ja koordinoi tiimin työtä.' },
|
||||||
|
coder: { name: 'Koodari — System Prompt', model: 'qwen-coder', default: 'Olet kokenut ohjelmistokehittäjä. Kirjoita selkeää, testattavaa koodia ja vastaa aina koodilla.' },
|
||||||
|
data: { name: 'Data-Agentti — System Prompt', model: 'qwen-coder', default: 'Olet tietokanta-asiantuntija. Vastaat skeemojen suunnittelusta, SQL-kyselyiden optimoinnista ja datamalleista.' },
|
||||||
|
qa: { name: 'QA — System Prompt', model: 'smollm-135m', default: 'Olet laadunvarmistaja (QA). Kirjoitat testejä, etsit virheitä ja varmistat, että kaikki reunatapaukset on huomioitu.' },
|
||||||
|
tester: { name: 'DevOps — System Prompt', model: 'smollm-135m', default: 'Olet DevOps-insinööri. Vastaat koodin julkaisuputkista, serveri-infrastruktuurista ja ympäristön suorituskyvystä.' },
|
||||||
|
};
|
||||||
|
const selectedAgents = new Set();
|
||||||
|
let sharedPrompt = localStorage.getItem('kpn-shared-prompt') || '';
|
||||||
|
|
||||||
|
// Ladataan tallennetut promptit localStoragesta
|
||||||
|
for (const key of Object.keys(agentPrompts)) {
|
||||||
|
const saved = localStorage.getItem('kpn-agent-prompt-' + key);
|
||||||
|
if (saved) agentPrompts[key].prompt = saved;
|
||||||
|
else agentPrompts[key].prompt = agentPrompts[key].default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePromptEditor() {
|
||||||
|
const editor = document.getElementById('agent-prompt-editor');
|
||||||
|
const nameEl = document.getElementById('agent-prompt-name');
|
||||||
|
const textEl = document.getElementById('agent-prompt-text');
|
||||||
|
const sharedEl = document.getElementById('shared-prompt-section');
|
||||||
|
const btnAll = document.getElementById('btn-toggle-all');
|
||||||
|
|
||||||
|
if (btnAll) {
|
||||||
|
if (selectedAgents.size === Object.keys(agentPrompts).length) {
|
||||||
|
btnAll.textContent = 'Tyhjennä valinnat';
|
||||||
|
} else {
|
||||||
|
btnAll.textContent = 'Valitse kaikki';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAgents.size === 0) {
|
||||||
|
editor.classList.remove('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.classList.add('visible');
|
||||||
|
|
||||||
|
if (selectedAgents.size === 1) {
|
||||||
|
const agent = [...selectedAgents][0];
|
||||||
|
const cfg = agentPrompts[agent];
|
||||||
|
nameEl.textContent = cfg.name;
|
||||||
|
textEl.value = cfg.prompt;
|
||||||
|
sharedEl.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
const names = [...selectedAgents].map(a => agentPrompts[a].name.split(' — ')[0]);
|
||||||
|
nameEl.textContent = names.join(' + ') + ' — Yhteinen konteksti';
|
||||||
|
textEl.value = sharedPrompt;
|
||||||
|
sharedEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.selectAgent = function(agent) {
|
||||||
|
const card = document.querySelector(`[data-agent="${agent}"]`);
|
||||||
|
|
||||||
|
if (selectedAgents.has(agent)) {
|
||||||
|
selectedAgents.delete(agent);
|
||||||
|
card.classList.remove('selected');
|
||||||
|
card.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
selectedAgents.add(agent);
|
||||||
|
card.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePromptEditor();
|
||||||
|
if (selectedAgents.size > 0) {
|
||||||
|
document.getElementById('agent-prompt-text')?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleAllAgents = function() {
|
||||||
|
const allAgents = Object.keys(agentPrompts);
|
||||||
|
if (selectedAgents.size === allAgents.length) {
|
||||||
|
selectedAgents.clear();
|
||||||
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('selected', 'active'));
|
||||||
|
} else {
|
||||||
|
allAgents.forEach(a => {
|
||||||
|
selectedAgents.add(a);
|
||||||
|
const card = document.querySelector(`[data-agent="${a}"]`);
|
||||||
|
if (card) card.classList.add('selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updatePromptEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Autosave prompti
|
||||||
|
document.getElementById('agent-prompt-text')?.addEventListener('input', (e) => {
|
||||||
|
if (selectedAgents.size === 0) return;
|
||||||
|
const saved = document.getElementById('agent-prompt-saved');
|
||||||
|
|
||||||
|
if (selectedAgents.size === 1) {
|
||||||
|
const agent = [...selectedAgents][0];
|
||||||
|
agentPrompts[agent].prompt = e.target.value;
|
||||||
|
localStorage.setItem('kpn-agent-prompt-' + agent, e.target.value);
|
||||||
|
} else {
|
||||||
|
sharedPrompt = e.target.value;
|
||||||
|
localStorage.setItem('kpn-shared-prompt', e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
saved.style.opacity = '1';
|
||||||
|
clearTimeout(saved._t);
|
||||||
|
saved._t = setTimeout(() => saved.style.opacity = '0', 1500);
|
||||||
|
});
|
||||||
|
|
||||||
window.switchMainTab = function(tab) {
|
window.switchMainTab = function(tab) {
|
||||||
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));
|
||||||
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active'));
|
||||||
@@ -779,9 +1014,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// URL-hash navigointi: #codelab tai #network
|
// URL-hash navigointi
|
||||||
if (window.location.hash === '#codelab') {
|
const initHash = window.location.hash.replace('#', '');
|
||||||
switchMainTab('codelab');
|
if (['codelab', 'agents'].includes(initHash)) {
|
||||||
|
switchMainTab(initHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synkronoi coder-status kun WS on jo auki (suora #codelab navigointi)
|
// Synkronoi coder-status kun WS on jo auki (suora #codelab navigointi)
|
||||||
@@ -1020,6 +1256,142 @@
|
|||||||
coderEl.style.color = '#f85149';
|
coderEl.style.color = '#f85149';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Terminaalin komentorivi
|
||||||
|
const termInput = document.getElementById('term-input');
|
||||||
|
const termPanel = document.getElementById('agent-terminal');
|
||||||
|
const termHistory = [];
|
||||||
|
let termHistIdx = -1;
|
||||||
|
|
||||||
|
function termLog(html, color) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'terminal-line';
|
||||||
|
if (color) div.style.color = color;
|
||||||
|
div.innerHTML = html;
|
||||||
|
termPanel.appendChild(div);
|
||||||
|
while (termPanel.children.length > 100) termPanel.removeChild(termPanel.firstChild);
|
||||||
|
termPanel.scrollTop = termPanel.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kpnRun(model, prompt) {
|
||||||
|
termLog(` → <span style="color:#58a6ff">${model}</span> käsittelee...`, '#8b949e');
|
||||||
|
try {
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
// Liitetään yhteinen konteksti + agentin oma system prompt
|
||||||
|
const agent = Object.values(agentPrompts).find(a => a.model === model);
|
||||||
|
const parts = [];
|
||||||
|
if (sharedPrompt) parts.push(sharedPrompt);
|
||||||
|
if (agent && agent.prompt) parts.push(agent.prompt);
|
||||||
|
parts.push(prompt);
|
||||||
|
const fullPrompt = parts.join('\n\n');
|
||||||
|
const res = await fetch('/api/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model, prompt: fullPrompt, task_id: taskId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
termLog(` ✗ Virhe: ${res.status} ${res.statusText}`, '#f85149');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
const response = (data.response || '').trim();
|
||||||
|
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(` ${response.replace(/</g,'<').replace(/\n/g,'\n ')}`, '#c9d1d9');
|
||||||
|
} catch (e) {
|
||||||
|
termLog(` ✗ ${e.message}`, '#f85149');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function termExec(cmd) {
|
||||||
|
termLog(`<span class="terminal-prompt">$</span> ${cmd.replace(/</g,'<')}`);
|
||||||
|
termHistory.unshift(cmd);
|
||||||
|
termHistIdx = -1;
|
||||||
|
|
||||||
|
const parts = cmd.trim().split(/\s+/);
|
||||||
|
if (parts[0] !== 'kpn') {
|
||||||
|
termLog('kpn: tuntematon komento. Kokeile: kpn help', '#f85149');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sub = parts[1];
|
||||||
|
|
||||||
|
if (sub === 'help' || !sub) {
|
||||||
|
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
|
||||||
|
termLog(' kpn run <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff');
|
||||||
|
termLog(' kpn status — verkon tila', '#a5d6ff');
|
||||||
|
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
|
||||||
|
termLog(' kpn clear — tyhjennä terminaali', '#a5d6ff');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'clear') {
|
||||||
|
termPanel.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'status') {
|
||||||
|
const nodes = statNodes.textContent || '0';
|
||||||
|
const vram = statVram.textContent || '?';
|
||||||
|
termLog(` Solmuja: ${nodes} | VRAM: ${vram} | Tehtäviä: ${statTasks.textContent || '0'}`, '#a5d6ff');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'models') {
|
||||||
|
termLog(' smollm-135m — SmolLM 135M (kevyt)', '#a5d6ff');
|
||||||
|
termLog(' qwen-05b — Qwen2.5 0.5B', '#a5d6ff');
|
||||||
|
termLog(' phi3-mini — Phi-3 Mini', '#a5d6ff');
|
||||||
|
termLog(' qwen-coder — Qwen2.5-Coder 0.5B', '#a5d6ff');
|
||||||
|
termLog(' qwen-coder-3b — Qwen2.5-Coder 3B', '#a5d6ff');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'hello') {
|
||||||
|
kpnRun('smollm-135m', 'Tervehdi käyttäjää iloisesti ja lyhyesti suomeksi. Ole innostunut ja energinen! Vastaa yhdellä lauseella.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'run') {
|
||||||
|
const model = parts[2];
|
||||||
|
const afterModel = cmd.replace(/^kpn\s+run\s+\S+\s*/, '');
|
||||||
|
const promptMatch = afterModel.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
|
||||||
|
const prompt = (promptMatch && (promptMatch[1] || promptMatch[2] || promptMatch[3] || '')).trim();
|
||||||
|
|
||||||
|
if (!model || !prompt) {
|
||||||
|
termLog(' Käyttö: kpn run <malli> "<prompti>"', '#f85149');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kpnRun(model, prompt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149');
|
||||||
|
}
|
||||||
|
|
||||||
|
termInput?.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const cmd = termInput.value.trim();
|
||||||
|
if (cmd) termExec(cmd);
|
||||||
|
termInput.value = '';
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (termHistIdx < termHistory.length - 1) {
|
||||||
|
termHistIdx++;
|
||||||
|
termInput.value = termHistory[termHistIdx];
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (termHistIdx > 0) {
|
||||||
|
termHistIdx--;
|
||||||
|
termInput.value = termHistory[termHistIdx];
|
||||||
|
} else {
|
||||||
|
termHistIdx = -1;
|
||||||
|
termInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klikkaa terminaalipaneelia → fokusoi input
|
||||||
|
termPanel?.addEventListener('click', () => termInput?.focus());
|
||||||
|
|
||||||
uiSocket.onmessage = (event) => {
|
uiSocket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const raw = event.data;
|
const raw = event.data;
|
||||||
@@ -1281,9 +1653,8 @@
|
|||||||
}
|
}
|
||||||
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
|
||||||
const model = data.model || '';
|
const model = data.model || '';
|
||||||
if (model.includes('coder')) document.getElementById('avatar-coder')?.classList.add('active');
|
if (model.includes('coder') || model.includes('Coder')) document.getElementById('avatar-coder')?.classList.add('active');
|
||||||
else document.getElementById('avatar-smol')?.classList.add('active');
|
else document.getElementById('avatar-tester')?.classList.add('active');
|
||||||
document.getElementById('avatar-discord')?.classList.add('active');
|
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
|
|||||||