Some checks are pending
build-and-push / image (push) Waiting to run
- Implemented demand pane for managing trend-driven design jobs. - Created diffusion pane for generating images via Stable Diffusion. - Added inbox pane for sweeping and routing artifacts through the CxAI inbox classifier. - Developed items pane for CRUD operations against /api/items. - Introduced lang pane for running language pipelines. - Established mac pane for macOS app distribution information. - Integrated slack pane for sending messages and displaying diagnostics. - Built system pane for process introspection and version information. - Launched tools pane for browsing and invoking MCP tools. - Set up websocket pane for connecting to the /ws/echo service.
149 lines
5.7 KiB
JavaScript
149 lines
5.7 KiB
JavaScript
// panes/agent.js — absorbs the standalone CxAI dashboard.
|
|
// MCP status + live events feed (2s poll) + semantic search.
|
|
import { registerPane } from '../app.js';
|
|
import { mcp, MCP_BASE, escapeHtml } from '../lib/api.js';
|
|
import { ok, err, fmtJSON, skeleton } from '../lib/ui.js';
|
|
|
|
const TPL = `
|
|
<div class="pane-head">
|
|
<div>
|
|
<div class="title">Agent</div>
|
|
<div class="sub">CxAI MCP control plane · <span class="mono">${escapeHtml(MCP_BASE)}</span></div>
|
|
</div>
|
|
<div class="grow"></div>
|
|
<span class="pill" id="agent-pill"><span class="dot"></span><span id="agent-pill-text">checking…</span></span>
|
|
</div>
|
|
|
|
<div class="grid cols-2">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Status</h2><span class="muted mono" id="agent-status-ts">—</span></div>
|
|
<pre id="agent-status">${skeleton(4)}</pre>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Semantic search</h2><span class="muted" id="agent-search-meta"></span></div>
|
|
<form id="agent-search-form" class="flex gap-2 mb-3">
|
|
<input class="input" id="agent-q" placeholder="ask the index…" autocomplete="off" />
|
|
<button class="btn btn-primary" type="submit">Search</button>
|
|
</form>
|
|
<div id="agent-hits" class="muted">Run a query to see results.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-title">
|
|
<h2>Live events</h2>
|
|
<label class="check"><input type="checkbox" id="agent-tail" checked /> auto-refresh (2s)</label>
|
|
</div>
|
|
<div class="scroll" style="max-height: 50vh">
|
|
<table class="t" id="agent-events">
|
|
<thead><tr><th style="width:160px">time</th><th style="width:200px">kind</th><th>detail</th></tr></thead>
|
|
<tbody><tr><td colspan="3" class="muted">loading…</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
let timer = null;
|
|
|
|
function setPill(state, text) {
|
|
const p = document.getElementById('agent-pill');
|
|
const t = document.getElementById('agent-pill-text');
|
|
const navPill = document.getElementById('nav-agent-pill');
|
|
if (!p || !t) return;
|
|
p.classList.remove('ok', 'err', 'warn', 'info');
|
|
p.classList.add(state);
|
|
t.textContent = text;
|
|
if (navPill) {
|
|
navPill.textContent = state === 'ok' ? '●' : state === 'err' ? '!' : '·';
|
|
navPill.style.color = state === 'ok' ? 'var(--ok)' : state === 'err' ? 'var(--err)' : '';
|
|
}
|
|
}
|
|
|
|
async function refreshStatus(host) {
|
|
try {
|
|
const s = await mcp.status();
|
|
host.querySelector('#agent-status').textContent = fmtJSON(s);
|
|
host.querySelector('#agent-status-ts').textContent = new Date().toLocaleTimeString();
|
|
setPill('ok', 'connected');
|
|
} catch (e) {
|
|
host.querySelector('#agent-status').textContent = `error: ${e.message}\n\nIs the MCP HTTP sidecar running on ${MCP_BASE}?`;
|
|
setPill('err', 'unavailable');
|
|
}
|
|
}
|
|
|
|
function fmtEventTs(ts) {
|
|
if (!ts) return '';
|
|
try {
|
|
if (typeof ts === 'number') return new Date(ts * (ts < 1e12 ? 1000 : 1)).toLocaleTimeString();
|
|
return new Date(ts).toLocaleTimeString();
|
|
} catch { return String(ts); }
|
|
}
|
|
|
|
async function refreshEvents(host) {
|
|
try {
|
|
const arr = await mcp.events(150);
|
|
const rows = (arr || []).slice().reverse().slice(0, 100).map(ev => `
|
|
<tr>
|
|
<td class="mono muted">${escapeHtml(fmtEventTs(ev.ts ?? ev.timestamp ?? ev.time))}</td>
|
|
<td><span class="pill info">${escapeHtml(ev.kind || ev.type || 'event')}</span></td>
|
|
<td class="mono" style="word-break:break-word">${escapeHtml(typeof ev.detail === 'string' ? ev.detail : JSON.stringify(ev.detail ?? ev.data ?? ev))}</td>
|
|
</tr>
|
|
`).join('');
|
|
host.querySelector('#agent-events tbody').innerHTML =
|
|
rows || '<tr><td colspan="3" class="muted">no events yet</td></tr>';
|
|
} catch (e) {
|
|
host.querySelector('#agent-events tbody').innerHTML =
|
|
`<tr><td colspan="3" class="muted">events unavailable: ${escapeHtml(e.message)}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
async function search(host) {
|
|
const q = host.querySelector('#agent-q').value.trim();
|
|
if (!q) return;
|
|
host.querySelector('#agent-search-meta').textContent = 'searching…';
|
|
host.querySelector('#agent-hits').innerHTML = skeleton(3);
|
|
try {
|
|
const res = await mcp.call('search_semantic_tool', { query: q, k: 10 });
|
|
const hits = res?.hits || res?.result?.hits || res?.results || res || [];
|
|
host.querySelector('#agent-search-meta').textContent = `${hits.length} hits`;
|
|
host.querySelector('#agent-hits').innerHTML = hits.length ? hits.map(h => `
|
|
<div class="row">
|
|
<div class="grow">
|
|
<div class="ttl mono">${escapeHtml(h.path || h.id || '?')}</div>
|
|
<div class="desc">${escapeHtml((h.snippet || h.text || '').slice(0, 280))}</div>
|
|
</div>
|
|
<span class="pill muted mono">${(h.distance ?? h.score ?? '').toString().slice(0, 6)}</span>
|
|
</div>
|
|
`).join('') : '<div class="muted" style="padding:12px">no hits</div>';
|
|
} catch (e) {
|
|
host.querySelector('#agent-search-meta').textContent = 'error';
|
|
host.querySelector('#agent-hits').innerHTML = `<div class="muted" style="padding:12px">${escapeHtml(e.message)}</div>`;
|
|
err(`search: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
registerPane('agent', {
|
|
label: 'Agent',
|
|
init(host) {
|
|
host.innerHTML = TPL;
|
|
host.querySelector('#agent-search-form').addEventListener('submit', (e) => {
|
|
e.preventDefault(); search(host);
|
|
});
|
|
refreshStatus(host);
|
|
refreshEvents(host);
|
|
|
|
const tick = () => {
|
|
if (timer) clearInterval(timer);
|
|
timer = setInterval(() => {
|
|
if (!host.classList.contains('active')) return;
|
|
if (!host.querySelector('#agent-tail')?.checked) return;
|
|
refreshEvents(host);
|
|
refreshStatus(host);
|
|
}, 2000);
|
|
};
|
|
tick();
|
|
},
|
|
refresh(host) { refreshStatus(host); refreshEvents(host); },
|
|
});
|