Forward Authorization in nginx; accept Spec 027 roles in JWT decode. Co-authored-by: Cursor <cursoragent@cursor.com>
213 lines
11 KiB
JavaScript
213 lines
11 KiB
JavaScript
(function () {
|
||
const esc = (s) => String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
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 (A0–A7)</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;
|
||
})();
|