Files
foodlinkk-command-center/cockpit/static/js/agents-terminals.js
T

245 lines
7.1 KiB
JavaScript

(function () {
const buffers = {};
let liveEs = null;
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
function nowIso() {
return new Date().toISOString();
}
function lineClass(type) {
if (!type) return 'line-action';
if (type === 'command') return 'line-command';
if (type === 'output') return 'line-output';
if (String(type).indexOf('handoff') >= 0) return 'line-handoff';
if (type === 'error') return 'line-error';
if (String(type).indexOf('monitor') >= 0) return 'line-handoff';
if (String(type).indexOf('browse') >= 0) return 'line-action';
return 'line-action';
}
function formatLine(entry) {
const t = (entry.at || '').substring(11, 19) || (entry.at || '').substring(11, 16) || '--:--';
const msg = entry.message || entry.title || '';
const detail = entry.detail ? ' — ' + String(entry.detail).slice(0, 200) : '';
return (
'<div class="term-line ' +
lineClass(entry.type) +
'"><span class="term-ts">' +
esc(t) +
'</span><span class="term-msg">' +
esc(msg + detail) +
'</span></div>'
);
}
function render(key) {
const els = document.querySelectorAll('[data-term-key="' + key + '"]');
if (!els.length) return;
const lines = buffers[key] || [];
const html =
!lines.length
? '<div class="term-line term-idle"><span class="term-msg">Typ <code>help</code> voor commando\'s · Enter om uit te voeren</span></div>'
: lines.slice(-80).map(formatLine).join('');
els.forEach(function (el) {
el.innerHTML = html;
el.scrollTop = el.scrollHeight;
const win = el.closest('.agent-term-window');
if (win && lines.length) win.classList.add('is-live');
});
}
function pushLine(key, entry, skipDup) {
if (!key || !entry) return;
key = String(key).toLowerCase();
if (!buffers[key]) buffers[key] = [];
if (skipDup && entry._id) {
const seen = buffers[key].some(function (l) {
return l._id === entry._id;
});
if (seen) return;
}
if (!entry.at) entry.at = nowIso();
buffers[key].push(entry);
if (buffers[key].length > 120) buffers[key].shift();
render(key);
}
function setBusy(key, busy) {
document.querySelectorAll('[data-term-input="' + key + '"]').forEach(function (input) {
input.disabled = !!busy;
input.classList.toggle('is-busy', !!busy);
});
document.querySelectorAll('[data-term-key="' + key + '"]').forEach(function (body) {
const win = body.closest('.agent-term-window');
if (win) win.classList.toggle('is-running', !!busy);
});
}
async function runCommand(key, cmd) {
key = String(key || '').toLowerCase();
cmd = (cmd || '').trim();
if (!cmd) return;
if (cmd.toLowerCase() === 'clear') {
buffers[key] = [];
render(key);
return;
}
pushLine(key, { type: 'command', message: '$ ' + cmd, at: nowIso() });
setBusy(key, true);
try {
const r = await fetch('/api/agents/souls/' + encodeURIComponent(key) + '/terminal/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: cmd }),
});
const data = await r.json().catch(function () {
return {};
});
if (!r.ok) {
const err =
(typeof data.detail === 'string' && data.detail) ||
(Array.isArray(data.detail) && data.detail[0]?.msg) ||
'Commando mislukt';
pushLine(key, { type: 'error', message: err, at: nowIso() });
return;
}
(data.lines || []).forEach(function (line) {
pushLine(key, {
type: line.type || 'output',
message: line.message || '',
detail: line.detail || '',
at: nowIso(),
});
});
} catch (e) {
pushLine(key, { type: 'error', message: e.message || 'Netwerkfout', at: nowIso() });
} finally {
setBusy(key, false);
}
}
function bindInputs() {
document.querySelectorAll('[data-term-input]').forEach(function (input) {
if (input._termBound) return;
input._termBound = true;
const key = input.getAttribute('data-term-input');
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
const val = input.value;
input.value = '';
runCommand(key, val);
}
});
input.addEventListener('click', function (e) {
e.stopPropagation();
});
});
}
async function loadHistory(key) {
try {
const r = await fetch('/api/agents/souls/' + encodeURIComponent(key) + '/events?limit=30');
if (!r.ok) return;
const data = await r.json();
buffers[key] = (data.items || []).map(function (ev) {
const meta = ev.metadata || {};
let type = 'action';
const et = ev.event_type || '';
if (et.indexOf('handoff') >= 0) type = 'handoff_out';
if (meta.target_agent) type = 'handoff_out';
if (meta.source_agent) type = 'handoff_in';
if (et.indexOf('monitor') >= 0) type = 'monitor';
if (et.indexOf('browse') >= 0) type = 'browse';
if (et === 'terminal_in') type = 'command';
if (et === 'terminal_out' || et === 'terminal_note') type = 'output';
return {
_id: ev.id,
type: type,
message: ev.title || ev.event_type,
detail: ev.body,
at: ev.created_at,
};
});
render(key);
} catch (e) {}
}
function handleLivePayload(d) {
if (!d || d.type === 'connected' || d.type === 'error') return;
const key = (d.agent || '').toLowerCase();
if (!key) return;
pushLine(
key,
{
_id: d.id,
type: d.type,
message: d.message,
detail: d.detail,
at: d.at,
},
true
);
document.querySelectorAll('[data-term-key="' + key + '"]').forEach(function (el) {
const win = el.closest('.agent-term-window');
if (win) win.classList.add('is-live');
});
}
function startLiveStream() {
if (liveEs || typeof EventSource === 'undefined') return;
liveEs = new EventSource('/api/agents/live/stream');
liveEs.onmessage = function (ev) {
try {
handleLivePayload(JSON.parse(ev.data));
} catch (e) {}
};
liveEs.onerror = function () {
/* EventSource auto-reconnects */
};
}
function stopLiveStream() {
if (liveEs) {
try {
liveEs.close();
} catch (e) {}
liveEs = null;
}
}
async function mountAll() {
const nodes = document.querySelectorAll('[data-term-key]');
const keys = [];
nodes.forEach(function (n) {
const k = n.getAttribute('data-term-key');
if (k && keys.indexOf(k) < 0) keys.push(k);
});
await Promise.all(keys.map(loadHistory));
bindInputs();
startLiveStream();
}
function stopAll() {
stopLiveStream();
}
window.AgentsTerminals = {
mountAll,
stopAll,
pushLine,
render,
runCommand,
bindInputs,
startLiveStream,
stopLiveStream,
};
})();