CxWebApp/static/js/panes/lang.js
CxAI Agent 75153b7fe9
Some checks are pending
build-and-push / image (push) Waiting to run
feat(cxwebapp): comprehensive pane enhancements
- All 14 panes rewritten with rich controls: KPIs, sparklines, meter
  gauges, presets, history sidebars, search/filter, bulk actions,
  schema-driven forms, copy buttons, auto-refresh.
- Backend /api/system enriched with cpu_count, rss_bytes, mem_total,
  mem_used_pct, loadavg[], user/system CPU times, max_rss_kb.
- CSS additions: .meter/.gauge, .seg, .chip(s), .spark, .slider, .dl,
  .link-card, details.card-d plus utility classes.
- ui.js helpers: copyToClipboard, withCopy/bindCopyButtons, sparkline,
  meterRow, fmtBytes/fmtNum, historyStore, inputFromSchema,
  collectSchemaForm.
- Files pane added; iCloud duplicate '*2' files removed.
2026-05-17 09:36:19 -05:00

169 lines
7.8 KiB
JavaScript

// panes/lang.js — language pipelines with cards, examples, run history.
import { registerPane } from '../app.js';
import { jget, jpost, escapeHtml } from '../lib/api.js';
import { fmtJSON, err, ok, copyToClipboard, historyStore } from '../lib/ui.js';
const runHist = historyStore('cx_lang_runs', 20);
const TPL = `
<div class="pane-head">
<div><div class="title">Lang</div><div class="sub">Run language pipelines via the sidecar.</div></div>
<div class="grow"></div>
<span class="pill" id="lang-pill"><span class="dot"></span><span id="lang-pill-text">—</span></span>
<button class="btn btn-secondary" id="lang-refresh">Refresh</button>
</div>
<div class="grid" style="grid-template-columns: 320px 1fr; gap: 16px;">
<div class="card no-pad scroll" style="max-height:74vh">
<div class="p-2"><input class="input" id="lang-search" placeholder="filter pipelines…"/></div>
<div id="lang-pipelines">loading…</div>
</div>
<div class="card">
<div class="card-title">
<h2 id="lang-name">Select a pipeline</h2>
<span class="muted xsmall" id="lang-status">—</span>
</div>
<p class="muted small" id="lang-desc">Pick a pipeline on the left.</p>
<div id="lang-body" hidden>
<div class="card-title"><h2 style="font-size:14px">Examples</h2></div>
<div class="chips mb-3" id="lang-examples"></div>
<label class="field"><span class="lbl">Input (JSON)</span>
<textarea class="input mono" id="lang-input" rows="8">{}</textarea>
</label>
<div class="btn-row mt-3">
<button class="btn btn-primary" id="lang-run">Run</button>
<button class="btn btn-secondary" id="lang-format">Format JSON</button>
<button class="btn btn-secondary" id="lang-copy-input">Copy input</button>
<button class="btn btn-ghost" id="lang-health">Health</button>
</div>
<div class="divider"></div>
<div class="card-title"><h2 style="font-size:14px">Result</h2>
<div class="flex gap-2">
<span class="muted xsmall" id="lang-meta"></span>
<button class="btn btn-ghost" id="lang-copy-out">Copy</button>
</div>
</div>
<pre id="lang-result" class="code muted" style="max-height:32vh">—</pre>
<details class="card-d mt-3"><summary>Recent runs</summary>
<div id="lang-history" class="body"></div>
</details>
</div>
</div>
</div>
`;
let pipelines = [];
let current = null;
function renderList(host) {
const q = host.querySelector('#lang-search').value.trim().toLowerCase();
const items = pipelines.filter(p => !q || (p.name + ' ' + (p.description || '')).toLowerCase().includes(q));
host.querySelector('#lang-pipelines').innerHTML = items.length
? items.map(p => `<button class="nav-item" data-p="${escapeHtml(p.name)}">
<span class="grow">
<div class="mono">${escapeHtml(p.name)}</div>
<div class="muted xsmall truncate">${escapeHtml(p.description || '')}</div>
</span>
</button>`).join('')
: '<div class="muted xsmall p-2">no pipelines</div>';
host.querySelectorAll('[data-p]').forEach(b => b.addEventListener('click', () => select(host, b.dataset.p)));
}
function renderHistory(host) {
const list = runHist.list().filter(r => !current || r.name === current.name);
host.querySelector('#lang-history').innerHTML = list.length
? list.map((r, i) => `<button class="nav-item" data-hi="${i}">
<span class="muted xsmall mono">${new Date(r._ts).toLocaleTimeString()}</span>
<span class="grow truncate">${escapeHtml(r.name)}</span>
<span class="pill ${r.ok ? 'ok' : 'err'}">${r.ok ? `${r.ms || '?'}ms` : 'err'}</span>
</button>`).join('')
: '<div class="muted xsmall p-2">no runs</div>';
host.querySelectorAll('#lang-history [data-hi]').forEach(b => b.addEventListener('click', () => {
const r = list[+b.dataset.hi];
if (r) { host.querySelector('#lang-input').value = fmtJSON(r.input); host.querySelector('#lang-result').textContent = fmtJSON(r.output); }
}));
}
function select(host, name) {
current = pipelines.find(p => p.name === name);
if (!current) return;
host.querySelector('#lang-name').textContent = current.name;
host.querySelector('#lang-desc').textContent = current.description || '—';
host.querySelector('#lang-body').hidden = false;
const ex = current.examples || current.sample_inputs || [];
host.querySelector('#lang-examples').innerHTML = ex.length
? ex.map((e, i) => `<button class="chip" data-ex="${i}">${escapeHtml(e.label || e.name || `example ${i + 1}`)}</button>`).join('')
: '<span class="muted xsmall">no examples</span>';
host.querySelectorAll('#lang-examples .chip').forEach(b => b.addEventListener('click', () => {
const e = ex[+b.dataset.ex];
host.querySelector('#lang-input').value = fmtJSON(e.input ?? e.value ?? e);
}));
host.querySelector('#lang-input').value = fmtJSON(current.default_input || {});
renderHistory(host);
}
async function refresh(host) {
try {
const r = await jget('/api/lang/pipelines');
pipelines = r.pipelines || [];
renderList(host);
host.querySelector('#lang-pill').classList.remove('err', 'warn'); host.querySelector('#lang-pill').classList.add('ok');
host.querySelector('#lang-pill-text').textContent = `${pipelines.length} pipelines`;
} catch (e) {
host.querySelector('#lang-pipelines').innerHTML = `<div class="muted xsmall p-2">${escapeHtml(e.message)}</div>`;
host.querySelector('#lang-pill').classList.add('err'); host.querySelector('#lang-pill-text').textContent = 'down';
}
}
registerPane('lang', {
label: 'Lang',
init(host) {
host.innerHTML = TPL;
host.querySelector('#lang-refresh').addEventListener('click', () => refresh(host));
host.querySelector('#lang-search').addEventListener('input', () => renderList(host));
host.querySelector('#lang-run').addEventListener('click', async () => {
if (!current) return err('select a pipeline');
let input;
try { input = JSON.parse(host.querySelector('#lang-input').value || '{}'); }
catch (e) { return err(`bad JSON: ${e.message}`); }
host.querySelector('#lang-status').textContent = 'running…';
const t0 = performance.now();
try {
const r = await jpost(`/api/lang/pipelines/${encodeURIComponent(current.name)}`, { input });
const ms = +r.duration_ms || (performance.now() - t0).toFixed(0);
host.querySelector('#lang-result').textContent = fmtJSON(r);
host.querySelector('#lang-status').textContent = `ok · ${ms}ms`;
host.querySelector('#lang-meta').textContent = `${ms}ms`;
runHist.push({ name: current.name, input, output: r, ms, ok: true });
renderHistory(host); ok('done');
} catch (e) {
host.querySelector('#lang-result').textContent = fmtJSON(e.body ?? { error: e.message });
host.querySelector('#lang-status').textContent = `error ${e.status ?? ''}`;
runHist.push({ name: current.name, input, error: e.message, ok: false });
renderHistory(host); err(e.message);
}
});
host.querySelector('#lang-format').addEventListener('click', () => {
const ta = host.querySelector('#lang-input');
try { ta.value = fmtJSON(JSON.parse(ta.value || '{}')); } catch (e) { err('bad JSON: ' + e.message); }
});
host.querySelector('#lang-copy-input').addEventListener('click', () => copyToClipboard(host.querySelector('#lang-input').value));
host.querySelector('#lang-copy-out').addEventListener('click', () => copyToClipboard(host.querySelector('#lang-result').textContent));
host.querySelector('#lang-health').addEventListener('click', async () => {
try { host.querySelector('#lang-result').textContent = fmtJSON(await jget('/api/lang/healthz')); }
catch (e) { host.querySelector('#lang-result').textContent = e.message; }
});
refresh(host);
},
refresh,
});