77 lines
3.5 KiB
JavaScript
77 lines
3.5 KiB
JavaScript
// #/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 what’s 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);
|
||
}
|