function remApi() {
return window.LctxApi && window.LctxApi.apiFetch ? window.LctxApi.apiFetch : null;
}
function remFmt() {
return window.LctxFmt || {};
}
function remCharts() {
return window.LctxCharts || {};
}
function tip(k) {
return window.LctxShared && window.LctxShared.tip ? window.LctxShared.tip(k) : '';
}
function remShared() {
return window.LctxShared || {};
}
class CockpitLearning extends HTMLElement {
constructor() {
super();
this._loading = true;
this._error = null;
this._data = null;
this._onRefresh = this._onRefresh.bind(this);
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.style.display = 'block';
document.addEventListener('lctx:refresh', this._onRefresh);
this.render();
this.loadData();
}
disconnectedCallback() {
document.removeEventListener('lctx:refresh', this._onRefresh);
this._destroyCharts();
}
_onRefresh() {
var v = document.getElementById('view-learning');
if (v && v.classList.contains('active')) this.loadData();
}
_destroyCharts() {
var Ch = remCharts();
if (!Ch.destroyIfNeeded) return;
Ch.destroyIfNeeded('ckle-savings');
Ch.destroyIfNeeded('ckle-compression');
Ch.destroyIfNeeded('ckle-volume');
}
async loadData() {
var fetchJson = remApi();
if (!fetchJson) {
this._error = 'API client not loaded';
this._loading = false;
this.render();
return;
}
this._loading = true;
this._error = null;
this.render();
try {
this._data = await fetchJson('/api/stats', { timeoutMs: 10000 });
} catch (e) {
this._error = e && e.error ? e.error : String(e || 'load failed');
this._data = null;
}
this._loading = false;
this.render();
this._renderCharts();
}
render() {
var F = remFmt();
var esc = F.esc || function (s) { return String(s); };
if (this._loading) {
this.innerHTML =
'<div class="card"><div class="loading-state">Loading learning data\u2026</div></div>';
return;
}
if (this._error && !this._data) {
this.innerHTML =
'<div class="card"><h3>Error</h3>' +
'<p class="hs" style="color:var(--red)">' + esc(String(this._error)) + '</p></div>';
return;
}
this.innerHTML =
'<div class="row r3">' +
'<div class="card"><div class="card-header"><h3>Savings Growth' + tip('savings_growth') + '</h3></div>' +
'<canvas id="ckle-savings" height="200"></canvas></div>' +
'<div class="card"><div class="card-header"><h3>Compression Trend' + tip('compression_trend') + '</h3></div>' +
'<canvas id="ckle-compression" height="200"></canvas></div>' +
'<div class="card"><div class="card-header"><h3>Command Volume' + tip('command_volume') + '</h3></div>' +
'<canvas id="ckle-volume" height="200"></canvas></div>' +
'</div>';
var S = remShared();
if (S.injectExpandButtons) S.injectExpandButtons(this);
}
_renderCharts() {
var Ch = remCharts();
if (!Ch.lineChart || typeof Chart === 'undefined') return;
var data = this._data;
if (!data) return;
var daily = data.daily || [];
var labels = [];
var savings = [];
var compression = [];
var volume = [];
for (var i = 0; i < daily.length; i++) {
var d = daily[i];
var dateLabel = d.date || d.day || String(i);
if (typeof dateLabel === 'string' && dateLabel.length > 10) {
dateLabel = dateLabel.slice(5, 10);
}
labels.push(dateLabel);
var inp = Number(d.input_tokens || d.total_input || 0);
var out = Number(d.output_tokens || d.total_output || 0);
savings.push(Math.max(0, inp - out));
var rate = inp > 0 ? Math.round(((inp - out) / inp) * 100) : 0;
compression.push(rate);
volume.push(Number(d.count || d.commands || d.calls || 0));
}
if (labels.length === 0) {
this.innerHTML =
'<div class="card"><div class="empty-state">' +
'<h2>No Daily Data Yet</h2>' +
'<p>Learning curves will appear as lean-ctx records daily usage statistics.</p>' +
'</div></div>';
return;
}
requestAnimationFrame(function () {
try {
Ch.lineChart('ckle-savings', labels, savings,
'#34d399', 'rgba(52,211,153,.06)');
} catch (_) {}
try {
Ch.lineChart('ckle-compression', labels, compression,
'#818cf8', 'rgba(129,140,248,.06)');
} catch (_) {}
try {
Ch.lineChart('ckle-volume', labels, volume,
'#38bdf8', 'rgba(56,189,248,.06)');
} catch (_) {}
});
}
}
class CockpitRoutes extends HTMLElement {
constructor() {
super();
this._loading = true;
this._error = null;
this._routes = [];
this._onRefresh = this._onRefresh.bind(this);
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.style.display = 'block';
document.addEventListener('lctx:refresh', this._onRefresh);
this.render();
this.loadData();
}
disconnectedCallback() {
document.removeEventListener('lctx:refresh', this._onRefresh);
}
_onRefresh() {
var v = document.getElementById('view-routes');
if (v && v.classList.contains('active')) this.loadData();
}
async loadData() {
var fetchJson = remApi();
if (!fetchJson) {
this._error = 'API client not loaded';
this._loading = false;
this.render();
return;
}
this._loading = true;
this._error = null;
this.render();
try {
var data = await fetchJson('/api/routes', { timeoutMs: 8000 });
this._routes = (data && data.routes) || (Array.isArray(data) ? data : []);
} catch (e) {
this._error = e && e.error ? e.error : String(e || 'load failed');
this._routes = [];
}
this._loading = false;
this.render();
}
render() {
var F = remFmt();
var esc = F.esc || function (s) { return String(s); };
var ff = F.ff || function (n) { return String(n); };
if (this._loading) {
this.innerHTML =
'<div class="card"><div class="loading-state">Loading routes\u2026</div></div>';
return;
}
if (this._error && this._routes.length === 0) {
this.innerHTML =
'<div class="card"><h3>Error</h3>' +
'<p class="hs" style="color:var(--red)">' + esc(String(this._error)) + '</p></div>';
return;
}
if (this._routes.length === 0) {
this.innerHTML =
'<div class="card"><div class="empty-state">' +
'<h2>No Routes</h2>' +
'<p>API route data appears after the daemon processes requests.</p>' +
'</div></div>';
return;
}
var methodColors = {
GET: 'tg', POST: 'tp', PUT: 'ty', PATCH: 'ty',
DELETE: 'td', HEAD: 'tb', OPTIONS: 'tb',
};
var rows = '';
for (var i = 0; i < this._routes.length; i++) {
var r = this._routes[i];
var method = String(r.method || 'GET').toUpperCase();
var cls = methodColors[method] || 'tb';
var count = r.count != null ? ff(r.count) : '\u2014';
rows +=
'<tr>' +
'<td><span class="tag ' + cls + '">' + esc(method) + '</span></td>' +
'<td style="font-family:var(--mono)">' + esc(r.path || r.route || '\u2014') + '</td>' +
'<td>' + esc(r.handler || '\u2014') + '</td>' +
'<td class="r">' + esc(count) + '</td></tr>';
}
this.innerHTML =
'<div class="card">' +
'<div class="card-header"><h3>API Routes' + tip('routes_table') + '</h3>' +
'<span class="badge">' + esc(ff(this._routes.length)) + ' routes</span></div>' +
'<div class="table-scroll"><table>' +
'<thead><tr><th>Method</th><th>Path</th><th>Handler</th>' +
'<th class="r">Calls</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table></div></div>';
}
}
class CockpitContextLayer extends HTMLElement {
constructor() {
super();
this._loading = true;
this._error = null;
this._ledger = null;
this._field = null;
this._onRefresh = this._onRefresh.bind(this);
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.style.display = 'block';
document.addEventListener('lctx:refresh', this._onRefresh);
this.render();
this.loadData();
}
disconnectedCallback() {
document.removeEventListener('lctx:refresh', this._onRefresh);
}
_onRefresh() {
var v = document.getElementById('view-contextlayer');
if (v && v.classList.contains('active')) this.loadData();
}
async loadData() {
var fetchJson = remApi();
if (!fetchJson) {
this._error = 'API client not loaded';
this._loading = false;
this.render();
return;
}
this._loading = true;
this._error = null;
this.render();
var results = await Promise.all([
fetchJson('/api/context-ledger', { timeoutMs: 10000 }).catch(function (e) {
return { __error: e && e.error ? e.error : String(e || 'error') };
}),
fetchJson('/api/context-field', { timeoutMs: 10000 }).catch(function (e) {
return { __error: e && e.error ? e.error : String(e || 'error') };
}),
]);
this._ledger = results[0] && !results[0].__error ? results[0] : null;
this._field = results[1] && !results[1].__error ? results[1] : null;
if (!this._ledger && !this._field) {
this._error = 'Could not load context layer data';
}
this._loading = false;
this.render();
}
render() {
var F = remFmt();
var esc = F.esc || function (s) { return String(s); };
var ff = F.ff || function (n) { return String(n); };
if (this._loading) {
this.innerHTML =
'<div class="card"><div class="loading-state">Loading context layer\u2026</div></div>';
return;
}
if (this._error && !this._ledger && !this._field) {
this.innerHTML =
'<div class="card"><h3>Error</h3>' +
'<p class="hs" style="color:var(--red)">' + esc(String(this._error)) + '</p></div>';
return;
}
var body = '';
body += this._renderMetrics(esc, ff);
body += this._renderModeDistribution(esc, ff);
body += this._renderFileList(esc, ff);
body += this._renderFieldInfo(esc, ff);
this.innerHTML = body;
}
_renderMetrics(esc, ff) {
var ledger = this._ledger || {};
var field = this._field || {};
var entries = ledger.entries || [];
var totalSent = ledger.total_tokens_sent || 0;
var totalSaved = ledger.total_tokens_saved || 0;
var cr = typeof ledger.compression_ratio === 'number' ? ledger.compression_ratio : 1;
var savedPct = Math.max(0, Math.min(100, Math.round((1 - Math.min(1, cr)) * 100)));
return (
'<div class="hero r4 stagger" style="margin-bottom:16px">' +
'<div class="hc"><span class="hl">Active files</span>' +
'<div class="hv">' + esc(ff(entries.length)) + '</div></div>' +
'<div class="hc"><span class="hl">Tokens sent</span>' +
'<div class="hv">' + esc(ff(totalSent)) + '</div></div>' +
'<div class="hc"><span class="hl">Tokens saved</span>' +
'<div class="hv" style="color:var(--green)">' + esc(ff(totalSaved)) + '</div></div>' +
'<div class="hc"><span class="hl">Compression</span>' +
'<div class="hv">' + esc(String(savedPct)) + '%</div></div></div>'
);
}
_renderModeDistribution(esc, ff) {
var ledger = this._ledger || {};
var modeDist = ledger.mode_distribution;
if (!modeDist || typeof modeDist !== 'object') return '';
var modes = Object.keys(modeDist).sort();
if (modes.length === 0) return '';
var html = '<div class="card" style="margin-bottom:20px"><h3>View modes in use' + tip('context_layer_modes') + '</h3>';
for (var i = 0; i < modes.length; i++) {
var m = modes[i];
html +=
'<div class="sr" style="padding:6px 0">' +
'<span class="sl"><span class="tag tg">' + esc(m) + '</span></span>' +
'<span class="sv">' + esc(ff(modeDist[m])) + ' files</span></div>';
}
return html + '</div>';
}
_renderFileList(esc, ff) {
var ledger = this._ledger || {};
var entries = ledger.entries || [];
if (entries.length === 0) {
return (
'<div class="card" style="margin-bottom:20px"><h3>Active context files' + tip('context_layer_files') + '</h3>' +
'<p class="hs">No files in the context ledger yet.</p></div>'
);
}
var limit = Math.min(entries.length, 30);
var rows = '';
for (var i = 0; i < limit; i++) {
var e = entries[i];
var path = e.path || '\u2014';
var shortPath = path.length > 50 ? '\u2026' + path.slice(-48) : path;
var mode = e.mode || e.active_view || 'full';
var sent = e.sent_tokens != null ? ff(e.sent_tokens) : '\u2014';
rows +=
'<tr>' +
'<td title="' + esc(path) + '">' + esc(shortPath) + '</td>' +
'<td><span class="tag tg">' + esc(mode) + '</span></td>' +
'<td class="r">' + esc(sent) + '</td></tr>';
}
return (
'<div class="card" style="margin-bottom:16px">' +
'<div class="card-header"><h3>Active context files' + tip('context_layer_files') + '</h3>' +
'<span class="badge">' + esc(ff(entries.length)) + '</span></div>' +
'<div class="table-scroll"><table>' +
'<thead><tr><th>Path</th><th>Mode</th><th class="r">Tokens</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table></div></div>'
);
}
_renderFieldInfo(esc, ff) {
var ledger = this._ledger || {};
var field = this._field || {};
var items = field.items || [];
var temp = field.temperature != null
? Number(field.temperature).toFixed(2) : '\u2014';
return (
'<div class="card">' +
'<h3>Field info' + tip('context_layer_field') + '</h3>' +
'<div class="sr"><span class="sl">Temperature</span>' +
'<span class="sv">' + esc(temp) + '</span></div>' +
'<div class="sr"><span class="sl">Field items</span>' +
'<span class="sv">' + esc(ff(items.length)) + '</span></div>' +
'<div class="sr"><span class="sl">Window size</span>' +
'<span class="sv">' + esc(ff(ledger.window_size || 0)) + '</span></div></div>'
);
}
}
customElements.define('cockpit-learning', CockpitLearning);
customElements.define('cockpit-routes', CockpitRoutes);
customElements.define('cockpit-contextlayer', CockpitContextLayer);
(function registerRemLoaders() {
function doRegister() {
var R = window.LctxRouter;
if (!R || !R.registerLoader) return;
R.registerLoader('learning', function () {
var section = document.getElementById('view-learning');
if (!section) return;
var el = section.querySelector('cockpit-learning');
if (!el) {
section.innerHTML = '';
el = document.createElement('cockpit-learning');
el.id = 'ckle-root';
section.appendChild(el);
} else if (typeof el.loadData === 'function') {
el.loadData();
}
});
R.registerLoader('routes', function () {
var section = document.getElementById('view-routes');
if (!section) return;
var el = section.querySelector('cockpit-routes');
if (!el) {
section.innerHTML = '';
el = document.createElement('cockpit-routes');
el.id = 'ckr-root';
section.appendChild(el);
} else if (typeof el.loadData === 'function') {
el.loadData();
}
});
R.registerLoader('contextlayer', function () {
var section = document.getElementById('view-contextlayer');
if (!section) return;
var el = section.querySelector('cockpit-contextlayer');
if (!el) {
section.innerHTML = '';
el = document.createElement('cockpit-contextlayer');
el.id = 'ckcl-root';
section.appendChild(el);
} else if (typeof el.loadData === 'function') {
el.loadData();
}
});
}
if (window.LctxRouter && window.LctxRouter.registerLoader) doRegister();
else document.addEventListener('DOMContentLoaded', doRegister);
})();
export { CockpitLearning, CockpitRoutes, CockpitContextLayer };