CxWebApp/static/js/panes/slack.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

132 lines
6.2 KiB
JavaScript

// panes/slack.js — Slack sidecar with respond form, presets, tools.
import { registerPane } from '../app.js';
import { jget, jpost, escapeHtml } from '../lib/api.js';
import { fmtJSON, err, ok, copyToClipboard } from '../lib/ui.js';
const TPL = `
<div class="pane-head">
<div><div class="title">Slack</div><div class="sub">Sidecar health, conversation memory, ad-hoc responses.</div></div>
<div class="grow"></div>
<span class="pill" id="slack-pill"><span class="dot"></span><span id="slack-pill-text">—</span></span>
<button class="btn btn-secondary" id="slack-refresh">Refresh</button>
</div>
<div class="grid cols-2 mb-3">
<div class="card">
<div class="card-title"><h2>Respond</h2><span class="muted xsmall" id="slack-resp-meta">—</span></div>
<form id="slack-form" class="grid gap-3">
<label class="field">
<span class="lbl">Message</span>
<textarea class="input" id="slack-text" rows="3" required placeholder="Hello team!"></textarea>
</label>
<div class="grid cols-2 gap-3">
<label class="field"><span class="lbl">Channel</span><input class="input" id="slack-channel" placeholder="rest" value="rest"/></label>
<label class="field"><span class="lbl">Thread ts</span><input class="input" id="slack-thread" placeholder="rest" value="rest"/></label>
</div>
<div class="btn-row">
<button class="btn btn-primary" type="submit">Generate reply</button>
<button type="button" class="btn btn-secondary" data-preset="standup">Standup</button>
<button type="button" class="btn btn-secondary" data-preset="bug">Bug report</button>
<button type="button" class="btn btn-secondary" data-preset="ship">Ship update</button>
</div>
</form>
<div class="divider"></div>
<div class="card-title"><h2 style="font-size:14px">Reply</h2><button class="btn btn-ghost" id="slack-copy">Copy</button></div>
<pre id="slack-reply" class="code muted" style="max-height:24vh">—</pre>
</div>
<div class="card">
<div class="card-title"><h2>Tools</h2><span class="muted xsmall" id="slack-tools-meta">—</span></div>
<div id="slack-tools-list" class="scroll" style="max-height:22vh">…</div>
<div class="divider"></div>
<div class="card-title"><h2 style="font-size:14px">Conversation memory</h2>
<button class="btn btn-ghost" id="slack-mem-copy">Copy</button>
</div>
<pre id="slack-memory" class="code muted" style="max-height:28vh">…</pre>
</div>
</div>
<div class="grid cols-2">
<div class="card">
<div class="card-title"><h2>healthz</h2></div>
<pre id="slack-health" class="code" style="max-height:24vh">…</pre>
</div>
<div class="card">
<div class="card-title"><h2>info</h2></div>
<pre id="slack-info" class="code" style="max-height:24vh">…</pre>
</div>
</div>
`;
const PRESETS = {
standup: 'Daily standup: ✅ Yesterday — ___ · 🎯 Today — ___ · 🚧 Blockers — none',
bug: 'Bug report: **Title** — short summary\n\n**Steps to reproduce:**\n1. ___\n2. ___\n\n**Expected:** ___\n**Actual:** ___\n**Env:** ___',
ship: '🚀 Shipped: **<feature>** — short summary. Try it at <link>. Feedback welcome 🙏',
};
async function refresh(host) {
try {
const [h, info, mem, tools] = await Promise.all([
jget('/api/slack/healthz').catch(e => ({ error: e.message })),
jget('/api/slack/info').catch(e => ({ error: e.message })),
jget('/api/slack/memory').catch(e => ({ error: e.message })),
jget('/api/slack/tools').catch(e => ({ error: e.message })),
]);
host.querySelector('#slack-health').textContent = fmtJSON(h);
host.querySelector('#slack-info').textContent = fmtJSON(info);
host.querySelector('#slack-memory').textContent = fmtJSON(mem);
const items = tools.tools || [];
host.querySelector('#slack-tools-meta').textContent = `${items.length} tools`;
host.querySelector('#slack-tools-list').innerHTML = items.length
? items.map(t => `<div class="row">
<div class="grow"><div class="ttl mono">${escapeHtml(t.name)}</div><div class="desc">${escapeHtml(t.description || '')}</div></div>
</div>`).join('')
: '<div class="muted xsmall p-2">(none)</div>';
const ok2 = !h?.error;
const pill = host.querySelector('#slack-pill');
pill.classList.remove('ok', 'err'); pill.classList.add(ok2 ? 'ok' : 'err');
host.querySelector('#slack-pill-text').textContent = ok2 ? 'up' : 'down';
} catch (e) {
host.querySelector('#slack-health').textContent = e.message;
}
}
registerPane('slack', {
label: 'Slack',
init(host) {
host.innerHTML = TPL;
host.querySelector('#slack-refresh').addEventListener('click', () => refresh(host));
host.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', () => {
host.querySelector('#slack-text').value = PRESETS[b.dataset.preset] || '';
}));
host.querySelector('#slack-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#slack-reply').textContent));
host.querySelector('#slack-mem-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#slack-memory').textContent));
host.querySelector('#slack-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = {
text: host.querySelector('#slack-text').value,
channel: host.querySelector('#slack-channel').value || 'rest',
thread_ts: host.querySelector('#slack-thread').value || 'rest',
};
host.querySelector('#slack-reply').textContent = 'thinking…';
host.querySelector('#slack-resp-meta').textContent = '…';
const t0 = performance.now();
try {
const r = await jpost('/api/slack/respond', body);
host.querySelector('#slack-reply').textContent = r.reply || fmtJSON(r);
host.querySelector('#slack-resp-meta').textContent = `${(performance.now() - t0).toFixed(0)}ms`;
ok('replied');
} catch (e) {
host.querySelector('#slack-reply').textContent = e.body?.detail || e.message;
host.querySelector('#slack-resp-meta').textContent = `error ${e.status ?? ''}`;
err(e.message);
}
});
refresh(host);
},
refresh,
});