Compare commits
6 Commits
ff3a720b8d
...
3d1b406e8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1b406e8d | ||
|
|
aa6c4739dd | ||
|
|
cbbf427a93 | ||
|
|
0a216f19e2 | ||
|
|
a2e7ed53ff | ||
|
|
950cae9d96 |
@@ -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">
|
||||
<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>
|
||||
<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 "kirjoita hello world"" spellcheck="false"
|
||||
<input id="term-input" type="text" placeholder="kpn run coder "kirjoita hello world"" 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 <malli> "<prompti>" — aja tehtävä verkossa', '#a5d6ff');
|
||||
termLog(' kpn pipeline "<tehtävä>" — 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,20 +2711,41 @@
|
||||
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
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user