ligbox-ops-platform/projects/ops-desk/frontend/assets/agentic-ops.js
Ligbox Spec Hub 6daa692af8 Fix Agentic Ops 401 — reuse Desk global api() and session check.
Forward Authorization in nginx; accept Spec 027 roles in JWT decode.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-20 06:28:42 +00:00

213 lines
11 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.

(function () {
const esc = (s) => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
let state = { threadId: null, selectedAgent: 'A6' };
/** Usa o helper global do Desk (app.js) — garante JWT igual aos outros módulos. */
async function agentsApi(path, opts = {}) {
const deskApi = typeof globalThis.api === 'function' ? globalThis.api : null;
if (deskApi) return deskApi(`/v1/agents${path}`, opts);
const h = authHeaders({ ...(opts.headers || {}) });
if (!(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h }, 60000);
if (r.status === 401) {
logout();
throw new Error('sessão expirada — faça login novamente');
}
if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
return r.json();
}
function agentCard(a) {
const active = state.selectedAgent === a.id ? ' agentic-agent-active' : '';
return `<article class="card agentic-agent-card${active}" data-agent-id="${esc(a.id)}" tabindex="0">
<h4>${esc(a.name)} <span class="pill pill-sm">${esc(a.id)}</span></h4>
<p class="ticket-meta">${esc(a.role)}</p>
<ul class="agentic-action-list">${(a.actions || []).slice(0, 3).map(x => `<li>${esc(x)}</li>`).join('')}</ul>
<p class="ticket-meta"><strong>Aprovação:</strong> ${esc(a.approval)}</p>
</article>`;
}
function inboxRow(m) {
return `<article class="card agentic-inbox-item" data-msg-id="${m.id}" data-thread-id="${m.thread_id}">
<div class="agentic-inbox-head">
<strong>${esc(m.agent_name || m.from_id)}</strong>
<span class="pill">${esc(m.thread_severity || 'info')}</span>
</div>
<p class="ticket-meta">${esc(m.thread_subject || m.message)} · ${esc(m.created_at)}</p>
<p class="agentic-inbox-body">${esc((m.body || '').slice(0, 280))}</p>
<div class="agentic-inbox-actions">
<button type="button" class="btn btn-primary btn-sm" data-open-thread="${m.thread_id}">Abrir conversa</button>
<button type="button" class="btn btn-ghost btn-sm" data-ack-msg="${m.id}">Arquivar</button>
</div>
</article>`;
}
function threadBubble(m) {
const isHuman = m.from_type === 'human';
const cls = isHuman ? 'agentic-bubble-human' : 'agentic-bubble-agent';
return `<div class="agentic-bubble ${cls}">
<div class="agentic-bubble-meta">${esc(m.from_label || m.from_id)} · ${esc(m.created_at)}</div>
<div class="agentic-bubble-body">${esc(m.body).replace(/\n/g, '<br>')}</div>
</div>`;
}
async function loadThread(el, threadId) {
state.threadId = threadId;
const box = el.querySelector('#agentic-thread-messages');
if (!box) return;
box.innerHTML = '<p class="loading">Carregando thread…</p>';
const data = await agentsApi(`/threads/${threadId}/messages`);
box.innerHTML = data.messages.map(threadBubble).join('') || '<p class="empty">Sem mensagens.</p>';
box.scrollTop = box.scrollHeight;
}
async function renderAgenticOps() {
const el = document.getElementById('agentic-ops-content');
if (!el) return;
el.innerHTML = '<p class="loading">Carregando Agentic Ops…</p>';
if (!getToken()) {
el.innerHTML = '<p class="error">Sessão não encontrada neste endereço. <a href="/login.html">Fazer login</a> (use sempre o mesmo URL — ex. desk.ligbox.com.br).</p>';
return;
}
if (typeof ensureValidSession === 'function') {
const ok = await ensureValidSession();
if (!ok) {
el.innerHTML = '<p class="error">Sessão expirada. <a href="/login.html">Fazer login</a></p>';
return;
}
}
try {
const [health, roster, inbox, threads, findings] = await Promise.all([
agentsApi('/health'),
agentsApi('/roster'),
agentsApi('/inbox?limit=20'),
agentsApi('/threads?limit=15'),
agentsApi('/findings?limit=15'),
]);
const tier = health.tier === 't1' ? 'T1 LLM' : 'T0';
const ollama = health.ollama
? `<span class="pill pill-ok">Ollama · ${esc(health.model)}</span>`
: '<span class="pill pill-warn">Ollama offline</span>';
const agents = roster.agents || [];
const inboxItems = inbox.messages || [];
const threadOpts = (threads.threads || []).map(t =>
`<option value="${t.id}"${state.threadId === t.id ? ' selected' : ''}>#${t.id} ${esc(t.subject)} (${esc(t.agent_name)})</option>`
).join('');
const fRows = (findings.findings || []).map(f =>
`<li><strong>${esc(f.title)}</strong> <span class="pill">${esc(f.severity)}</span>
${f.suggested_human_action ? `<br><span class="ticket-meta">${esc(f.suggested_human_action)}</span>` : ''}</li>`
).join('') || '<li class="empty">Nenhum finding aberto.</li>';
el.innerHTML = `
<style>
.agentic-layout{display:grid;grid-template-columns:minmax(200px,1fr) minmax(260px,1.2fr) minmax(280px,1.4fr);gap:1rem;margin-top:.5rem}
@media(max-width:1100px){.agentic-layout{grid-template-columns:1fr}}
.agentic-agent-card{cursor:pointer;border:1px solid var(--border,#333);margin-bottom:.5rem;padding:.75rem}
.agentic-agent-active{border-color:#3b82f6;box-shadow:0 0 0 1px #3b82f680}
.agentic-action-list{font-size:.85rem;margin:.4rem 0;padding-left:1.1rem;color:var(--muted,#aaa)}
.agentic-inbox-item{margin-bottom:.75rem}
.agentic-inbox-body{font-size:.9rem;margin:.35rem 0;white-space:pre-wrap}
.agentic-thread-panel{display:flex;flex-direction:column;min-height:420px}
.agentic-thread-messages{flex:1;overflow-y:auto;max-height:360px;padding:.5rem;background:rgba(0,0,0,.15);border-radius:6px;margin:.5rem 0}
.agentic-bubble{margin:.5rem 0;padding:.6rem .8rem;border-radius:8px;max-width:95%}
.agentic-bubble-agent{background:rgba(59,130,246,.12);border-left:3px solid #3b82f6}
.agentic-bubble-human{background:rgba(34,197,94,.1);border-left:3px solid #22c55e;margin-left:auto}
.agentic-bubble-meta{font-size:.75rem;color:var(--muted,#888);margin-bottom:.25rem}
.agentic-chat-box{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem}
.agentic-chat-box textarea{width:100%;min-height:72px}
</style>
<div class="toolbar agentic-toolbar">
<div><h2>Agentic Ops</h2><p class="ticket-meta">Spec 029 · ${tier} ${ollama} · ${inboxItems.length} pendente(s)</p></div>
<button type="button" class="btn btn-primary btn-sm" id="btn-agentic-refresh">Actualizar</button>
</div>
<div class="agentic-layout">
<section>
<h3>Agentes (A0A7)</h3>
<p class="ticket-meta">Clique para seleccionar destino do chat.</p>
${agents.map(agentCard).join('')}
</section>
<section>
<h3>Inbox operadores</h3>
<p class="ticket-meta">Mensagens dos agentes que exigem acção humana.</p>
${inboxItems.length ? inboxItems.map(inboxRow).join('') : '<p class="empty">Inbox vazia.</p>'}
<h3 style="margin-top:1rem">Findings abertos</h3>
<ul class="agentic-findings-list">${fRows}</ul>
</section>
<section class="card agentic-thread-panel">
<h3>Janela de contexto</h3>
<label class="ticket-meta">Thread
<select id="agentic-thread-select" class="input">${threadOpts || '<option value="">—</option>'}</select>
</label>
<div id="agentic-thread-messages" class="agentic-thread-messages"><p class="empty">Seleccione uma thread ou abra da inbox.</p></div>
<textarea id="agentic-reply-input" rows="2" class="input" placeholder="Responder ao agente…"></textarea>
<button type="button" class="btn btn-primary btn-sm" id="btn-agentic-reply">Enviar resposta</button>
<hr style="margin:1rem 0;opacity:.3">
<h4>Chat Copiloto (${esc(state.selectedAgent)})</h4>
<div class="agentic-chat-box">
<textarea id="agentic-chat-input" rows="2" placeholder="Pergunta ao agente seleccionado…"></textarea>
<button type="button" class="btn btn-ghost btn-sm" id="btn-agentic-chat">Perguntar</button>
</div>
<div id="agentic-chat-answer" class="agentic-chat-answer" hidden></div>
</section>
</div>`;
el.querySelector('#btn-agentic-refresh')?.addEventListener('click', renderAgenticOps);
el.querySelectorAll('.agentic-agent-card').forEach(card => {
card.addEventListener('click', () => {
state.selectedAgent = card.dataset.agentId;
renderAgenticOps();
});
});
el.querySelectorAll('[data-open-thread]').forEach(btn => {
btn.addEventListener('click', () => loadThread(el, parseInt(btn.dataset.openThread, 10)));
});
el.querySelectorAll('[data-ack-msg]').forEach(btn => {
btn.addEventListener('click', async () => {
await agentsApi(`/messages/${btn.dataset.ackMsg}/ack`, { method: 'POST' });
await renderAgenticOps();
});
});
el.querySelector('#agentic-thread-select')?.addEventListener('change', (e) => {
const id = parseInt(e.target.value, 10);
if (id) loadThread(el, id);
});
el.querySelector('#btn-agentic-reply')?.addEventListener('click', async () => {
const input = el.querySelector('#agentic-reply-input');
const tid = state.threadId || parseInt(el.querySelector('#agentic-thread-select')?.value, 10);
const body = (input?.value || '').trim();
if (!tid || !body) return;
await agentsApi(`/threads/${tid}/reply`, {
method: 'POST',
body: JSON.stringify({ body, target_agent: state.selectedAgent }),
});
input.value = '';
await loadThread(el, tid);
});
el.querySelector('#btn-agentic-chat')?.addEventListener('click', async () => {
const input = el.querySelector('#agentic-chat-input');
const out = el.querySelector('#agentic-chat-answer');
const q = (input?.value || '').trim();
if (!q) return;
out.hidden = false;
out.innerHTML = '<p class="loading">A pensar…</p>';
try {
const res = await agentsApi('/chat', {
method: 'POST',
body: JSON.stringify({ question: q, include_findings: true, target_agent: state.selectedAgent }),
});
out.innerHTML = `<p><strong>${esc(state.selectedAgent)}</strong> <span class="ticket-meta">(${esc(res.model)})</span></p><p>${esc(res.answer)}</p>`;
if (res.thread_id) {
state.threadId = res.thread_id;
await loadThread(el, res.thread_id);
}
} catch (err) {
out.innerHTML = `<p class="error">${esc(err.message)}</p>`;
}
});
if (state.threadId) await loadThread(el, state.threadId);
} catch (err) {
el.innerHTML = `<p class="error">Erro: ${esc(err.message)}</p>`;
}
}
window.renderAgenticOps = renderAgenticOps;
})();