Answer the agents' questions on the page
Menus and permission prompts are read off a seat's screen and shown as labelled buttons (plus Esc); an agent that stopped with a question gets a reply box; queue items blocked on a human seat are decisions recorded with rig queue resolve. Notifications now carry the question itself. Claude-Session: https://claude.ai/code/session_01QhLkBGEip8wQdaZ54z4AqJ
This commit is contained in:
+57
-25
@@ -41,13 +41,63 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function post(path, payload) {
|
||||
const r = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Rig-Pocket': '1' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return r.ok;
|
||||
}
|
||||
|
||||
// A reply box: textarea + button. onSend gets the text and returns a promise of success.
|
||||
function replyBox(placeholder, label, onSend) {
|
||||
const input = $('textarea', { class: 'wt-input', rows: '2', placeholder });
|
||||
const status = $('span', { class: 'wt-muted' });
|
||||
const go = async () => {
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
status.textContent = 'Sender…';
|
||||
if (await onSend(text)) { input.value = ''; status.textContent = 'Sendt'; setTimeout(load, 1500); } else status.textContent = 'Fejl';
|
||||
};
|
||||
return $('div', { class: 'wt-stack' }, input,
|
||||
$('div', { class: 'wt-row wt-row--between' }, status, $('button', { class: 'wt-btn wt-btn--primary', onclick: go }, label)));
|
||||
}
|
||||
|
||||
// What a seat is asking, with the controls to answer it.
|
||||
function askBlock(s, after) {
|
||||
const a = s.ask;
|
||||
if (!a) return null;
|
||||
const done = (ok) => { if (after) after(); setTimeout(load, 1500); return ok; };
|
||||
if (a.kind === 'menu') {
|
||||
return $('div', { class: 'wt-stack' },
|
||||
a.title ? $('span', {}, a.title) : null,
|
||||
$('div', { class: 'wt-stack' },
|
||||
a.options.map((o) => $('button', { class: `wt-btn ${o.key === '1' ? 'wt-btn--primary' : ''}`,
|
||||
onclick: async (e) => { e.target.disabled = true; done(await post('api/send', { seat: s.name, key: o.key })); } },
|
||||
`${o.key}. ${o.label}`)),
|
||||
$('button', { class: 'wt-btn', onclick: async () => done(await post('api/send', { seat: s.name, key: 'esc' })) }, 'Esc (annullér)')));
|
||||
}
|
||||
return $('div', { class: 'wt-stack' },
|
||||
$('blockquote', { class: 'wt-muted' }, a.text),
|
||||
replyBox('Dit svar…', 'Svar', async (text) => done(await post('api/send', { seat: s.name, text }))));
|
||||
}
|
||||
|
||||
function overview() {
|
||||
const view = [];
|
||||
const waiting = snap.rigs.flatMap((r) => r.seats.filter((s) => s.waiting));
|
||||
if (waiting.length) {
|
||||
const gates = snap.queue.filter((q) => q.gate);
|
||||
if (waiting.length || gates.length) {
|
||||
view.push($('section', { class: 'wt-card wt-card--raised wt-stack' },
|
||||
$('h2', { class: 'wt-card-title' }, 'Venter på dig'),
|
||||
waiting.map((s) => $('a', { class: 'wt-row wt-row--between', href: `#/seat/${s.name}` }, s.name, seatTag(s)))));
|
||||
gates.map((q) => $('div', { class: 'wt-stack' },
|
||||
$('div', { class: 'wt-row wt-row--between' }, $('span', { class: 'wt-tag wt-tag--warn' }, 'beslutning'),
|
||||
$('span', { class: 'wt-muted' }, `${q.source} · ${ago(q.updated)}`)),
|
||||
$('span', {}, q.summary || q.id),
|
||||
replyBox('Din beslutning…', 'Send beslutning', (text) => post('api/resolve', { id: q.id, text })))),
|
||||
waiting.map((s) => $('div', { class: 'wt-stack' },
|
||||
$('a', { class: 'wt-row wt-row--between', href: `#/seat/${s.name}` }, $('strong', {}, s.name), seatTag(s)),
|
||||
askBlock(s)))));
|
||||
}
|
||||
|
||||
for (const r of snap.rigs) {
|
||||
@@ -87,15 +137,6 @@ function overview() {
|
||||
return view;
|
||||
}
|
||||
|
||||
async function send(seat, payload) {
|
||||
const r = await fetch('api/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Rig-Pocket': '1' },
|
||||
body: JSON.stringify({ seat, ...payload }),
|
||||
});
|
||||
return r.ok;
|
||||
}
|
||||
|
||||
function seatView(name) {
|
||||
const s = snap.rigs.flatMap((r) => r.seats).find((x) => x.name === name);
|
||||
const pre = $('pre', { class: 'wt-pre' }, 'Henter skærm…');
|
||||
@@ -108,23 +149,12 @@ function seatView(name) {
|
||||
clearInterval(timer);
|
||||
timer = setInterval(refresh, 5000);
|
||||
|
||||
const input = $('textarea', { class: 'wt-input', rows: '3', placeholder: `Skriv til ${name}…` });
|
||||
const status = $('span', { class: 'wt-muted' });
|
||||
const doSend = async (payload) => {
|
||||
status.textContent = 'Sender…';
|
||||
status.textContent = (await send(name, payload)) ? 'Sendt' : 'Fejl';
|
||||
setTimeout(refresh, 1200);
|
||||
};
|
||||
return [
|
||||
$('a', { href: '#/', class: 'wt-muted' }, '← Oversigt'),
|
||||
$('div', { class: 'wt-row wt-row--between' }, $('h2', {}, name), s ? seatTag(s) : null),
|
||||
s && s.ask && s.ask.kind === 'menu' ? $('section', { class: 'wt-card wt-card--raised' }, askBlock(s, () => setTimeout(refresh, 1200))) : null,
|
||||
pre,
|
||||
s && s.waiting ? $('div', { class: 'wt-row' },
|
||||
$('span', { class: 'wt-muted' }, 'Svar på prompten:'),
|
||||
['1', '2', '3'].map((k) => $('button', { class: 'wt-btn', onclick: () => doSend({ key: k }) }, k))) : null,
|
||||
$('div', { class: 'wt-stack' }, input,
|
||||
$('div', { class: 'wt-row wt-row--between' }, status,
|
||||
$('button', { class: 'wt-btn wt-btn--primary', onclick: () => { if (input.value.trim()) { doSend({ text: input.value }); input.value = ''; } } }, 'Send'))),
|
||||
replyBox(`Skriv til ${name}…`, 'Send', async (text) => { const ok = await post('api/send', { seat: name, text }); setTimeout(refresh, 1200); return ok; }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -133,8 +163,10 @@ function render() {
|
||||
document.getElementById('updated').textContent = ago(snap.updated);
|
||||
const m = location.hash.match(/^#\/seat\/(.+)$/);
|
||||
const view = document.getElementById('view');
|
||||
// keep the open seat view (and anything typed) while the overview data refreshes
|
||||
// keep the open seat view, and never wipe an answer being typed
|
||||
if (m && view.dataset.seat === m[1]) return;
|
||||
const typing = [...view.querySelectorAll('textarea')].some((t) => t.value || t === document.activeElement);
|
||||
if (!m && !view.dataset.seat && typing) return;
|
||||
view.dataset.seat = m ? m[1] : '';
|
||||
if (!m) clearInterval(timer);
|
||||
view.replaceChildren(...(m ? seatView(decodeURIComponent(m[1])) : overview()).filter(Boolean));
|
||||
|
||||
Reference in New Issue
Block a user