Files
agentic-studio/network-poc/static/index.html
Jaakko Vanhala cbbf427a93 Tab-completion kpn-terminaaliin: ennustava komennonsyöttö sana kerrallaan
TAB täydentää kontekstin mukaan:
- tyhjä → "kpn "
- "kpn " → "kpn help", "kpn run", "kpn pipeline" jne.
- "kpn run " → agentit ja mallit (coder, manager, qwen-coder...)
- "kpn run coder " → esimerkkiprompteja ("hello world in python")
- "kpn pi" → "kpn pipeline "
- osittainen sana → yhteinen etuliite tai ainoa vaihtoehto

Tukee myös kpn pipeline -esimerkkiprompteja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:51:01 +03:00

2949 lines
154 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kipinä Agentic Playground</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<style>
:root {
--bg-color: #0d1117;
--panel-bg: #161b22;
--text-color: #c9d1d9;
--accent-color: #58a6ff;
--success-color: #3fb950;
--border-color: #30363d;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
flex-direction: column;
box-sizing: border-box;
}
.container {
background-color: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
width: 100%;
max-width: 1400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
text-align: center;
margin-bottom: 20px;
}
.device-info {
background-color: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 20px;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
color: #8b949e;
text-align: left;
display: none;
}
.device-info span { color: var(--text-color); }
.dashboard-panel {
background-color: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.stat-box {
text-align: center;
flex-grow: 1;
}
.stat-box h3 {
margin: 0;
color: var(--accent-color);
font-size: 28px;
}
.stat-box p {
margin: 5px 0 0 0;
font-size: 14px;
color: #8b949e;
}
.slider-container {
margin: 20px 0;
text-align: left;
}
input[type=range] {
width: 100%;
margin-top: 10px;
accent-color: var(--accent-color);
}
h1 { margin-bottom: 5px; }
h1 span { color: var(--accent-color); }
.sub { color: #8b949e; margin-bottom: 25px; }
.main-tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 0;
}
.main-tab {
padding: 10px 20px;
font-size: 15px;
font-weight: 500;
color: #8b949e;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
}
.main-tab:hover { color: var(--text-color); }
.main-tab.active { color: var(--accent-color); border-bottom-color: var(--accent-color); }
.main-panel { display: none; }
.main-panel.active { display: block; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } }
.code-output {
font-family: 'Courier New', Courier, monospace;
background: #010409;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 14px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.code-output .hljs { background: transparent; padding: 0; }
.code-task-card {
background: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 14px;
margin-bottom: 12px;
}
.code-task-card .prompt { color: #d29922; font-size: 14px; margin-bottom: 10px; }
.code-task-card .meta { color: #8b949e; font-size: 12px; margin-top: 10px; }
.code-step {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #8b949e;
padding: 6px 0;
}
.code-step.active { color: var(--accent-color); }
.code-step.done { color: var(--success-color); }
.code-step.error { color: #f85149; }
.step-icon { font-size: 16px; width: 20px; text-align: center; }
.status-box {
font-family: 'Courier New', Courier, monospace;
background-color: #010409;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
height: 120px;
overflow-y: auto;
text-align: left;
}
.status-box p {
margin: 0 0 5px 0;
color: var(--success-color);
font-size: 14px;
}
.btn {
background-color: #238636;
color: #ffffff;
border: 1px solid rgba(240, 246, 252, 0.1);
border-radius: 6px;
padding: 10px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn:hover { background-color: #2ea043; }
.hidden { display: none; }
.compat-banner {
border-radius: 6px;
padding: 14px 18px;
margin-bottom: 20px;
font-size: 14px;
line-height: 1.6;
display: none;
}
.compat-banner.gpu {
background: #23392020;
border: 1px solid #3fb95040;
color: var(--success-color);
}
.compat-banner.cpu {
background: #d2992215;
border: 1px solid #d2992240;
color: #d29922;
}
.compat-banner code {
background: #0d1117;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
color: var(--text-color);
}
.compat-banner summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.compat-banner details[open] summary {
margin-bottom: 10px;
}
.chat-box {
background-color: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
height: 500px;
overflow-y: auto;
text-align: left;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.chat-msg {
background-color: #0d1117;
padding: 12px;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
font-size: 15px;
}
.chat-prompt {
color: #8b949e;
font-size: 13px;
margin-bottom: 5px;
display: block;
}
.token-detail {
background: #010409;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px 12px;
margin-top: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.8;
display: none;
}
.token-detail.visible { display: block; }
.token-detail .tok {
background: #1c2333;
border: 1px solid #30363d;
border-radius: 3px;
padding: 2px 5px;
margin: 2px;
display: inline-block;
color: var(--text-color);
}
.token-detail .tok-en { border-color: #58a6ff44; }
.token-detail .tok-fi { border-color: #d2992244; }
.toggle-tokens {
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
color: #8b949e;
font-size: 12px;
padding: 3px 8px;
cursor: pointer;
}
.toggle-tokens:hover { color: var(--text-color); border-color: #8b949e; }
.task-option {
background: var(--panel-bg);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 14px;
cursor: pointer;
transition: border-color 0.2s;
position: relative;
}
.task-option:hover { border-color: #8b949e; }
.task-option.selected { border-color: var(--accent-color); background: #58a6ff10; }
.task-title { font-weight: 600; font-size: 15px; color: var(--text-color); margin-bottom: 4px; }
.task-desc { font-size: 12px; color: #8b949e; line-height: 1.4; margin-bottom: 8px; }
.task-size { font-size: 11px; color: #6e7681; }
.task-badge {
position: absolute;
top: 10px;
right: 10px;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
}
.task-ready { background: #23392050; color: var(--success-color); border: 1px solid #23392080; }
.task-soon { background: #d2992215; color: #d29922; border: 1px solid #d2992240; }
.task-info {
display: none;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border-color);
font-size: 12px;
line-height: 1.6;
color: #8b949e;
}
.task-info strong { color: var(--text-color); }
.task-info em { color: var(--accent-color); font-style: normal; }
.task-option.selected .task-info { display: block; }
.download-bar {
background: #0d1117;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 16px;
display: none;
}
.download-bar .bar-track {
background: #21262d;
border-radius: 4px;
height: 8px;
margin-top: 8px;
overflow: hidden;
}
.download-bar .bar-fill {
background: var(--accent-color);
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.metric-card {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.metric-val {
font-size: 20px;
font-weight: 700;
color: var(--accent-color);
}
.metric-label {
font-size: 11px;
color: #8b949e;
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;
white-space: pre-wrap;
}
.terminal-line { margin: 4px 0; }
.terminal-prompt { color: #d29922; }
.org-chart {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
perspective: 1000px;
padding: 25px 50px;
}
.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: 510px;
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 {
background: linear-gradient(145deg, rgba(33, 38, 45, 0.4) 0%, rgba(13, 17, 23, 0.8) 100%);
backdrop-filter: blur(12px);
border: 1px solid rgba(240, 246, 252, 0.1);
border-radius: 16px;
padding: 12px 10px;
text-align: center;
width: 130px;
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);
}
@keyframes idle-breathe {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-2px) scale(1.01); }
}
@keyframes talking-head {
0% { transform: scale(1.05) scaleY(1) translateY(0); }
25% { transform: scale(1.05) scaleY(0.96) scaleX(1.02) translateY(2px); }
50% { transform: scale(1.05) scaleY(1.02) scaleX(0.98) translateY(-2px); }
75% { transform: scale(1.05) scaleY(0.97) scaleX(1.01) translateY(1px); }
100% { transform: scale(1.05) scaleY(1) translateY(0); }
}
.avatar-card img {
width: 80px;
height: 80px;
border-radius: 18px;
margin-bottom: 8px;
border: 2px solid rgba(240, 246, 252, 0.1);
transition: all 0.4s ease;
object-fit: cover;
background: #010409;
animation: idle-breathe 4s infinite ease-in-out;
transform-origin: bottom center;
}
.avatar-card.active, .avatar-card.selected {
opacity: 1;
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.selected img {
border-color: var(--accent-color);
box-shadow: 0 0 25px rgba(88, 166, 255, 0.5);
transform: scale(1.05);
animation: none;
}
.avatar-card.active img {
border-color: var(--accent-color);
box-shadow: 0 0 25px rgba(88, 166, 255, 0.8);
animation: talking-head 0.4s infinite ease-in-out;
transform-origin: bottom center;
}
@keyframes talking-head-gallery {
0% { transform: scaleY(1) translateY(0); }
25% { transform: scaleY(0.94) scaleX(1.04) translateY(3px); }
50% { transform: scaleY(1.04) scaleX(0.96) translateY(-3px); }
75% { transform: scaleY(0.96) scaleX(1.02) translateY(1px); }
100% { transform: scaleY(1) translateY(0); }
}
.gallery-head {
width: 55px;
height: 55px;
border-radius: 12px;
border: 2px solid rgba(240, 246, 252, 0.1);
object-fit: cover;
background: #010409;
transition: all 0.3s ease;
opacity: 0.4;
filter: grayscale(80%);
}
.gallery-head.active {
opacity: 1;
filter: grayscale(0%);
border-color: var(--accent-color);
box-shadow: 0 0 15px rgba(88, 166, 255, 0.5);
transform-origin: bottom center;
}
@keyframes confused-shake {
0% { transform: translateX(0); }
25% { transform: translateX(-2px) rotate(-3deg); }
50% { transform: translateX(0); }
75% { transform: translateX(2px) rotate(3deg); }
100% { transform: translateX(0); }
}
.gallery-head-wrap[data-tooltip]::before {
content: attr(data-tooltip);
position: absolute;
bottom: 110%;
left: 50%;
transform: translateX(-50%);
background: rgba(13, 17, 23, 0.95);
color: #f0f6fc;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
white-space: pre-wrap;
width: 140px;
text-align: left;
border: 1px solid var(--border-color);
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.gallery-head-wrap:hover[data-tooltip]:not([data-tooltip=""])::before { opacity: 1; }
/* Yhteiset kuplasäännöt */
.gallery-head-wrap.state-question::after,
.gallery-head-wrap.state-alert::after,
.gallery-head-wrap.active:not(.state-question):not(.state-alert)::after {
position: absolute;
top: -10px;
right: -10px;
font-size: 14px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
z-index: 10;
}
/* State: Kysymys (Oranssi ?) */
.gallery-head-wrap.state-question::after {
content: '?';
color: #ffffff;
font-weight: 900;
font-family: system-ui, -apple-system, sans-serif;
animation: speech-pulse 1s infinite alternate;
background: #d29922; border: 1px solid #e3a830;
}
.gallery-head.state-question {
border-color: #d29922; box-shadow: 0 0 15px rgba(210, 153, 34, 0.4);
animation: confused-shake 2s infinite ease-in-out; filter: grayscale(10%); opacity: 0.9;
}
/* State: Alert (Punainen !) */
.gallery-head-wrap.state-alert::after {
content: '!';
color: #ffffff;
font-weight: 900;
font-family: system-ui, -apple-system, sans-serif;
animation: speech-pulse 0.5s infinite alternate;
background: #da3633; border: 1px solid #ff7b72;
}
.gallery-head.state-alert {
border-color: #ff4444; box-shadow: 0 0 15px rgba(255, 68, 68, 0.5);
animation: confused-shake 0.5s infinite; filter: grayscale(30%); opacity: 0.9;
}
.gallery-head-wrap { position: relative; display: inline-block; cursor: help; }
@keyframes speech-pulse {
0% { transform: scale(0.8) translateY(0); opacity: 0.6; }
50% { transform: scale(1.1) translateY(-2px); opacity: 1; }
100% { transform: scale(0.8) translateY(0); opacity: 0.6; }
}
.gallery-head-wrap.active:not(.state-question):not(.state-alert)::after {
content: '💬';
background: #0d1117;
border: 1px solid var(--accent-color);
}
.avatar-name { font-weight: 700; font-size: 13px; color: #f0f6fc; letter-spacing: 0.5px; margin-bottom: 2px; }
.avatar-role { font-size: 10px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; line-height: 1.2; word-wrap: break-word; }
.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); }
.lang-selector { display: flex; gap: 6px; background: #010409; padding: 4px; border-radius: 6px; border: 1px solid var(--border-color); }
.lang-btn { background: transparent; border: none; color: #8b949e; font-size: 11px; font-weight: 600; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; }
.lang-btn:hover { color: #c9d1d9; }
.lang-btn.active { background: rgba(88, 166, 255, 0.15); color: var(--accent-color); }
@media (max-width: 768px) {
body { padding: 5px; margin: 0; }
.container { padding: 15px; border: none; border-radius: 0; border-bottom: 1px solid var(--border-color); }
.dashboard-panel { flex-direction: column; gap: 15px; padding: 10px; }
.stat-box { border-right: none !important; border-bottom: 1px solid #30363d; padding-bottom: 10px; }
.stat-box:last-child { border-bottom: none; padding-bottom: 0; }
/* Typography & Header */
h1 { font-size: 22px; }
.sub { font-size: 11px; }
.lang-selector { flex-direction: column; gap: 4px; }
[style*="justify-content: space-between; align-items: flex-start"] { align-items: center !important; }
/* Tabs */
.main-tabs { display: flex; overflow-x: auto; white-space: nowrap; padding-bottom: 5px; margin-bottom: 15px; gap: 10px; }
.main-tab { padding: 8px 10px; font-size: 13px; text-align: center; }
/* Grid optimizations */
#task-selector { grid-template-columns: 1fr !important; }
#metrics-grid { grid-template-columns: 1fr 1fr !important; }
/* Org chart mobile tweaks */
.org-chart { padding: 20px 10px; }
.org-branch { display: none; }
.org-connector { margin-bottom: 10px; height: 20px; }
.org-level { flex-wrap: wrap; justify-content: center; gap: 15px !important; }
#avatar-observer { display: block; position: relative !important; right: auto !important; top: auto !important; margin: 0 auto; margin-bottom: 15px; }
/* Avatar cards downscaling */
.avatar-card { width: 100px; padding: 8px 4px; }
.avatar-card img { width: 55px; height: 55px; margin-bottom: 4px; border-radius: 12px; }
.avatar-name { font-size: 11px; margin-bottom: 1px; }
.avatar-role { font-size: 8px; line-height: 1.1; }
/* User Input Area */
#user-input-box > div { flex-direction: column; }
#send-btn { width: 100%; padding: 12px; }
#code-input-container { flex-direction: column !important; }
#code-send-btn { width: 100%; margin-top: 5px; }
}
</style>
</head>
<body>
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
<div>
<h1 style="margin-bottom:0;" data-i18n="main_title"><span style="color:#ff6b00">Kipinä</span> <span>Agentic Playground</span></h1>
<p class="sub" style="margin-bottom:0;"><span data-i18n="main_subtitle">Hajautettu WebGPU Laskentaverkko</span> · <span id="hub-version" style="color:#58a6ff">-</span></p>
</div>
<div class="lang-selector">
<button class="lang-btn active" onclick="setLanguage('fi')" data-lang="fi">FI</button>
<button class="lang-btn" onclick="setLanguage('se')" data-lang="se">SE</button>
<button class="lang-btn" onclick="setLanguage('en')" data-lang="en">EN</button>
</div>
</div>
<!-- Päävälilehdet -->
<div class="main-tabs">
<div class="main-tab active" onclick="switchMainTab('network')" data-i18n="tab_network">Laskentaverkko</div>
<div class="main-tab" onclick="switchMainTab('codelab')" data-i18n="tab_codelab">Koodilaboratorio</div>
<div class="main-tab" onclick="switchMainTab('agents')" data-i18n="tab_agents">Kipinä Agentic Playground</div>
</div>
<!-- PANEELI 1: Laskentaverkko -->
<div id="panel-network" class="main-panel active">
<!-- Global Cluster Statistics (UI) -->
<div class="dashboard-panel">
<div class="stat-box" style="border-right: 1px solid #30363d;">
<h3 id="stat-nodes">0</h3>
<p data-i18n="stat_nodes_lbl">Aktiivisia Nodeja</p>
</div>
<div class="stat-box" style="border-right: 1px solid #30363d;">
<h3 id="stat-tasks">0</h3>
<p data-i18n="stat_tasks_lbl">Verkossa Suoritettua Tehtävää (Globaali)</p>
</div>
<div class="stat-box">
<h3 id="stat-vram">0 GB</h3>
<p data-i18n="stat_vram_lbl">Verkon yhteis-VRAM</p>
</div>
</div>
<div id="device-info" class="device-info"></div>
<div id="compat-banner" class="compat-banner"></div>
<div id="initial-state">
<!-- Tehtävävalitsin -->
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px;text-align:left">
<div style="font-weight:600;font-size:15px;margin-bottom:12px" data-i18n="task_title">Valitse tehtävä</div>
<div id="task-selector" style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<label class="task-option selected" data-task="tokenize">
<input type="radio" name="task" value="tokenize" checked style="display:none">
<div class="task-title">Tokenisointivertailu</div>
<div class="task-desc">EN/FI-kieliparien tokenisointitehokkuuden vertailu Qwen2.5-tokenizeria käyttäen</div>
<div class="task-size">Lataus: ~7 MB (tokenizer)</div>
<span class="task-badge task-ready">Valmis</span>
<div class="task-info">
<strong>Miten tokenisaatio toimii?</strong>
Kielimallit eivät lue tekstiä kirjain kerrallaan. Sen sijaan teksti pilkotaan <em>tokeneiksi</em> — sanoja, tavuja tai sananosia, joista jokaisella on oma numerotunnisteensa mallin sanastossa.
<br><br>
Tokenizer on <em>BPE</em> (Byte Pair Encoding) -algoritmi: se yhdistää yleisimpiä merkkipareja isommiksi yksiköiksi. Englannissa "the" on yksi token, mutta suomessa "kirjoittamisen" voi olla 3-4 tokenia, koska tokenizer on koulutettu pääosin englanninkielisellä datalla.
<br><br>
<strong>Miksi tällä on väliä?</strong> Enemmän tokeneita = kalliimpaa ja hitaampaa. Sama lause suomeksi voi maksaa 50-100% enemmän tokeneita kuin englanniksi.
</div>
</label>
<label class="task-option" data-task="smollm-135m">
<input type="radio" name="task" value="smollm-135m" style="display:none">
<div class="task-title">SmolLM 135M</div>
<div class="task-desc">Kevyt kielimalli tekstigeneraatioon — sopii kaikille laitteille (CPU)</div>
<div class="task-size">Lataus: ~269 MB (safetensors) + 2 MB (tokenizer)</div>
<span class="task-badge task-ready">Valmis</span>
<div class="task-info">
<strong>SmolLM 135M</strong> (HuggingFace)
<br>Llama-arkkitehtuuri: 30 kerrosta, 576-dim embeddings, 9 attention-headiä.
<br><br>
135 miljoonaa parametria — noin 1000x pienempi kuin GPT-4. Silti kykenee yksinkertaiseen tekstigeneraatioon. Tämä malli mahtuu mihin tahansa laitteeseen ja pyörii kokonaan selaimessasi WebAssemblylla.
<br><br>
<strong>Miten inferenssi toimii?</strong> Malli ennustaa aina seuraavan tokenin edellisten perusteella (<em>autoregressive generation</em>). Jokainen token vaatii yhden "forward pass" -laskennan kaikkien kerrosten läpi. 135M-mallilla tämä kestää ~0.8s selaimessa ja ~90ms natiivisti.
</div>
</label>
<label class="task-option" data-task="qwen-05b">
<input type="radio" name="task" value="qwen-05b" style="display:none">
<div class="task-title">Qwen2.5 0.5B</div>
<div class="task-desc">Tehokkaampi kielimalli — vaatii vähintään 2 GB muistia (CPU)</div>
<div class="task-size">Lataus: ~990 MB (safetensors) + 7 MB (tokenizer)</div>
<span class="task-badge task-ready">Valmis</span>
<div class="task-info">
<strong>Qwen2.5 0.5B</strong> (Alibaba Cloud)
<br>24 kerrosta, 896-dim, 14 attention-headiä, 2 KV-headiä (GQA).
<br><br>
490 miljoonaa parametria ja 151 936 tokenin sanasto — 3x suurempi kuin SmolLM ja huomattavasti koherentimpi. <em>Grouped Query Attention</em> (GQA) vähentää muistinkäyttöä jakamalla key/value-headit 14:n query-headin kesken.
<br><br>
<strong>Miksi tämä on hitaampi?</strong> Jokaisessa kerroksessa lasketaan attention-matriisi (Q*K^T), joka skaalautuu O(n^2) sekvenssipituuden mukaan. 24 kerrosta x 14 headiä = 336 attention-laskentaa per token. Selaimessa CPU/Wasm: ~2.5s/token, natiivisti: ~90ms/token.
</div>
</label>
<label class="task-option" data-task="phi3-mini">
<input type="radio" name="task" value="phi3-mini" style="display:none">
<div class="task-title">Phi-3 Mini 3.8B</div>
<div class="task-desc">Iso kielimalli — vaatii native-noden (Docker + GPU)</div>
<div class="task-size">~7.6 GB — liian suuri selaimelle</div>
<span class="task-badge task-soon">Vain native</span>
<div class="task-info">
<strong>Phi-3 Mini 3.8B</strong> (Microsoft)
<br>32 kerrosta, 3072-dim, 32 attention-headiä.
<br><br>
3.8 miljardia parametria — luokassaan yksi tehokkaimmista. Microsoftin "small language model" -tutkimuksen tulos: laadukas koulutusdata kompensoi pientä mallikokoa. Pärjää monissa tehtävissä 7B-13B mallien tasolla.
<br><br>
<strong>Miksi ei pyöri selaimessa?</strong> F32-painot vaativat ~15 GB muistia. Selainten Wasm-muistiraja on tyypillisesti 4 GB. GPU-kiihdytyksellä (CUDA/ROCm) malli mahtuu 24 GB VRAM-näytönohjaimeen ja generoi ~50-100 tok/s.
</div>
</label>
</div>
</div>
<button id="start-btn" class="btn" data-i18n="btn_join">Liity laskentaverkkoon</button>
</div>
<div id="active-state" class="hidden">
<div id="download-bar" class="download-bar">
<div style="display:flex;justify-content:space-between;font-size:13px">
<span id="dl-label">Ladataan mallia...</span>
<span id="dl-pct" style="color:var(--accent-color);font-weight:600">0%</span>
</div>
<div class="bar-track"><div id="dl-fill" class="bar-fill" style="width:0%"></div></div>
<div id="dl-detail" style="font-size:11px;color:#8b949e;margin-top:4px">0 / 0 MB</div>
</div>
<!-- Resurssipaneeli -->
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-weight:600;font-size:15px" data-i18n="resource_mgmt">Resurssien hallinta</span>
<span id="node-status" style="font-size:12px;color:#8b949e">Ei yhdistetty</span>
</div>
<!-- Kuormitussäädin -->
<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
<span data-i18n="power_limiter">Laskentatehon rajoitin</span>
<strong id="load-display" style="color:var(--accent-color)">50%</strong>
</div>
<input type="range" id="gpu-load" min="0" max="100" value="50" style="width:100%;accent-color:var(--accent-color)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:2px">
<span>Pysäytetty</span><span>Säästö</span><span>Tasapaino</span><span>Suorituskyky</span><span>Maksimi</span>
</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)">
<span data-i18n="auto_tasks">Vastaanota automaattisia tehtäviä hubilta</span>
</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">
<div class="metric-val" id="m-tasks">0</div>
<div class="metric-label" data-i18n="metric_tasks">Tehtäviä</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-avg-time">-</div>
<div class="metric-label" data-i18n="metric_avg">Ka. aika</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-tokens">0</div>
<div class="metric-label" data-i18n="metric_tokens">Tokeneita</div>
</div>
<div class="metric-card">
<div class="metric-val" id="m-uptime">0s</div>
<div class="metric-label" data-i18n="metric_uptime">Käynnissä</div>
</div>
</div>
</div>
<div id="user-input-box" class="hidden" style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:12px;margin-bottom:12px">
<div style="font-size:13px;color:#8b949e;margin-bottom:8px" data-i18n="try_own_text">Kokeile omaa tekstiä:</div>
<div style="display:flex;gap:8px">
<input type="text" id="user-text" placeholder="Kirjoita teksti tokenisoitavaksi tai promptiksi..." style="flex:1;background:var(--panel-bg);border:1px solid var(--border-color);border-radius:4px;padding:8px 12px;color:var(--text-color);font-size:14px;outline:none">
<button id="send-btn" style="background:#238636;color:#fff;border:1px solid rgba(240,246,252,0.1);border-radius:4px;padding:8px 16px;font-size:14px;cursor:pointer;white-space:nowrap" data-i18n="btn_tokenize">Tokenisoi</button>
</div>
</div>
<div id="chat-box" class="chat-box hidden">
<div style="color: #8b949e; text-align: center; margin-top: 80px;">Odotetaan Generointitehtäviä Hubilta...</div>
</div>
<div id="log-box" class="status-box">
<p>> Odotetaan uusia tehtäviä Hubulta...</p>
</div>
</div>
</div><!-- /panel-network -->
<!-- PANEELI 2: Koodilaboratorio -->
<div id="panel-codelab" class="main-panel">
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600;font-size:15px">Qwen2.5-Coder-0.5B-Instruct</span>
<span id="coder-status" style="font-size:12px;color:#8b949e">Ei yhdistetty</span>
</div>
<p style="font-size:12px;color:#8b949e;line-height:1.5;margin-bottom:12px">
Code-specialized language model trained on 5.5T tokens of source code.
Generates Python code in your browser via WebAssembly. Choose model size and write your own prompt.
</p>
<!-- Model size selector -->
<div style="display:flex;gap:8px;margin-bottom:10px">
<label style="flex:1;display:flex;align-items:center;gap:6px;background:var(--panel-bg);border:2px solid var(--accent-color);border-radius:4px;padding:8px 12px;cursor:pointer;font-size:13px" id="coder-opt-05b">
<input type="radio" name="coder-size" value="05b" checked style="accent-color:var(--accent-color)">
<div>
<strong style="color:var(--text-color)">0.5B</strong>
<span style="color:#8b949e"> — 990 MB, ~0.4 tok/s</span>
</div>
</label>
<label style="flex:1;display:flex;align-items:center;gap:6px;background:var(--panel-bg);border:2px solid var(--border-color);border-radius:4px;padding:8px 12px;cursor:pointer;font-size:13px" id="coder-opt-3b">
<input type="radio" name="coder-size" value="3b" style="accent-color:var(--accent-color)">
<div>
<strong style="color:var(--text-color)">3B</strong>
<span style="color:#8b949e"> — 6.2 GB, better quality, slower</span>
</div>
</label>
</div>
<div style="display:flex;gap:8px;align-items:start">
<div style="flex:1">
<div style="display:flex;gap:8px;margin-bottom:4px">
<input type="text" id="code-input" placeholder='e.g. Write a Python function that checks if a number is prime' style="flex:1;background:var(--panel-bg);border:1px solid var(--border-color);border-radius:4px;padding:8px 12px;color:var(--text-color);font-size:14px;outline:none;display:block" >
<textarea id="code-input-json" placeholder='{"prompt":"Write a fibonacci function","system":"You are a Python expert","max_tokens":128}' style="flex:1;background:var(--panel-bg);border:1px solid var(--border-color);border-radius:4px;padding:8px 12px;color:var(--text-color);font-size:13px;font-family:Courier New,monospace;outline:none;resize:vertical;min-height:60px;display:none"></textarea>
<button id="code-send-btn" style="background:#238636;color:#fff;border:1px solid rgba(240,246,252,0.1);border-radius:4px;padding:8px 16px;font-size:14px;cursor:pointer;align-self:stretch">Generate</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<label style="font-size:11px;color:#8b949e;cursor:pointer;display:flex;align-items:center;gap:4px">
<input type="checkbox" id="json-mode-toggle" style="accent-color:var(--accent-color)"> JSON mode
</label>
<details id="json-help" style="font-size:11px;color:#8b949e;display:none">
<summary style="cursor:pointer;color:var(--accent-color)">JSON syntax</summary>
<div style="background:#010409;border:1px solid var(--border-color);border-radius:4px;padding:10px;margin-top:6px;font-family:Courier New,monospace;font-size:12px;line-height:1.6;color:var(--text-color)">
{<br>
&nbsp;&nbsp;<span style="color:#79c0ff">"prompt"</span>: <span style="color:#a5d6ff">"Write a bubble sort"</span>,<br>
&nbsp;&nbsp;<span style="color:#79c0ff">"system"</span>: <span style="color:#a5d6ff">"You are a Python expert. Write only code."</span>,<br>
&nbsp;&nbsp;<span style="color:#79c0ff">"max_tokens"</span>: <span style="color:#79c0ff">128</span>,<br>
&nbsp;&nbsp;<span style="color:#79c0ff">"language"</span>: <span style="color:#a5d6ff">"python"</span><br>
}
<div style="margin-top:8px;color:#8b949e;font-family:sans-serif">
<strong style="color:var(--text-color)">Fields:</strong><br>
<code>prompt</code> (required) — the coding task<br>
<code>system</code> — system prompt override<br>
<code>max_tokens</code> — max tokens to generate (default: 128)<br>
<code>language</code> — hint for syntax highlighting
</div>
</div>
</details>
</div>
</div>
</div>
<div id="code-loading" style="display:none;margin-top:8px;font-size:12px;color:#d29922">Starting Coder model...</div>
</div>
<!-- Koodilaboratorion metriikat -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px">
<div class="metric-card">
<div class="metric-val" id="code-m-tasks">0</div>
<div class="metric-label">Tehtäviä</div>
</div>
<div class="metric-card">
<div class="metric-val" id="code-m-tokens">0</div>
<div class="metric-label">Tokeneita</div>
</div>
<div class="metric-card">
<div class="metric-val" id="code-m-speed">-</div>
<div class="metric-label">tok/s</div>
</div>
</div>
<!-- Latausvaiheet -->
<div id="code-pipeline" style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px;display:none">
<div style="font-size:13px;font-weight:600;margin-bottom:12px">Valmistautuminen</div>
<div id="code-steps" style="display:flex;flex-direction:column;gap:8px">
<div class="code-step" id="step-wasm">
<span class="step-icon">&#9711;</span>
<span>WebAssembly-ytimen lataus</span>
</div>
<div class="code-step" id="step-tokenizer">
<span class="step-icon">&#9711;</span>
<span>Tokenizer (7 MB)</span>
</div>
<div class="code-step" id="step-model">
<span class="step-icon">&#9711;</span>
<span>Qwen2.5-Coder-0.5B painot (990 MB)</span>
<span id="step-model-pct" style="color:var(--accent-color);margin-left:auto;font-size:12px"></span>
</div>
<div class="code-step" id="step-build">
<span class="step-icon">&#9711;</span>
<span>Mallin rakentaminen muistiin</span>
</div>
<div class="code-step" id="step-ready">
<span class="step-icon">&#9711;</span>
<span>Valmis generoimaan</span>
</div>
</div>
</div>
<!-- Kooditulokset -->
<div id="code-results" style="display:flex;flex-direction:column;gap:12px">
<div data-placeholder style="color:#8b949e;text-align:center;padding:40px">Kirjoita ohjelmointitehtävä ja paina Koodaa</div>
</div>
</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.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="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(--text-color)"><span style="color:#ff6b00">Kipinä</span> 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>
</div>
<div class="workspace-split" style="display:flex; gap:20px; align-items:flex-start; flex-wrap: wrap;">
<!-- LEFT COLUMN: Org chart & Prompt Editor -->
<div style="flex:1; min-width:300px; overflow-x:auto;">
<div class="org-chart">
<!-- Taso 1 -->
<div class="org-level">
<div class="avatar-card" id="avatar-client" data-agent="client" onclick="selectAgent('client')">
<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="org-connector"></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% + 350px); 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 class="avatar-card" id="avatar-kpn" data-agent="manager" onclick="selectAgent('manager')">
<img src="/avatars/karhunpentu.png" alt="Manageri (Karhunpentu)">
<div class="avatar-name">Manageri</div>
<div class="avatar-role">KPN CLI</div>
</div>
</div>
<div class="org-connector"></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">SOFTAKEHITYS</div>
</div>
<div class="avatar-card" id="avatar-data" data-agent="data" onclick="selectAgent('data')">
<img src="/avatars/pesukarhu_notext.png" alt="Data-Agentti (Pesukarhu)">
<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>
<!-- Prompt Editor -->
<div class="agent-prompt-editor" id="agent-prompt-editor" style="margin-top:20px;">
<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>
<!-- RIGHT COLUMN: Puhuvat Päät Gallery -->
<div style="flex-basis:150px; flex-shrink:0;">
<div style="background:rgba(1, 4, 9, 0.6); border:1px solid var(--border-color); border-radius:6px; padding:12px; height: 100%;">
<div id="all-heads-gallery" style="display:flex; flex-wrap:wrap; gap:10px; justify-content:center;">
<div class="gallery-head-wrap" id="wrap-client"><img src="/avatars/kettu_notext.png" id="gallery-client" class="gallery-head" alt="Asiakas"></div>
<div class="gallery-head-wrap" id="wrap-observer"><img src="/avatars/aikuinen_susi.png" id="gallery-observer" class="gallery-head" alt="Tarkkailija"></div>
<div class="gallery-head-wrap" id="wrap-manager"><img src="/avatars/karhunpentu.png" id="gallery-manager" class="gallery-head" alt="Manageri"></div>
<div class="gallery-head-wrap" id="wrap-coder"><img src="/avatars/kipina_notext.png" id="gallery-coder" class="gallery-head" alt="Koodari"></div>
<div class="gallery-head-wrap" id="wrap-data"><img src="/avatars/pesukarhu_notext.png" id="gallery-data" class="gallery-head" alt="Data"></div>
<div class="gallery-head-wrap" id="wrap-qa"><img src="/avatars/susi_notext.png" id="gallery-qa" class="gallery-head" alt="QA"></div>
<div class="gallery-head-wrap" id="wrap-tester"><img src="/avatars/laiskiainen_notext.png" id="gallery-tester" class="gallery-head" alt="DevOps"></div>
</div>
<div style="text-align: center; margin-top: 16px;">
<button class="btn" id="simu-btn" onclick="toggleSimulation()" style="font-size: 11px; padding: 4px 10px; background: #0d1a2d; border-color: #58a6ff;">Käynnistä simulaatio</button>
</div>
</div>
</div>
</div>
<div id="agent-hub-status" style="margin-top:20px;padding:8px 14px;background:#0d1117;border:1px solid var(--border-color);border-radius:6px 6px 0 0;font-family:'Courier New',monospace;font-size:13px;display:flex;align-items:center;gap:12px;cursor:help" title="WebSocket-yhteys Kipinä Hubiin — hallitsee tehtävien jakelun ja solmujen koordinoinnin">
<span style="display:flex;align-items:center;gap:6px" title="Hub-yhteyden tila">
<span id="agent-hub-dot" style="width:8px;height:8px;border-radius:50%;background:#d29922;display:inline-block"></span>
<span style="color:#8b949e">Hub:</span>
<span id="agent-hub-label" style="color:#d29922">Yhdistetään...</span>
</span>
<span style="color:#30363d"></span>
<span style="display:flex;align-items:center;gap:6px" id="agent-compute-wrap">
<span id="agent-compute-dot" style="width:8px;height:8px;border-radius:50%;background:#30363d;display:inline-block"></span>
<span style="color:#8b949e">Laskenta:</span>
<span id="agent-compute-label" style="color:#8b949e"></span>
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
</span>
</div>
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
</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 &quot;kirjoita hello world&quot;" spellcheck="false"
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
</div>
</div>
</div><!-- /panel-agents -->
</div>
<script type="module">
import init, { start_agent_node, set_gpu_load, set_auto_tasks } from './pkg/node.js';
// HTML-escape kaikelle käyttäjä-/backendidatalle joka menee innerHTML:ään
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Poistaa system-promptin näkyvästä prompt-tekstistä (agents-pipeline lisää sen alkuun)
function stripSystemPrompt(prompt) {
if (!prompt) return '';
// Poistetaan kaikki ennen viimeistä kappaletta (system + agent promptit erotettu \n\n:llä)
const parts = prompt.split('\n\n');
return parts[parts.length - 1] || prompt;
}
// 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');
const t = window.currentLangDict || { btn_clear_all: 'Tyhjennä valinnat', btn_select_all: 'Valitse kaikki' };
if (btnAll) {
if (selectedAgents.size === Object.keys(agentPrompts).length) {
btnAll.textContent = t.btn_clear_all;
} else {
btnAll.textContent = t.btn_select_all;
}
}
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
// Aseta true kun olet luonut _puhuva.gif tiedostot /avatars -kansioon
window.USE_ANIMATED_GIFS = false;
// Update gallery heads
document.querySelectorAll('.gallery-head').forEach(el => {
el.classList.remove('active');
if (window.USE_ANIMATED_GIFS) {
el.src = el.src.replace('_puhuva.gif', '.png');
}
});
document.querySelectorAll('.gallery-head-wrap').forEach(el => el.classList.remove('active'));
selectedAgents.forEach(agent => {
const gel = document.getElementById('gallery-' + agent);
const wrap = document.getElementById('wrap-' + agent);
if (gel) {
gel.classList.add('active');
if (window.USE_ANIMATED_GIFS) {
gel.src = gel.src.replace('.png', '_puhuva.gif');
}
}
if (wrap) wrap.classList.add('active');
});
checkAgentConfusion();
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 isAll = selectedAgents.size === Object.keys(agentPrompts).length;
const lang = localStorage.getItem('kpn_lang') || 'fi';
let title = "";
if (isAll) {
title = lang === 'fi' ? 'Kaikki agentit — Yhteinen konteksti' : (lang === 'se' ? 'Alla agenter — Delad kontext' : 'All agents — Shared context');
} else if (selectedAgents.size > 2) {
title = lang === 'fi' ? `${selectedAgents.size} agenttia — Yhteinen konteksti` : (lang === 'se' ? `${selectedAgents.size} agenter — Delad kontext` : `${selectedAgents.size} agents — Shared context`);
} else {
const names = [...selectedAgents].map(a => agentPrompts[a].name.split(' — ')[0]);
const suffix = lang === 'fi' ? ' — Yhteinen konteksti' : (lang === 'se' ? ' — Delad kontext' : ' — Shared context');
title = names.join(' + ') + suffix;
}
nameEl.textContent = title;
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);
}
checkAgentConfusion();
saved.style.opacity = '1';
clearTimeout(saved._t);
saved._t = setTimeout(() => saved.style.opacity = '0', 1500);
});
function checkAgentConfusion() {
Object.keys(agentPrompts).forEach(agent => {
const prompt = agentPrompts[agent].prompt || "";
const wrap = document.getElementById('wrap-' + agent);
const gel = document.getElementById('gallery-' + agent);
if (!wrap || !gel) return;
// Nollataan tilat
wrap.classList.remove('state-question', 'state-alert');
gel.classList.remove('state-question', 'state-alert');
wrap.removeAttribute('data-tooltip');
const pLow = prompt.toLowerCase();
const agentTitle = (agentPrompts[agent].name.split(' — ')[0] || "AGENTTI").toUpperCase();
// Hälytys / Virhe
if (pLow.includes('todo') || pLow.includes('viallinen')) {
wrap.classList.add('state-alert');
gel.classList.add('state-alert');
wrap.setAttribute('data-tooltip', `${agentTitle}: "Kriittinen virhe ohjeistuksessa!"\n(Koodissa tai promptissa esiintyy teksti TODO tai viallinen. Korjaa ohje.)`);
}
// Kysyttävää / Hämmennys
else if (prompt.trim().length <= 15 || prompt.includes('?')) {
wrap.classList.add('state-question');
gel.classList.add('state-question');
const questions = {
client: 'Millaisia uusia ominaisuuksia tuotteessa pitäisi olla?',
manager: 'Kuka asiantuntijoista ottaa vastuulleen tämän taskin?',
coder: 'Käytetäänkö tässä komponentissa uusinta React-ohjetta?',
data: 'Millainen tietorakenne käyttäjästä tallennetaan kantaan?',
qa: 'Onko tälle koodille olemassa jo kattavat yksikkötestit?',
tester: 'Mihin haluaisit julkaista tämän laiteympäristön version?',
observer: 'Mitä laatumetriikkoja minun tulisi ensisijaisesti painottaa?'
};
const exampleQ = questions[agent] || 'Mitä minun pitäisi tehdä seuraavaksi?';
const reason = prompt.trim().length <= 15 ? 'Määrittely on tällä hetkellä liian lyhyt.' : 'Ohje on jätetty avoimeksi (? -merkki).';
wrap.setAttribute('data-tooltip', `[?] ${agentTitle}: "${exampleQ}"\n\n(Agentti odottaa päätöstäsi: ${reason})`);
}
// Normaali keskustelu aktiivisena
else if (selectedAgents.has(agent)) {
if (agent === 'client') {
wrap.setAttribute('data-tooltip', `💬 ${agentTitle}: "Mietin parhaillani uusia vaatimuksia!\nPysykää kuulolla, kerron niistä Managerille."`);
} else if (agent === 'manager') {
wrap.setAttribute('data-tooltip', `💬 ${agentTitle}: "Käyn läpi asiakkaan toiveita.\nDelegoin uudet taskit pian Koodarille ja QA:lle!"`);
} else {
const targets = { coder: 'DevOpsin', data: 'Koodarin', qa: 'Data-Agentin', tester: 'QA:n', observer: 'Managerin' };
const targetName = targets[agent] || 'Koodarin';
wrap.setAttribute('data-tooltip', `💬 ${agentTitle}: "Hei, minäkin haluan osallistua!\nVoisin tehdä ${targetName} asiaan tällaisen toiminnallisuuden!"`);
}
}
});
}
// Tarkistetaan heti alussa
setTimeout(checkAgentConfusion, 100);
// -- SIMULAATIO --
window.simulationInterval = null;
window.toggleSimulation = function() {
const btn = document.getElementById('simu-btn');
if (window.simulationInterval) {
clearInterval(window.simulationInterval);
window.simulationInterval = null;
if (btn) btn.textContent = 'Käynnistä simulaatio';
checkAgentConfusion(); // Palautetaan normaalitilat
return;
}
if (btn) btn.textContent = 'Lopeta simulaatio';
const agentsList = Object.keys(agentPrompts);
window.simulationInterval = setInterval(() => {
// Ensin putsataan kaikkien state takaisin normaaliksi
document.querySelectorAll('.gallery-head-wrap').forEach(w => w.classList.remove('state-alert', 'state-question'));
document.querySelectorAll('.gallery-head').forEach(g => g.classList.remove('state-alert', 'state-question'));
checkAgentConfusion();
// Arvotaan reagointi (20% todennäköisyys ettei kukaan hälytä juuri nyt)
if (Math.random() < 0.2) return;
const randAgent = agentsList[Math.floor(Math.random() * agentsList.length)];
const wrap = document.getElementById('wrap-' + randAgent);
const gel = document.getElementById('gallery-' + randAgent);
const agentTitle = (agentPrompts[randAgent].name.split(' — ')[0] || "AGENTTI").toUpperCase();
if (wrap && gel) {
const isAlert = Math.random() > 0.5;
const cClass = isAlert ? 'state-alert' : 'state-question';
// Poistetaan "normaali" active, ettei tooltip jää alle, vaikkei tämä ehkä haittaisi
wrap.classList.remove('state-alert', 'state-question');
gel.classList.remove('state-alert', 'state-question');
wrap.classList.add(cClass);
gel.classList.add(cClass);
const simAlerts = {
client: `${agentTitle}: "🚨 Aikataulu on vaarassa!"\n(Simuloitu huoli: "Miksi luvattu ominaisuus ei ole vielä tuotannossa?")`,
manager: `${agentTitle}: "🚨 Tiimin kapasiteetti ylitetty!"\n(Simuloitu varoitus: "Tarvitaan lisäresursseja Koodari-Nodelle heti.")`,
coder: `${agentTitle}: "🚨 Kääntäjävirhe!"\n(Simuloitu ongelma: "Riippuvuuspuu hajosi viimeisimmän commitin jälkeen. Korjaan...")`,
data: `${agentTitle}: "🚨 Tietokannan lukko!"\n(Simuloitu poikkeama: "Taulussa 'users' on transaction deadlock. Terminoidaan kysely.")`,
qa: `${agentTitle}: "🚨 Regressio havaittu!"\n(Simuloitu löydös: "Testiautomaatio raportoi 3 rikkinäistä polkua. Palautetaan koodarille!")`,
tester: `${agentTitle}: "🚨 Node piiputtaa!"\n(Simuloitu hälytys: "WebAssembly-muistinkulutus 95%. Allokoidaan lisää resursseja.")`,
observer: `${agentTitle}: "🚨 Poikkeama protokollassa!"\n(Simuloitu valvojan ilmoitus: "Solmu palautti epäilyttävän vastauksen. Rajoitan oikeuksia.")`
};
const simQuestions = {
client: `[?] ${agentTitle}: "Saisimmeko tähän vielä yhden muutoksen?"\n(Simuloitu lisätoive: "Voisimmeko muuttaa napin värit hieman kirkkaammiksi?")`,
manager: `[?] ${agentTitle}: "Onko arkkitehtuuri jo valmis?"\n(Simuloitu kysely: "Laittakaa minulle päivitys rajapintojen tilanteesta.")`,
coder: `[?] ${agentTitle}: "Täsmennystä kaivataan..."\n(Simuloitu kysely: "Tehdäänkö tämä komponentti uudestaan vai hyödynnetäänkö vanhaa?")`,
data: `[?] ${agentTitle}: "Outo tietorakenne?"\n(Simuloitu utelu: "Miksi asiakkaan lähettämä JSON on formatoitu näin? Pyydän korjausta.")`,
qa: `[?] ${agentTitle}: "Puuttuvat testiolosuhteet?"\n(Simuloitu ihmettely: "Onko meillä valmista testidataa tälle skenaariolle?")`,
tester: `[?] ${agentTitle}: "Julkaisulupa?"\n(Simuloitu kysymys: "Docker-imaget ovat valmiina. Voidaanko painaa nappia?")`,
observer: `[?] ${agentTitle}: "Laatumetriikat uupuvat..."\n(Simuloitu huomio: "Mittaustulokset viiveestä eivät ole saapuneet vielä lokiin.")`
};
let textRaw = "";
let termColor = "";
if (isAlert) {
const txt = simAlerts[randAgent] || `${agentTitle}: "🚨 Hälytys verkossa!"`;
wrap.setAttribute('data-tooltip', txt);
textRaw = txt.replace(/\n\(/g, ' - ').replace(/\)/g, '');
termColor = '#ff4444';
} else {
const txt = simQuestions[randAgent] || `[?] ${agentTitle}: "Minulla olisi ehdotus..."`;
wrap.setAttribute('data-tooltip', txt);
textRaw = txt.replace(/\n\(/g, ' - ').replace(/\)/g, '');
termColor = '#d29922';
}
// Tulostetaan tapahtuma terminaaliin
if (typeof termLog === 'function') {
termLog(`<span style="color:${termColor}">[SIMULAATIO]</span> ${textRaw}`);
}
// Häly kestää tasan 5 sekuntia, sitten palautuu normaaliksi
setTimeout(() => {
if (window.simulationInterval) {
wrap.classList.remove(cClass);
gel.classList.remove(cClass);
checkAgentConfusion();
}
}, 5000);
}
}, 6000); // Tapahtuu 6 sekunnin välein
};
window.switchMainTab = function(tab) {
document.querySelectorAll('.main-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active'));
document.getElementById('panel-' + tab).classList.add('active');
document.querySelector(`.main-tab[onclick*="${tab}"]`).classList.add('active');
window.location.hash = tab;
// Siivotaan streaming-kortit näkymistä tab-vaihdon yhteydessä
document.querySelectorAll('.streaming-card').forEach(el => el.remove());
// 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,
}));
}
// Codelab: käynnistetään oma laskentasolmu automaattisesti
// Agents: käyttäjä käynnistää itse "Alusta laskentasolmu" -napista
if (tab === 'codelab') {
if (typeof ensureCoderNode === 'function') ensureCoderNode();
}
};
// URL-hash navigointi
const initHash = window.location.hash.replace('#', '');
if (['codelab', 'agents'].includes(initHash)) {
switchMainTab(initHash);
}
// 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'
// Mallivalinnan radio-napit
document.querySelectorAll('input[name="coder-size"]').forEach(radio => {
radio.addEventListener('change', (e) => {
coderSize = e.target.value;
// Visuaalinen korostus
document.getElementById('coder-opt-05b').style.borderColor = coderSize === '05b' ? 'var(--accent-color)' : 'var(--border-color)';
document.getElementById('coder-opt-3b').style.borderColor = coderSize === '3b' ? 'var(--accent-color)' : 'var(--border-color)';
// Jos jo liittynyt, pitää liittyä uudelleen toisella mallilla
if (coderJoined) {
coderJoined = false;
document.getElementById('coder-status').textContent = 'Model changed — rejoin on next generate';
document.getElementById('coder-status').style.color = '#d29922';
}
});
});
const btn = document.getElementById('start-btn');
const logBox = document.getElementById('log-box');
const loadSlider = document.getElementById('gpu-load');
const loadDisplay = document.getElementById('load-display');
const statNodes = document.getElementById('stat-nodes');
const statVram = document.getElementById('stat-vram');
const statTasks = document.getElementById('stat-tasks');
const chatBox = document.getElementById('chat-box');
// Tehtävävalitsin
let selectedTask = 'tokenize';
document.querySelectorAll('.task-option').forEach(opt => {
opt.addEventListener('click', () => {
document.querySelectorAll('.task-option').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
selectedTask = opt.dataset.task;
});
});
let currentChatMsg = null;
// Reaaliaikaiset metriikat
const metrics = {
tasks: 0,
totalTokens: 0,
totalTimeMs: 0,
startTime: null,
};
function updateMetrics() {
document.getElementById('m-tasks').textContent = metrics.tasks;
document.getElementById('m-tokens').textContent = metrics.totalTokens.toLocaleString('fi-FI');
document.getElementById('m-avg-time').textContent = metrics.tasks > 0
? (metrics.totalTimeMs / metrics.tasks).toFixed(1) + 'ms'
: '-';
if (metrics.startTime) {
const sec = Math.floor((Date.now() - metrics.startTime) / 1000);
if (sec < 60) document.getElementById('m-uptime').textContent = sec + 's';
else if (sec < 3600) document.getElementById('m-uptime').textContent = Math.floor(sec/60) + 'min';
else document.getElementById('m-uptime').textContent = Math.floor(sec/3600) + 'h ' + (Math.floor(sec/60)%60) + 'min';
}
}
setInterval(updateMetrics, 1000);
// Laskentaverkko: status Connected (keltainen) ↔ Computing (vihreä)
let computingTimer = null;
function flashComputing() {
const el = document.getElementById('node-status');
if (!el || !window.wasm_active) return;
el.textContent = 'Computing';
el.style.color = 'var(--success-color)';
clearTimeout(computingTimer);
computingTimer = setTimeout(() => {
el.textContent = 'Connected';
el.style.color = '#d29922';
}, 3000);
}
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
const originalLog = console.log;
let logQueue = [];
let logFlushPending = false;
function flushLogs() {
if (!logQueue.length) return;
const frag = document.createDocumentFragment();
for (const msg of logQueue) {
const p = document.createElement('p');
p.textContent = '> ' + msg;
frag.appendChild(p);
}
logBox.appendChild(frag);
while (logBox.children.length > 20) logBox.removeChild(logBox.firstChild);
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!
loadSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
loadDisplay.textContent = val + '%';
if (window.wasm_active) {
set_gpu_load(val);
}
// Tilapäivitys
const statusEl = document.getElementById('node-status');
if (val === 0) {
statusEl.textContent = 'Pysäytetty';
statusEl.style.color = '#f85149';
} else if (val <= 25) {
statusEl.textContent = 'Säästötila';
statusEl.style.color = '#d29922';
} else {
statusEl.textContent = 'Aktiivinen';
statusEl.style.color = 'var(--success-color)';
}
});
// 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');
function sendUserText() {
const text = userInput.value.trim();
if (!text || !uiSocket || uiSocket.readyState !== 1) return;
const msg = JSON.stringify({
type: 'user_text',
text: text,
task_type: selectedTask,
});
uiSocket.send(msg);
userInput.value = '';
console.log(`Lähetetty: "${text}" (${selectedTask})`);
}
sendBtn?.addEventListener('click', sendUserText);
userInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendUserText();
});
// Kytkemme sivuston UI-puolen (JS) omaan passiiviseen WebSocket-kuuntelijaan.
const uiSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`);
window._uiSocket = uiSocket;
uiSocket.onopen = async () => {
// Päivitetään agents-näkymän hub-status
const hubDot = document.getElementById('agent-hub-dot');
const hubLabel = document.getElementById('agent-hub-label');
const hubStatus = document.getElementById('agent-hub-status');
if (hubDot) hubDot.style.background = '#3fb950';
if (hubLabel) { hubLabel.textContent = 'Yhdistetty'; hubLabel.style.color = '#3fb950'; }
if (hubStatus) hubStatus.title = 'Yhdistetty Kipinä Hubiin — tehtävien jakelu ja solmujen koordinointi aktiivinen';
// Päivitetään molemmat statukset
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;
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: 'viewer',
has_webgpu: hasGPU,
gpu: gpuInfo,
}));
};
uiSocket.onclose = () => {
const hubDot = document.getElementById('agent-hub-dot');
const hubLabel = document.getElementById('agent-hub-label');
const hubStatus2 = document.getElementById('agent-hub-status');
if (hubDot) hubDot.style.background = '#f85149';
if (hubLabel) { hubLabel.textContent = 'Yhteys katkennut'; hubLabel.style.color = '#f85149'; }
if (hubStatus2) hubStatus2.title = 'WebSocket-yhteys hubiin katkesi — tarkista verkkoyhteytesi tai hubin tila. Lataa sivu uudelleen yhdistääksesi.';
const el = document.getElementById('node-status');
el.textContent = 'Disconnected';
el.style.color = '#f85149';
const coderEl = document.getElementById('coder-status');
if (coderEl) {
coderEl.textContent = 'Disconnected';
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;
}
// Aktiiviset streaming-rivit task_id:n mukaan
const activeStreams = {};
// Lähettää promptin mallille ja palauttaa vastauksen (tai null virhetilanteessa)
async function kpnRun(model, prompt, silent) {
termLog(` → <span style="color:#58a6ff">${model}</span> käsittelee...`, '#8b949e');
const taskId = crypto.randomUUID();
try {
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');
// Luodaan streaming-rivi terminaaliin
if (!silent) {
const streamDiv = document.createElement('div');
streamDiv.className = 'terminal-line';
streamDiv.style.color = '#c9d1d9';
streamDiv.innerHTML = ' <span class="stream-content"></span><span style="color:#8b949e;animation:blink 1s infinite">▌</span>';
termPanel.appendChild(streamDiv);
termPanel.scrollTop = termPanel.scrollHeight;
activeStreams[taskId] = streamDiv;
}
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) {
const errText = await res.text().catch(() => res.statusText);
termLog(`${errText}`, '#f85149');
return null;
}
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>`);
if (!silent) {
const highlighted = highlightCode(response).replace(/\n/g, '\n ');
termLog(` <pre style="margin:0;font:inherit;white-space:pre-wrap">${highlighted}</pre>`);
}
return response;
} catch (e) {
termLog(`${e.message}`, '#f85149');
return null;
} finally {
if (activeStreams[taskId]) {
activeStreams[taskId].remove();
delete activeStreams[taskId];
}
}
}
// Pipeline: manageri → koodari → testaaja
async function kpnPipeline(task) {
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
// Vaihe 1: Manageri analysoi
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span> — tehtävän analyysi`);
const managerPrompt = `Analysoi seuraava ohjelmistokehitystehtävä ja kirjoita koodarille selkeä tekninen ohje mitä koodata. Vastaa lyhyesti.\n\nTehtävä: ${task}`;
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
// Vaihe 2: Koodari toteuttaa
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span> — toteutus`);
const coderPrompt = `${plan}\n\nKirjoita koodi yllä olevan ohjeen mukaisesti.`;
const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
if (!code) { termLog(' ✗ Pipeline keskeytyi (koodari)', '#f85149'); return; }
// Vaihe 3: Testaaja arvioi
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span> — arviointi`);
const testerPrompt = `Arvioi seuraava koodi lyhyesti. Onko siinä bugeja? Puuttuuko testejä? Anna arvosana 1-5.\n\nTehtävä: ${task}\n\nKoodi:\n${code}`;
await kpnRun(agentPrompts.tester.model, testerPrompt);
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
}
function termExec(cmd) {
termLog(`<span class="terminal-prompt">$</span> ${esc(cmd)}`);
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 &lt;malli&gt; "&lt;prompti&gt;" — aja tehtävä verkossa', '#a5d6ff');
termLog(' kpn pipeline "&lt;tehtävä&gt;" — manageri → koodari → testaaja', '#a5d6ff');
termLog(' kpn load — lataa kielimalli omalle koneelle', '#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 === 'load') {
const btn = document.getElementById('agent-compute-btn');
if (btn && btn.dataset.state === 'ready') {
termLog(' ✓ Kielimalli on jo ladattu ja valmis', '#3fb950');
} else {
termLog(' Alustetaan laskentasolmua...', '#d29922');
if (btn) btn.click(); // Käytetään samaa logiikkaa kuin napissa
else ensureCoderNode();
}
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 === 'pipeline') {
const afterPipeline = cmd.replace(/^kpn\s+pipeline\s*/, '');
const pMatch = afterPipeline.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
if (!pTask) {
termLog(' Käyttö: kpn pipeline "&lt;tehtävä&gt;"', '#f85149');
return;
}
kpnPipeline(pTask);
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') {
let 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 &lt;agentti/malli&gt; "&lt;prompti&gt;"', '#f85149');
return;
}
// Jos käyttäjä syötti agentin nimen (esim. "coder"), vaihdetaan se oikeaksi tekoälymalliksi ("qwen-coder")
if (agentPrompts[model]) {
model = agentPrompts[model].model;
}
kpnRun(model, prompt);
return;
}
termLog(` kpn: tuntematon alikomento "${sub}". Kokeile: kpn help`, '#f85149');
}
// Tab-completion: ennustava komennonsyöttö sana kerrallaan
const kpnCommands = {
'kpn': ['help', 'run', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
'kpn run': ['coder', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
'kpn pipeline': ['"'],
};
// Esimerkkipromptit malleittain
const kpnExamples = {
'kpn run coder': ['"hello world in python"', '"fibonacci in rust"', '"quicksort in javascript"'],
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
'kpn run tester': ['"testaa login-toiminto"'],
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
};
function tabComplete(input) {
const val = input.value;
const words = val.trimEnd().split(/\s+/);
// Etsitään sopiva täydennystaso
// "kpn" → "kpn " alikomennot, "kpn run" → mallit, "kpn run coder" → prompti
for (let depth = words.length; depth >= 1; depth--) {
const prefix = words.slice(0, depth).join(' ');
const partial = words[depth] || '';
// Tarkistetaan esimerkkipromptit ensin
if (kpnExamples[prefix] && !partial) {
const example = kpnExamples[prefix][Math.floor(Math.random() * kpnExamples[prefix].length)];
input.value = prefix + ' ' + example;
return true;
}
// Komentojen täydennys
const candidates = kpnCommands[prefix];
if (candidates) {
const matches = partial
? candidates.filter(c => c.startsWith(partial))
: candidates;
if (matches.length === 1) {
words[depth] = matches[0];
input.value = words.slice(0, depth + 1).join(' ') + ' ';
return true;
} else if (matches.length > 1 && !partial) {
input.value = prefix + ' ' + matches[0];
return true;
} else if (matches.length > 1) {
// Yhteinen etuliite
let common = matches[0];
for (const m of matches) {
while (!m.startsWith(common)) common = common.slice(0, -1);
}
if (common.length > partial.length) {
words[depth] = common;
input.value = words.slice(0, depth + 1).join(' ');
return true;
}
}
}
}
// Tyhjä input → "kpn "
if (!val.trim()) {
input.value = 'kpn ';
return true;
}
return false;
}
termInput?.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
tabComplete(termInput);
} else 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) => {
try {
const raw = event.data;
if (raw.includes('"single_tokenize"') || raw.includes('"download_progress"')) return;
const data = JSON.parse(raw);
if (data.type === "stats") {
statNodes.textContent = data.nodes;
statVram.textContent = data.vram_gb + " GB";
if (data.tasks !== undefined) {
statTasks.textContent = data.tasks;
}
if (data.version) {
document.getElementById('hub-version').textContent = 'v' + data.version;
}
} else if (data.type === "node_joined") {
chatBox.classList.remove('hidden');
} else if (data.type === "download_progress") {
const dlBar = document.getElementById('download-bar');
if (data.pct < 100) {
dlBar.style.display = 'block';
document.getElementById('dl-label').textContent = `Ladataan: ${data.file}`;
document.getElementById('dl-pct').textContent = data.pct + '%';
document.getElementById('dl-fill').style.width = data.pct + '%';
document.getElementById('dl-detail').textContent = `${data.loaded_mb} / ${data.total_mb} MB`;
} 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">${esc(t)}</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">"${esc(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 > 5) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
flashComputing();
} else if (data.type === "pair_task" && selectedTask === 'tokenize') {
chatBox.classList.remove('hidden');
if (chatBox.children.length === 1 && chatBox.children[0].textContent.includes('Odotetaan')) {
chatBox.innerHTML = '';
}
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg';
msgDiv.innerHTML = `<span class="chat-prompt">Tokenisoidaan...</span>
<div style="font-size:12px;color:#8b949e">
<div><strong style="color:#58a6ff">EN</strong> "${esc(data.en)}"</div>
<div><strong style="color:#d29922">FI</strong> "${esc(data.fi)}"</div>
</div>`;
chatBox.appendChild(msgDiv);
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === "pair_done") {
chatBox.classList.remove('hidden');
const en = data.en || {};
const fi = data.fi || {};
const overhead = data.overhead_pct || 0;
const nodeId = data.node_id || "?";
const ms = data.duration_ms || 0;
// Päivitetään metriikat
metrics.tasks++;
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
metrics.totalTimeMs += ms;
updateMetrics();
flashComputing();
// Lokiboksiin yhteenveto
console.log(`EN: ${en.token_count} tokenia (${(en.chars_per_token||0).toFixed(2)} m/t) vs FI: ${fi.token_count} tokenia (${(fi.chars_per_token||0).toFixed(2)} m/t) | ylikustannus: ${overhead}% | ${typeof ms === 'number' ? ms.toFixed(2) : ms}ms`);
const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2));
const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2));
// Värit tehokkuudelle
const cptColor = (v) => v >= 4 ? "#3fb950" : v >= 3 ? "#d29922" : "#f85149";
// Ylikustannuksen väri
const ovColor = overhead > 20 ? "#f85149" : overhead > 0 ? "#d29922" : "#3fb950";
// Korvataan viimeisin "Tokenisoidaan..."-viesti, tai luodaan uusi
const lastMsg = chatBox.lastElementChild;
const msgDiv = (lastMsg && lastMsg.querySelector('.chat-prompt')?.textContent === 'Tokenisoidaan...')
? lastMsg : document.createElement('div');
msgDiv.className = 'chat-msg';
// Tokenilistat renderöitäväksi
const renderTokens = (tokens, cls) => (tokens || []).map(t =>
`<span class="tok ${cls}">${esc(t)}</span>`
).join('');
const enTokHtml = renderTokens(en.tokens, 'tok-en');
const fiTokHtml = renderTokens(fi.tokens, 'tok-fi');
const detailId = 'tok-' + Date.now();
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;display:grid;grid-template-columns:32px 1fr auto auto auto;gap:6px 10px;align-items:baseline">
<strong style="color:#58a6ff">EN</strong>
<span style="color:#79b8ff">"${esc(en.text)}"</span>
<span style="color:#8b949e">${en.char_count} m</span>
<span style="color:var(--accent-color);font-weight:600">${en.token_count} tok</span>
<span style="color:${cptColor(enCpt)};font-weight:600">${enCpt} m/t</span>
<strong style="color:#d29922">FI</strong>
<span style="color:#e3b341">"${esc(fi.text)}"</span>
<span style="color:#8b949e">${fi.char_count} m</span>
<span style="color:var(--accent-color);font-weight:600">${fi.token_count} tok</span>
<span style="color:${cptColor(fiCpt)};font-weight:600">${fiCpt} m/t</span>
</div>
<div id="${detailId}" class="token-detail">
<div style="margin-bottom:6px"><strong style="color:#58a6ff;font-size:12px">EN (${en.token_count})</strong> ${enTokHtml}</div>
<div><strong style="color:#d29922;font-size:12px">FI (${fi.token_count})</strong> ${fiTokHtml}</div>
</div>
<div style="margin-top:10px;display:flex;justify-content:space-between;align-items:baseline;font-size:14px">
<span style="color:#8b949e">(<span style="color:#d29922">${fi.token_count}</span> / <span style="color:#58a6ff">${en.token_count}</span> 1) × 100 = <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
<span style="font-size:15px">FI ylikustannus: <strong style="color:${ovColor}">${overhead > 0 ? '+' : ''}${overhead}%</strong></span>
</div>`;
if (!msgDiv.parentNode) chatBox.appendChild(msgDiv);
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === "llm_done") {
// Reititetäänkö agents-näkymään vai codelab-näkymään?
const isAgentsTask = data.task_id && activeStreams[data.task_id];
const isCoder = (data.model || '').includes('Coder');
if (isAgentsTask) {
// Agents-pipeline: päivitetään terminaali
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.firstChild.querySelector('.stream-content')) 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');
}
} else if (isCoder) {
// Codelab: erillinen addCodeResult-handler käsittelee (rivi 2364)
// Poistetaan vain streaming-kortti codelabista
if (codeResults) codeResults.querySelector('.streaming-card')?.remove();
} else {
// Muu malli (network-näkymä): näytetään chatBoxissa
chatBox.querySelector('.streaming-card')?.remove();
chatBox.classList.remove('hidden');
const nodeId = data.node_id || "?";
const model = data.model || "LLM";
const tokGen = data.tokens_generated || 0;
const durMs = data.duration_ms || 0;
const tokS = data.tokens_per_sec || 0;
const loadMs = data.load_time_ms || 0;
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg';
msgDiv.style.borderLeftColor = '#a371f7';
msgDiv.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="color:#a371f7;font-weight:600;font-size:15px">Solmu #${nodeId}${model}</span>
<span style="color:#8b949e;font-size:12px">${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms | ${tokS} tok/s</span>
</div>
<div style="font-size:13px;color:#8b949e;margin-bottom:6px">
Prompt: <span style="color:#d29922">"${esc(stripSystemPrompt(data.prompt))}"</span>
</div>
<div style="font-size:14px;color:var(--text-color);line-height:1.5;${(model.includes('Coder') || (data.response||'').includes('def ')) ? 'font-family:Courier New,monospace;background:#010409;padding:10px;border-radius:4px;white-space:pre-wrap;font-size:12px' : ''}">
${data.response ? highlightCode(data.response) : '<em>tyhjä vastaus</em>'}
</div>
<div style="margin-top:8px;font-size:12px;color:#8b949e">
${tokGen} tokenia generoitu | malli ladattu: ${typeof loadMs === 'number' ? loadMs.toFixed(0) : loadMs}ms
</div>`;
chatBox.appendChild(msgDiv);
if (chatBox.children.length > 5) chatBox.removeChild(chatBox.firstChild);
chatBox.scrollTop = chatBox.scrollHeight;
}
metrics.tasks++;
metrics.totalTokens += (data.tokens_generated || 0);
metrics.totalTimeMs += (data.duration_ms || 0);
flashComputing();
updateMetrics();
console.log(`[${data.model || 'LLM'}] ${data.tokens_generated || 0} tokenia | ${typeof data.duration_ms === 'number' ? data.duration_ms.toFixed(0) : data.duration_ms || '?'}ms | ${data.tokens_per_sec || '?'} tok/s | "${(data.response || '').substring(0, 60)}..."`);
} else if (data.type === "llm_error") {
// Virheenkäsittely: siivotaan streaming-tila
const errMsg = data.error || 'Tuntematon virhe';
if (data.task_id && activeStreams[data.task_id]) {
// Agents-pipeline: näytetään virhe terminaalissa
activeStreams[data.task_id].remove();
delete activeStreams[data.task_id];
}
chatBox.querySelector('.streaming-card')?.remove();
if (codeResults) codeResults.querySelector('.streaming-card')?.remove();
const term = document.getElementById('agent-terminal');
if (term) {
const div = document.createElement('div');
div.className = 'terminal-line';
div.style.color = '#f85149';
div.innerHTML = ` ✗ LLM-virhe: ${errMsg}`;
term.appendChild(div);
term.scrollTop = term.scrollHeight;
}
console.warn('[LLM Error]', errMsg);
} else if (data.type === "llm_chunk") {
// Agents-terminaalin streaming: päivitetään aktiivinen rivi task_id:n perusteella
if (data.task_id && activeStreams[data.task_id]) {
const streamDiv = activeStreams[data.task_id];
const contentEl = streamDiv.querySelector('.stream-content');
if (contentEl) {
contentEl.textContent += data.token || '';
termPanel.scrollTop = termPanel.scrollHeight;
}
// Agents-pipeline omistaa tämän chunkin, ei näytetä muualla
} else {
// Ei agents-task → näytetään streaming-kortti oikeassa näkymässä
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: "${esc(stripSystemPrompt(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">&#9696;</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;
}
}
} else if (data.type === "task_routed") {
const term = document.getElementById('agent-terminal');
const isQueued = data.status === 'queued';
const color = isQueued ? '#d29922' : '#58a6ff';
const icon = isQueued ? '⏳' : '→';
const msg = esc(data.message || '');
// Agents-terminaali
if (term && data.task_id && activeStreams[data.task_id]) {
const div = document.createElement('div');
div.className = 'terminal-line';
div.style.color = color;
div.innerHTML = ` ${icon} ${msg}`;
if (isQueued) div.id = 'routing-' + data.task_id;
// Päivitetään olemassaoleva jonorivi jos löytyy
const existing = document.getElementById('routing-' + data.task_id);
if (existing) { existing.innerHTML = ` ${icon} ${msg}`; existing.style.color = color; }
else term.appendChild(div);
term.scrollTop = term.scrollHeight;
}
// Codelab-loading-teksti
const codeLoading = document.getElementById('code-loading');
if (codeLoading && codeLoading.style.display !== 'none') {
codeLoading.textContent = isQueued
? `${msg}`
: `${msg} — generoidaan...`;
}
} else if (data.type === "llm_prompt") {
if (data.task_id) {
const term = document.getElementById('agent-terminal');
if (term) {
const model = data.model || 'llm';
const promptShort = esc(stripSystemPrompt(data.prompt)).substring(0, 50);
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.firstChild.querySelector('.stream-content')) term.removeChild(term.firstChild);
term.scrollTop = term.scrollHeight;
}
}
document.querySelectorAll('.avatar-card').forEach(c => c.classList.remove('active'));
const model = data.model || '';
const p = data.prompt ? data.prompt.toLowerCase() : '';
if (p.includes('tiiminvetäjä') || p.includes('pilko')) {
document.getElementById('avatar-kpn')?.classList.add('active');
} else if (p.includes('arvioi seuraava koodi') || p.includes('ohjelmiston julkaisu')) {
document.getElementById('avatar-tester')?.classList.add('active');
} else if (p.includes('tervehdi')) {
document.getElementById('avatar-client')?.classList.add('active');
} else if (p.includes('test')) {
document.getElementById('avatar-qa')?.classList.add('active');
} else if (model.includes('coder') || model.includes('Coder')) {
// Koodari aktivoituu, jos kyse on suoraan koodarille osoitetusta mallitehtävästä (esim. network task)
document.getElementById('avatar-coder')?.classList.add('active');
} else if (model.includes('deepseek') || model.includes('r1')) {
document.getElementById('avatar-observer')?.classList.add('active');
}
// Emme enää aseta oletusagenttia, jottei tuntemattomissa verkkopyynnöissä mikään turhaan hypi silmille.
}
} catch(e) {}
};
btn.addEventListener('click', async () => {
// 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: detectedGpuInfo,
selected_task: selectedTask
};
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')
? (hasWebGPU ? "WebGPU + CPU" : "CPU")
: "CPU (Candle Wasm)";
const vramStr = deviceInfo.gpu?.estimated_vram_gb ? `~${deviceInfo.gpu.estimated_vram_gb} GB` : "?";
const ramNote = deviceInfo.device_memory_gb >= 8 ? "8+ GB (selaimen raja)" : `~${deviceInfo.device_memory_gb} GB`;
// Näytetään laitetiedot paneelissa
const diPanel = document.getElementById('device-info');
diPanel.style.display = 'block';
diPanel.innerHTML = [
`Laskenta: <span>${computeBackend}</span>`,
hasWebGPU ? `GPU: <span>${gpuStr}</span>` : `GPU: <span style="color:#f85149">ei WebGPU:ta</span>`,
hasWebGPU ? `VRAM: <span>${vramStr}</span>` : null,
`CPU: <span>${deviceInfo.cpu_cores} ydintä</span>`,
`RAM: <span>${ramNote}</span>`,
`Varaus: <span>${deviceInfo.allocated_gb} GB</span>`
].filter(Boolean).join(' &middot; ');
// Yhteensopivuusbanneri
const banner = document.getElementById('compat-banner');
banner.style.display = 'block';
if (hasWebGPU) {
banner.className = 'compat-banner gpu';
banner.innerHTML = `WebGPU tunnistettu — ${gpuStr}. Tokenisaatio käyttää GPU:ta, LLM-inferenssi CPU:ta (Candle Wasm).`;
} else {
// Tunnistetaan selain ohjeen personointia varten
const ua = navigator.userAgent;
const isFirefox = ua.includes('Firefox');
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave);
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
const isLinux = ua.includes('Linux');
let browserTip = '';
if (isFirefox) {
browserTip = `
<p><strong>Firefox</strong> ei tue WebGPU:ta oletuksena.</p>
<p>Ota käyttöön: <code>about:config</code> → <code>dom.webgpu.enabled</code> = <code>true</code> → käynnistä uudelleen.</p>
<p>Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.</p>`;
} else if ((isChrome || isBrave) && isLinux) {
const browser = isBrave ? 'brave-browser' : 'google-chrome';
browserTip = `
<p><strong>${isBrave ? 'Brave' : 'Chrome'} + Linux</strong>: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.</p>
<p>Kokeile käynnistää selain komentoriviltä:</p>
<code>${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11</code>`;
} else if (isSafari) {
browserTip = `
<p><strong>Safari</strong>: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).</p>
<p>Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.</p>`;
} else {
browserTip = `
<p>Selaimesi ei tue WebGPU:ta. Kokeile <strong>Chrome 113+</strong> tai <strong>Brave</strong>.</p>`;
}
banner.className = 'compat-banner cpu';
banner.innerHTML = `
<details>
<summary>CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita</summary>
${browserTip}
<p style="margin-top:8px;color:#8b949e;font-size:12px">Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.</p>
</details>`;
}
document.getElementById('initial-state').classList.add('hidden');
document.getElementById('active-state').classList.remove('hidden');
document.getElementById('user-input-box').classList.remove('hidden');
btn.style.display = 'none';
// Nappin teksti ja placeholder tehtävän mukaan
const sendBtnEl = document.getElementById('send-btn');
const placeholderEl = document.getElementById('user-text');
const t = window.currentLangDict || translations.fi;
if (selectedTask === 'tokenize') {
sendBtnEl.textContent = t.btn_tokenize || 'Tokenisoi';
} else if (selectedTask === 'qwen-coder') {
sendBtnEl.textContent = 'Koodaa';
} else {
sendBtnEl.textContent = 'Generoi';
}
try {
if (!wasmInitialized) {
console.log("Ladataan Burn Wasm -binääriä...");
await init();
wasmInitialized = true;
}
window.wasm_active = true;
metrics.startTime = Date.now();
// Asetetaan Connected-tila (keltainen) — vihreäksi vasta kun laskentaa tapahtuu
const nodeStatusEl = document.getElementById('node-status');
nodeStatusEl.textContent = 'Connected';
nodeStatusEl.style.color = '#d29922';
// Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen
set_gpu_load(parseInt(loadSlider.value));
// WebAssembly yhdistää oikeaksi Agent Nodeksi
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
const taskIds = {'tokenize': 0, 'smollm-135m': 1, 'qwen-05b': 2, 'phi3-mini': 3, 'qwen-coder-05b': 4, 'qwen-coder-3b': 5};
const taskId = taskIds[selectedTask] || 0;
await start_agent_node(wsUrl, hasWebGPU, JSON.stringify(deviceInfo), taskId);
} catch(e) {
console.log("Virhe GPU-käynnistyksessä: " + e);
}
});
// === Koodilaboratorio ===
const codeInput = document.getElementById('code-input');
const codeSendBtn = document.getElementById('code-send-btn');
const codeResults = document.getElementById('code-results');
const codeLoading = document.getElementById('code-loading');
let coderWsReady = false;
let coderWs = null; // Erillinen WS coder-nodelle
let pendingCodePrompt = null;
// Yksinkertainen Python-syntaksikorostus
function highlightCode(code) {
if (typeof hljs !== 'undefined') {
try {
const result = hljs.highlightAuto(code);
return result.value;
} catch(e) {}
}
return esc(code);
}
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;
const tokS = data.tokens_per_sec || 0;
const response = esc(data.response);
codeMetrics.tasks++;
codeMetrics.tokens += tokGen;
codeMetrics.lastSpeed = tokS;
document.getElementById('code-m-tasks').textContent = codeMetrics.tasks;
document.getElementById('code-m-tokens').textContent = codeMetrics.tokens.toLocaleString('fi-FI');
document.getElementById('code-m-speed').textContent = tokS + ' tok/s';
if (codeResults.querySelector('[data-placeholder]')) {
codeResults.innerHTML = '';
}
codeLoading.style.display = 'none';
codeSendBtn.disabled = false;
codeSendBtn.textContent = 'Generate';
document.getElementById('coder-status').textContent = 'Connected';
document.getElementById('coder-status').style.color = '#d29922';
const card = document.createElement('div');
card.className = 'code-task-card';
card.innerHTML = `
<div class="prompt">${esc(stripSystemPrompt(data.prompt))}</div>
<div class="code-output">${highlightCode(response)}</div>
<div class="meta">
${model} · ${tokGen} tokenia · ${typeof durMs === 'number' ? durMs.toFixed(0) : durMs}ms · ${tokS} tok/s
</div>`;
codeResults.insertBefore(card, codeResults.firstChild);
if (codeResults.children.length > 10) codeResults.removeChild(codeResults.lastChild);
}
// Kuuntele coder-tuloksia UI WebSocketista (vain codelab-tehtävät)
uiSocket.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'llm_done' && (data.model || '').includes('Coder')) {
// Agents-pipeline asettaa aina task_id:n, codelabin user_text-polku ei koskaan
if (data.task_id) return;
addCodeResult(data);
}
} catch(e) {}
});
// Pipeline-vaiheiden päivitys
function setStep(id, state, extra) {
const el = document.getElementById(id);
if (!el) return;
el.className = 'code-step ' + state;
const icon = el.querySelector('.step-icon');
if (state === 'active') icon.textContent = '\u25F7'; // spinning
else if (state === 'done') icon.textContent = '\u2713';
else if (state === 'error') icon.textContent = '\u2717';
if (extra) {
const pct = document.getElementById(id + '-pct');
if (pct) pct.textContent = extra;
}
}
// Kuuntele console.log-viestejä pipeline-vaiheiden seuraamiseksi
const origCodeLog = console.log;
const codeLogListener = (...args) => {
const msg = args.join(' ');
if (msg.includes('[Coder]') || msg.includes('[Storage]') || msg.includes('Burn Wasm') || msg.includes('Kipinä Agent Node')) {
if (msg.includes('Burn Wasm')) setStep('step-wasm', 'active');
if (msg.includes('Agent Node käynnistyy')) { setStep('step-wasm', 'done'); }
// Tokenizer: [Coder] tai [Storage] -prefiksi
if (msg.includes('Tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('tokenizer') && msg.includes('löytyi')) { setStep('step-tokenizer', 'done'); }
if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('Ladataan') && msg.includes('tokenizer')) { setStep('step-tokenizer', 'active'); }
if ((msg.includes('[Coder]') || msg.includes('[Storage]')) && msg.includes('tokenizer') && msg.includes('tallennettu')) { setStep('step-tokenizer', 'done'); }
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('lataus:')) {
setStep('step-model', 'active');
const match = msg.match(/lataus: (\d+)%/);
if (match) setStep('step-model', 'active', match[1] + '%');
}
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('löytyi')) { setStep('step-model', 'done', 'cache'); }
if (msg.includes('[Coder]') && msg.includes('model') && msg.includes('tallennettu')) { setStep('step-model', 'done', '100%'); }
if (msg.includes('[Coder]') && msg.includes('Rakennetaan')) { setStep('step-build', 'active'); }
if (msg.includes('Agent Node käynnistyy') || msg.includes('Rakennetaan')) {
const cd = document.getElementById('agent-compute-dot');
const cl = document.getElementById('agent-compute-label');
const btn = document.getElementById('agent-compute-btn');
if (cd) cd.style.background = '#d29922';
if (cl) { cl.textContent = 'Ladataan...'; cl.style.color = '#d29922'; }
if (btn && btn.dataset.state !== 'ready') {
btn.dataset.state = 'loading';
btn.textContent = 'Peruuta';
btn.style.borderColor = '#f85149';
btn.style.color = '#f85149';
}
}
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
// Malli on valmis — merkataan kaikki vaiheet valmiiksi
setStep('step-wasm', 'done');
setStep('step-tokenizer', 'done');
const pctSpan = document.getElementById('step-model-pct');
if (pctSpan && pctSpan.textContent.includes('100%')) {
setStep('step-model', 'done', '100%');
} else {
setStep('step-model', 'done', 'cache');
}
setStep('step-build', 'done');
setStep('step-ready', 'done');
// Agents-sivun compute-status: valmis
const cd = document.getElementById('agent-compute-dot');
const cl = document.getElementById('agent-compute-label');
const btn = document.getElementById('agent-compute-btn');
if (cd) cd.style.background = '#3fb950';
if (cl) { cl.textContent = 'Qwen2.5-Coder'; cl.style.color = '#3fb950'; }
if (btn) { btn.dataset.state = 'ready'; btn.textContent = '✓ Valmis'; btn.style.borderColor = '#3fb950'; btn.style.color = '#3fb950'; btn.style.cursor = 'default'; btn.title = 'Kielimalli ladattu — oma kone on valmis laskentaan'; }
}
if (msg.includes('[Coder]') && msg.includes('Syöte:')) {
// Pipeline piiloon kun generointi alkaa
setTimeout(() => { document.getElementById('code-pipeline').style.display = 'none'; }, 1000);
}
}
};
// Lisätään kuuntelija alkuperäisen console.log ylikirjoituksen päälle
const _prevConsoleLog = console.log;
console.log = function(...args) { _prevConsoleLog.apply(console, args); codeLogListener(...args); };
// Käynnistä Coder-node automaattisesti ensimmäisellä kerralla
async function ensureCoderNode() {
if (coderJoined) return;
coderJoined = true;
document.getElementById('coder-status').textContent = 'Käynnistyy...';
document.getElementById('coder-status').style.color = '#d29922';
document.getElementById('code-pipeline').style.display = 'block';
setStep('step-wasm', 'active');
try {
if (!wasmInitialized) {
await init();
wasmInitialized = true;
}
setStep('step-wasm', 'done');
setStep('step-tokenizer', 'active');
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
const deviceInfo = {
allocated_gb: 4,
cpu_cores: navigator.hardwareConcurrency || 0,
device_memory_gb: navigator.deviceMemory || 0,
platform: navigator.platform || "",
gpu: null,
selected_task: coderSize === '3b' ? 'qwen-coder-3b' : 'qwen-coder-05b'
};
const taskId = coderSize === '3b' ? 5 : 4;
// Tunnistetaan WebGPU myös koodilaboratorion puolella
let coderHasWebGPU = false;
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
try {
const testDevice = await adapter.requestDevice({ requiredLimits: { maxInterStageShaderComponents: 60 } });
coderHasWebGPU = true;
testDevice.destroy();
} catch(e) {
coderHasWebGPU = false;
}
}
} catch(e) {}
}
await start_agent_node(wsUrl, coderHasWebGPU, JSON.stringify(deviceInfo), taskId);
document.getElementById('coder-status').textContent = 'Connected';
document.getElementById('coder-status').style.color = '#d29922';
coderWsReady = true;
// Proaktiivinen mallin esilataus: lähetetään tyhjä warmup-prompt
// joka triggeröi get_or_build_model:n ilman varsinaista generointia.
// Pipeline-tilakone seuraa logeja ja merkkaa vaiheet valmiiksi.
setTimeout(() => {
if (uiSocket && uiSocket.readyState === 1) {
uiSocket.send(JSON.stringify({
type: 'user_text',
text: '{"prompt":"warmup","max_tokens":1}',
task_type: 'qwen-coder'
}));
}
}, 500);
if (pendingCodePrompt) {
setTimeout(() => {
sendCodeToHub(pendingCodePrompt);
}, 2000); // Hieman pidempi odotus jotta warmup ehtii ensin
pendingCodePrompt = null;
}
} catch(e) {
console.log("Coder-virhe: " + e);
document.getElementById('coder-status').textContent = 'Virhe';
document.getElementById('coder-status').style.color = '#f85149';
coderJoined = false;
}
}
// Agents-sivun coder-node käynnistetään "Alusta laskentasolmu" -napista tai kpn load -komennolla
// Laskentasolmun käynnistys/pysäytys -nappi
let computeAbortController = null;
document.getElementById('agent-compute-btn')?.addEventListener('click', () => {
const btn = document.getElementById('agent-compute-btn');
const cl = document.getElementById('agent-compute-label');
if (!btn) return;
if (btn.dataset.state === 'ready') return; // Jo valmis, ei tehdä mitään
if (btn.dataset.state === 'loading') {
// Cancel — ladataan sivua uudelleen koska Wasm-latausta ei voi pysäyttää
btn.textContent = 'Peruutetaan...';
btn.disabled = true;
window.location.reload();
return;
}
// Käynnistetään
btn.dataset.state = 'loading';
btn.textContent = 'Peruuta';
btn.style.borderColor = '#f85149';
btn.style.color = '#f85149';
btn.title = 'Peruuta kielimallin lataus';
ensureCoderNode();
});
// JSON mode toggle
const jsonToggle = document.getElementById('json-mode-toggle');
const jsonHelp = document.getElementById('json-help');
const textInput = document.getElementById('code-input');
const jsonInput = document.getElementById('code-input-json');
jsonToggle?.addEventListener('change', () => {
if (jsonToggle.checked) {
textInput.style.display = 'none';
jsonInput.style.display = 'block';
jsonHelp.style.display = 'block';
} else {
textInput.style.display = 'block';
jsonInput.style.display = 'none';
jsonHelp.style.display = 'none';
}
});
function sendCodeToHub(text) {
if (uiSocket && uiSocket.readyState === 1) {
uiSocket.send(JSON.stringify({ type: 'user_text', text: text, task_type: 'qwen-coder' }));
}
}
async function handleCodeSubmit() {
let promptText;
if (jsonToggle.checked) {
// JSON mode
const raw = jsonInput.value.trim();
if (!raw) return;
try {
const parsed = JSON.parse(raw);
if (!parsed.prompt) { alert('JSON must contain "prompt" field'); return; }
// Lähetetään koko JSON hubille — node lukee promptin ja parametrit
promptText = raw;
} catch(e) {
alert('Invalid JSON: ' + e.message);
return;
}
} else {
// Text mode
promptText = textInput.value.trim();
if (!promptText) return;
textInput.value = '';
}
codeSendBtn.disabled = true;
codeSendBtn.textContent = 'Generating...';
codeLoading.style.display = 'block';
if (!coderJoined) {
pendingCodePrompt = promptText;
const dlSize = coderSize === '3b' ? '~6.2 GB' : '~990 MB';
codeLoading.textContent = `Loading Qwen2.5-Coder-${coderSize === '3b' ? '3B' : '0.5B'} (${dlSize} on first run)...`;
await ensureCoderNode();
} else {
codeLoading.textContent = 'Generating code...';
document.getElementById('coder-status').textContent = 'Computing';
document.getElementById('coder-status').style.color = 'var(--success-color)';
sendCodeToHub(promptText);
}
}
codeSendBtn?.addEventListener('click', handleCodeSubmit);
textInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleCodeSubmit(); });
const translations = {
fi: {
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
main_subtitle: "Hajautettu WebGPU Laskentaverkko",
tab_network: "Laskentaverkko",
tab_codelab: "Koodilaboratorio",
tab_agents: "Kipinä Agentic Playground",
stat_nodes_lbl: "Aktiivisia Nodeja",
stat_tasks_lbl: "Verkossa Suoritettua Tehtävää (Globaali)",
stat_vram_lbl: "Verkon yhteis-VRAM",
btn_select_all: "Valitse kaikki",
btn_clear_all: "Tyhjennä valinnat",
task_title: "Valitse tehtävä",
btn_join: "Liity laskentaverkkoon",
btn_disconnect: "Katkaise Yhteys",
resource_mgmt: "Resurssien hallinta",
power_limiter: "Laskentatehon rajoitin",
auto_tasks: "Vastaanota automaattisia tehtäviä hubilta",
try_own_text: "Kokeile omaa tekstiä:",
btn_tokenize: "Tokenisoi",
btn_code: "Koodaa",
btn_generate: "Generoi",
metric_tasks: "Tehtäviä",
metric_avg: "Ka. aika",
metric_tokens: "Tokeneita",
metric_uptime: "Käynnissä"
},
se: {
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
main_subtitle: "Decentraliserat WebGPU Beräkningsnätverk",
tab_network: "Kalkylnätverk",
tab_codelab: "Kodlaboratorium",
tab_agents: "Kipinä Agentic Playground",
stat_nodes_lbl: "Aktiva Noder",
stat_tasks_lbl: "Slutförda Uppgifter (Globalt)",
stat_vram_lbl: "Nätverkets totala VRAM",
btn_select_all: "Välj alla",
btn_clear_all: "Rensa val",
task_title: "Välj uppgift",
btn_join: "Gå med i nätverket",
btn_disconnect: "Koppla från",
resource_mgmt: "Resurshantering",
power_limiter: "Beräkningskraftsbegränsare",
auto_tasks: "Ta emot automatiska uppgifter från hubben",
try_own_text: "Prova med egen text:",
btn_tokenize: "Tokenisera",
btn_code: "Koda",
btn_generate: "Generera",
metric_tasks: "Uppgifter",
metric_avg: "Snittid",
metric_tokens: "Tokens",
metric_uptime: "Drifttid"
},
en: {
main_title: "<span style=\"color:#ff6b00\">Kipinä</span> <span>Agentic Playground</span>",
main_subtitle: "Decentralized WebGPU Compute Network",
tab_network: "Compute Network",
tab_codelab: "Code Laboratory",
tab_agents: "Kipinä Agentic Playground",
stat_nodes_lbl: "Active Nodes",
stat_tasks_lbl: "Tasks Completed (Global)",
stat_vram_lbl: "Total Network VRAM",
btn_select_all: "Select all",
btn_clear_all: "Clear selection",
task_title: "Choose task",
btn_join: "Join Compute Network",
btn_disconnect: "Disconnect",
resource_mgmt: "Resource Management",
power_limiter: "Compute Power Limiter",
auto_tasks: "Receive automatic tasks from hub",
try_own_text: "Test your own text:",
btn_tokenize: "Tokenize",
btn_code: "Code",
btn_generate: "Generate",
metric_tasks: "Tasks",
metric_avg: "Avg. Time",
metric_tokens: "Tokens",
metric_uptime: "Uptime"
}
};
window.setLanguage = function(lang) {
localStorage.setItem('kpn_lang', lang);
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
const btn = document.querySelector(`.lang-btn[data-lang="${lang}"]`);
if (btn) btn.classList.add('active');
const t = translations[lang] || translations.fi;
window.currentLangDict = t;
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (t[key]) {
if(t[key].includes('<')) el.innerHTML = t[key];
else el.textContent = t[key];
}
});
if(window.updatePromptEditor) window.updatePromptEditor();
// Käännä lennossa ne painikkeet jotka ovat ehkä vaihtaneet tekstiä dynaamisesti (esim. JS-tilan muutokset)
const sendBtnEl = document.getElementById('send-btn');
if (sendBtnEl && window.wasm_active) {
// Riippuu valitusta tehtävästä
const sTask = window.selectedTask || document.querySelector('.task-option.selected')?.dataset?.task;
if (sTask === 'tokenize') sendBtnEl.textContent = t.btn_tokenize || 'Tokenisoi';
else if (sTask === 'qwen-coder') sendBtnEl.textContent = t.btn_code || 'Koodaa';
else sendBtnEl.textContent = t.btn_generate || 'Generoi';
}
const jbtn = document.getElementById('start-btn');
if (jbtn) {
// start-btn vaihtuu connect / disconnect kun ollaan aktiivitilassa
if (window.wasm_active || jbtn.textContent === 'Katkaise Yhteys' || jbtn.textContent === 'Koppla från' || jbtn.textContent === 'Disconnect') {
jbtn.textContent = t.btn_disconnect || 'Katkaise Yhteys';
} else {
jbtn.textContent = t.btn_join || 'Liity laskentaverkkoon';
}
}
const cbtn = document.getElementById('code-send-btn');
if (cbtn && !cbtn.textContent.includes('...')) {
cbtn.textContent = t.btn_generate || 'Generate';
}
};
document.addEventListener('DOMContentLoaded', () => {
const savedLang = localStorage.getItem('kpn_lang') || 'fi';
setLanguage(savedLang);
// Valitaan Asiakas-agentti automaattisesti sivun ladattua (muttei jatkossa)
setTimeout(() => {
if (window.selectAgent) window.selectAgent('client');
}, 100);
});
</script>
</body>
</html>