6 Commits

Author SHA1 Message Date
Jaakko Vanhala
3d1b406e8d Alasvetovalikko kpn-terminaalin autocompletioniin
TAB avaa dropdown-valikon käytettävissä olevista vaihtoehdoista:
- Nuolilla (ylös/alas) navigointi
- Enter tai TAB valitsee korostetun vaihtoehdon
- Esc sulkee valikon
- Klikkaus valitsee suoraan
- Yksi vaihtoehto → täydennetään suoraan ilman valikkoa

Valikko näyttää kontekstin mukaan: alikomennot, mallit/agentit
tai esimerkkiprompteja. Sulkeutuu automaattisesti kun klikataan muualle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:53:47 +03:00
Jaakko Vanhala
aa6c4739dd Shift-TAB poistaa viimeisen sanan kpn-terminaalissa
Poistaa viimeisen sanan tai lainausmerkeissä olevan kokonaisuuden:
- "kpn run coder " → Shift-TAB → "kpn run "
- 'kpn run coder "hello world"' → Shift-TAB → "kpn run coder "

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:51:39 +03:00
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
Jaakko Vanhala
0a216f19e2 Laskentasolmun käynnistys käyttäjän hallinnassa: nappi + cancel + kpn load
Status-palkissa "Alusta laskentasolmu" -nappi joka:
- Klikkaa → käynnistää kielimallin latauksen omalle koneelle
- Latauksen aikana muuttuu "Peruuta"-napiksi (punainen)
- Valmis → vihreä "✓ Valmis" -tila
Myös kpn load -komento terminaalissa tekee saman.
Agents-sivulla ei enää automaattista käynnistystä — käyttäjä valitsee itse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:50:07 +03:00
Jaakko Vanhala
a2e7ed53ff Laskentavalmiuden indikaattori agents-sivulle + kpn load -komento
Status-palkissa näkyy nyt Hub-yhteyden lisäksi laskentasolmun tila:
- Harmaa "Ei käynnissä ⟩" — klikkaa käynnistääksesi
- Keltainen "Ladataan mallia..." — malli latautuu
- Vihreä "Valmis (Qwen2.5-Coder)" — valmis laskentaan

Kaksi tapaa käynnistää:
1. Klikkaa compute-statusta status-palkissa
2. Kirjoita terminaaliin: kpn load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:48:50 +03:00
Jaakko Vanhala
950cae9d96 Agents-sivu käynnistää oman laskentasolmun: käyttäjän kone valjastetaan laskentaan
Kun käyttäjä avaa #agents-sivun, käynnistetään automaattisesti Wasm coder-node
jotta tehtävät reitittyvät omalle koneelle eikä ulkoisille solmuille.
Sama logiikka kuin codelabissa (ensureCoderNode + warmup).
Toimii sekä suoralla #agents-navigoinnilla että tab-vaihdolla.

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

View File

@@ -1089,17 +1089,27 @@
</div>
</div>
<div id="agent-hub-status" title="WebSocket-yhteys Kipinä Hubiin — hallitsee tehtävien jakelun ja solmujen koordinoinnin" style="margin-top:20px;padding:8px 14px;background:#0d1117;border:1px solid var(--border-color);border-radius:6px 6px 0 0;font-family:'Courier New',monospace;font-size:13px;display:flex;align-items:center;gap:8px;cursor:help">
<div 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">
<div style="position:relative;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"
<input id="term-input" type="text" placeholder="kpn run coder &quot;kirjoita hello world&quot;" spellcheck="false" autocomplete="off"
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
<div id="term-dropdown" style="display:none;position:absolute;bottom:100%;left:30px;background:#161b22;border:1px solid #30363d;border-radius:6px;max-height:200px;overflow-y:auto;font-size:13px;min-width:200px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,0.4)"></div>
</div>
</div>
</div><!-- /panel-agents -->
@@ -1448,6 +1458,12 @@
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
@@ -1820,6 +1836,7 @@
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');
@@ -1831,6 +1848,18 @@
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 || '?';
@@ -1887,18 +1916,201 @@
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;
}
// Dropdown-autocompletionin tila
const dropdown = document.getElementById('term-dropdown');
let dropdownItems = [];
let dropdownIdx = -1;
let dropdownPrefix = ''; // Inputin alku joka säilyy valinnan yhteydessä
function getCandidates(val) {
const words = val.trimEnd().split(/\s+/);
for (let depth = words.length; depth >= 1; depth--) {
const prefix = words.slice(0, depth).join(' ');
const partial = words[depth] || '';
// Esimerkkipromptit
if (kpnExamples[prefix] && !partial) {
return { items: kpnExamples[prefix], prefix: prefix + ' ' };
}
// Komennot
const candidates = kpnCommands[prefix];
if (candidates) {
const matches = partial ? candidates.filter(c => c.startsWith(partial)) : candidates;
if (matches.length > 0) {
return { items: matches, prefix: prefix + ' ' };
}
}
}
if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' };
return { items: [], prefix: val };
}
function showDropdown(items, prefix) {
if (!dropdown || items.length === 0) { hideDropdown(); return; }
dropdownItems = items;
dropdownPrefix = prefix;
dropdownIdx = -1;
dropdown.innerHTML = items.map((item, i) =>
`<div class="term-dd-item" data-idx="${i}" style="padding:6px 12px;cursor:pointer;color:#c9d1d9;white-space:nowrap;border-bottom:1px solid #21262d">${esc(item)}</div>`
).join('');
dropdown.style.display = 'block';
// Klikkaus-handlerit
dropdown.querySelectorAll('.term-dd-item').forEach(el => {
el.addEventListener('mouseenter', () => highlightDropdown(parseInt(el.dataset.idx)));
el.addEventListener('click', () => { selectDropdown(); termInput.focus(); });
});
}
function hideDropdown() {
if (dropdown) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; }
dropdownItems = [];
dropdownIdx = -1;
}
function highlightDropdown(idx) {
dropdownIdx = idx;
dropdown.querySelectorAll('.term-dd-item').forEach((el, i) => {
el.style.background = i === idx ? '#30363d' : 'transparent';
el.style.color = i === idx ? '#58a6ff' : '#c9d1d9';
});
// Varmistetaan näkyvyys
const active = dropdown.children[idx];
if (active) active.scrollIntoView({ block: 'nearest' });
}
function selectDropdown() {
if (dropdownIdx >= 0 && dropdownIdx < dropdownItems.length) {
termInput.value = dropdownPrefix + dropdownItems[dropdownIdx] + (dropdownItems[dropdownIdx].startsWith('"') ? '' : ' ');
}
hideDropdown();
}
termInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// Dropdown auki: nuolet navigoi, Enter/Tab valitsee, Esc sulkee
if (dropdown && dropdown.style.display === 'block') {
if (e.key === 'ArrowDown') {
e.preventDefault();
highlightDropdown(Math.min(dropdownIdx + 1, dropdownItems.length - 1));
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
highlightDropdown(Math.max(dropdownIdx - 1, 0));
return;
}
if ((e.key === 'Enter' || e.key === 'Tab') && dropdownIdx >= 0) {
e.preventDefault();
selectDropdown();
return;
}
if (e.key === 'Escape') {
e.preventDefault();
hideDropdown();
return;
}
}
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
hideDropdown();
const val = termInput.value.trimEnd();
if (!val) return;
const quoteMatch = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/);
if (quoteMatch) {
termInput.value = (quoteMatch[1] || quoteMatch[2]).trimEnd() + ' ';
} else {
const lastSpace = val.lastIndexOf(' ');
termInput.value = lastSpace > 0 ? val.substring(0, lastSpace + 1) : '';
}
} else if (e.key === 'Tab') {
e.preventDefault();
// Näytä dropdown tai täydennä jos vain yksi vaihtoehto
const { items, prefix } = getCandidates(termInput.value);
if (items.length === 1) {
termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' ');
hideDropdown();
} else if (items.length > 1) {
showDropdown(items, prefix);
}
} else if (e.key === 'Enter') {
hideDropdown();
const cmd = termInput.value.trim();
if (cmd) termExec(cmd);
termInput.value = '';
} else if (e.key === 'ArrowUp') {
} else if (e.key === 'ArrowUp' && !dropdown?.style.display?.includes('block')) {
e.preventDefault();
if (termHistIdx < termHistory.length - 1) {
termHistIdx++;
termInput.value = termHistory[termHistIdx];
}
} else if (e.key === 'ArrowDown') {
} else if (e.key === 'ArrowDown' && !dropdown?.style.display?.includes('block')) {
e.preventDefault();
if (termHistIdx > 0) {
termHistIdx--;
@@ -1910,6 +2122,11 @@
}
});
// Suljetaan dropdown kun klikataan muualle
document.addEventListener('click', (e) => {
if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDropdown();
});
// Klikkaa terminaalipaneelia → fokusoi input
termPanel?.addEventListener('click', () => termInput?.focus());
@@ -2494,6 +2711,19 @@
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');
@@ -2508,6 +2738,14 @@
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
@@ -2594,6 +2832,34 @@
}
}
// 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');