Files
Void-Homelab/public/views/little_blue.js
2026-06-04 21:44:35 +10:00

77 lines
3.5 KiB
JavaScript
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.
// #/little-blue — the caretaker page: Little Blue's chat (reuses agent_chat) +
// a manual Actions panel (run whitelisted actions; approve/reject risky ones).
import { el, mount, clear } from '../dom.js';
import { api } from '../api.js';
import { wireAgentChat } from '../components/agent_chat.js';
const BLUE_LABELS = {
search: '🔍 looking', list_actions: '📋 checking what she can do', propose_action: '🔧 taking action'
};
function toast(host, msg, kind) {
const t = el('div', { class: 'lb-toast' + (kind === 'err' ? ' err' : '') }, msg);
host.appendChild(t);
setTimeout(() => t.remove(), 4000);
}
async function renderActions(panel, toastHost) {
clear(panel);
let actions = [], pending = [];
try { ({ actions } = await api.get('/api/actions')); } catch (e) { actions = []; }
try { ({ pending } = await api.get('/api/actions/pending')); } catch (e) { pending = []; }
panel.appendChild(el('div', { class: 'lb-sec-t' }, 'Actions'));
if (!actions.length) {
panel.appendChild(el('div', { class: 'muted' }, 'No actions configured yet (populate config/actions.json).'));
}
for (const a of actions) {
panel.appendChild(el('div', { class: 'lb-action' },
el('span', { class: 'lb-a-label' }, a.label || a.id),
el('span', { class: 'lb-a-tier ' + a.tier }, a.tier),
el('button', { class: 'lb-run', onclick: async (ev) => {
ev.target.disabled = true;
try {
const r = await api.post(`/api/actions/${a.id}/run`);
toast(toastHost, r.executed ? `Ran "${a.label || a.id}"` : `Queued "${a.label || a.id}" for approval`);
renderActions(panel, toastHost);
} catch (e) { toast(toastHost, 'Failed: ' + e.message, 'err'); ev.target.disabled = false; }
} }, a.tier === 'risky' ? 'Request' : 'Run')));
}
if (pending.length) {
panel.appendChild(el('div', { class: 'lb-sec-t' }, 'Awaiting approval'));
for (const p of pending) {
const row = el('div', { class: 'lb-pending' },
el('span', { class: 'lb-a-label' }, p.action_id),
el('button', { class: 'ok', onclick: () => resolve(p.id, 'approve') }, 'Approve'),
el('button', { class: 'no', onclick: () => resolve(p.id, 'reject') }, 'Reject'));
panel.appendChild(row);
async function resolve(id, verb) {
try { await api.post(`/api/actions/pending/${id}/${verb}`); toast(toastHost, verb === 'approve' ? 'Approved' : 'Rejected'); renderActions(panel, toastHost); }
catch (e) { toast(toastHost, 'Failed: ' + e.message, 'err'); }
}
}
}
}
export async function render(main) {
const log = el('div', { class: 'rail-log sentinel-log' });
const input = el('textarea', { class: 'rail-input', rows: 1, placeholder: 'Tell Little Blue whats wrong…' });
const actionsPanel = el('div', { class: 'lb-actions' });
const toastHost = el('div', { class: 'lb-toasts' });
mount(main,
el('h1', { class: 'view-h1' }, '◆ Little Blue — Caretaker'),
el('p', { class: 'view-sub' }, 'She keeps the lab alive. Safe fixes run on her word; risky ones wait for yours.'),
toastHost,
el('div', { class: 'lb-grid' },
el('div', { class: 'lb-chat' }, log, el('div', { class: 'rail-inputwrap' }, input)),
actionsPanel));
const chat = wireAgentChat({
logEl: log, inputEl: input,
historyUrl: '/api/little-blue', turnUrl: '/api/little-blue/turn',
agentName: 'Little Blue', showDrafts: false, toolLabels: BLUE_LABELS
});
await chat.load();
await renderActions(actionsPanel, toastHost);
}