var CATEGORY_COLORS = {
ARCHITECTURE: '#38bdf8',
TESTING: '#34d399',
DEBUGGING: '#f87171',
WORKFLOW: '#818cf8',
DEPLOYMENT: '#f472b6',
PERFORMANCE: '#fbbf24',
E2E: '#34d399',
SECURITY: '#f87171',
API: '#60a5fa',
DATABASE: '#c084fc',
};
var EDGE_STYLES = {
category: { color: 'var(--border)', dash: null },
depends_on: { color: 'rgba(56,189,248,0.55)', dash: null },
related_to: { color: 'var(--border-light)', dash: null },
supports: { color: 'rgba(52,211,153,0.55)', dash: null },
contradicts: { color: 'rgba(248,113,113,0.6)', dash: null },
supersedes: { color: 'rgba(192,132,252,0.6)', dash: '6,3' },
};
var DEFAULT_NODE_COLOR = '#6b6b88';
var LABEL_MAX = 22;
function api() {
return window.LctxApi && window.LctxApi.apiFetch ? window.LctxApi.apiFetch : null;
}
function fmtLib() {
return window.LctxFmt || {};
}
function shared() {
return window.LctxShared || {};
}
function catColor(cat) {
var upper = String(cat || '').toUpperCase();
return CATEGORY_COLORS[upper] || DEFAULT_NODE_COLOR;
}
function truncLabel(s) {
if (!s) return '';
return s.length > LABEL_MAX ? s.slice(0, LABEL_MAX - 1) + '\u2026' : s;
}
function tip(k) {
return window.LctxShared && window.LctxShared.tip ? window.LctxShared.tip(k) : '';
}
function edgeStyle(kind) {
return EDGE_STYLES[kind] || EDGE_STYLES.related_to;
}
class CockpitKnowledge extends HTMLElement {
constructor() {
super();
this._simulation = null;
this._zoom = null;
this._showValues = false;
this._isFullscreen = false;
this._minimapTimer = null;
this._data = null;
this._relations = null;
this._error = null;
this._loading = true;
this._onRefresh = this._onRefresh.bind(this);
this._onViewChange = this._onViewChange.bind(this);
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.style.display = 'block';
document.addEventListener('lctx:refresh', this._onRefresh);
document.addEventListener('lctx:view', this._onViewChange);
this.render();
this.loadData();
}
disconnectedCallback() {
document.removeEventListener('lctx:refresh', this._onRefresh);
document.removeEventListener('lctx:view', this._onViewChange);
this._destroySimulation();
}
_onViewChange(e) {
var viewId = e && e.detail && e.detail.viewId;
if (viewId === 'knowledge') {
if (this._simulation) this._simulation.alpha(0.1).restart();
if (!this._minimapTimer) this._startMinimap();
} else {
if (this._simulation) this._simulation.stop();
if (this._minimapTimer) { clearInterval(this._minimapTimer); this._minimapTimer = null; }
}
}
_onRefresh() {
var v = document.getElementById('view-knowledge');
if (v && v.classList.contains('active')) this.loadData();
}
_destroySimulation() {
if (this._simulation) {
this._simulation.stop();
this._simulation = null;
}
if (this._minimapTimer) {
clearInterval(this._minimapTimer);
this._minimapTimer = null;
}
}
async loadData() {
var fetchJson = api();
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/knowledge', { timeoutMs: 12000 }).catch(function (e) {
return { __error: e && e.error ? e.error : String(e || 'error') };
}),
fetchJson('/api/knowledge-relations', { timeoutMs: 12000 }).catch(function (e) {
return { __error: e && e.error ? e.error : String(e || 'error') };
}),
]);
var knowledge = results[0];
var relations = results[1];
if (knowledge && knowledge.__error) {
this._error = String(knowledge.__error);
}
this._data = knowledge && !knowledge.__error ? knowledge : null;
this._relations = relations && !relations.__error ? relations : null;
this._loading = false;
this.render();
this._buildGraph();
}
render() {
var F = fmtLib();
var esc = F.esc || function (s) { return String(s); };
var fmt = F.fmt || function (n) { return String(n); };
var S = shared();
if (this._loading) {
if (S.showLoading) {
S.showLoading(this);
} else {
this.innerHTML = '<div class="card"><div class="loading-state">Loading knowledge graph\u2026</div></div>';
}
return;
}
if (this._error && !this._data) {
if (S.showError) {
S.showError(this, this._error);
} else {
this.innerHTML =
'<div class="card"><h3>Error</h3>' +
'<p class="hs" style="color:var(--red)">' + esc(this._error) + '</p></div>';
}
return;
}
var facts = this._currentFacts();
if (facts.length === 0) {
if (S.showGuidedEmpty) {
S.showGuidedEmpty(
this,
'No knowledge facts yet',
'The knowledge graph populates as lean-ctx learns about your project.',
[
'Run lean-ctx in a project to auto-discover patterns',
'Use lean-ctx knowledge add <category> <key> <value> to add facts manually',
'Facts bootstrap from project index data on first load',
]
);
} else {
this.innerHTML =
'<div class="empty-state"><h2>No knowledge facts yet</h2>' +
'<p>The knowledge graph populates as lean-ctx learns about your project.</p></div>';
}
return;
}
var body = '';
body += this._renderMetrics(facts, esc, fmt);
body += this._renderGraphContainer(facts, esc);
body += this._renderHowItWorks();
this.innerHTML = body;
S.bindHowItWorks && S.bindHowItWorks(this);
}
_currentFacts() {
if (!this._data || !this._data.facts) return [];
return this._data.facts.filter(function (f) {
if (f.valid_until) return false;
return true;
});
}
_renderMetrics(facts, esc, fmt) {
var categories = {};
var totalConf = 0;
var highConf = 0;
for (var i = 0; i < facts.length; i++) {
var f = facts[i];
categories[f.category] = true;
var c = typeof f.confidence === 'number' ? f.confidence : 0;
totalConf += c;
if (c >= 0.8) highConf++;
}
var catCount = Object.keys(categories).length;
var avgConf = facts.length > 0 ? Math.round((totalConf / facts.length) * 100) : 0;
return (
'<div class="hero r4 stagger">' +
'<div class="hc"><span class="hl">Total Facts</span><div class="hv">' +
esc(fmt(facts.length)) + '</div></div>' +
'<div class="hc"><span class="hl">Categories</span><div class="hv">' +
esc(fmt(catCount)) + '</div></div>' +
'<div class="hc"><span class="hl">Avg Confidence</span><div class="hv">' +
esc(String(avgConf)) + '%</div></div>' +
'<div class="hc"><span class="hl">High Confidence</span><div class="hv">' +
esc(fmt(highConf)) + '</div></div>' +
'</div>'
);
}
_renderGraphContainer(facts, esc) {
var edges = this._relations && this._relations.edges ? this._relations.edges : [];
var categories = {};
for (var i = 0; i < facts.length; i++) {
categories[facts[i].category] = true;
}
var catKeys = Object.keys(categories).sort();
var legend = catKeys.map(function (cat) {
return (
'<div class="graph-legend-item">' +
'<div class="graph-legend-dot" style="background:' + catColor(cat) + ';color:' + catColor(cat) + '"></div>' +
esc(cat) +
'</div>'
);
}).join('');
var statsHtml =
'<span>' + facts.length + '</span> facts ' +
'<span>' + edges.length + '</span> relations ' +
'<span>' + catKeys.length + '</span> categories';
return (
'<div class="d3-container" id="kgContainer">' +
'<div class="graph-stats">' + statsHtml + '</div>' +
'<div class="graph-toolbar" id="kgToolbar">' +
'<button type="button" data-act="toggle-values" title="Toggle values">%</button>' +
'<div class="tb-sep"></div>' +
'<button type="button" data-act="zoom-in" title="Zoom in">+</button>' +
'<button type="button" data-act="zoom-out" title="Zoom out">\u2212</button>' +
'<button type="button" data-act="reset" title="Reset view">\u21BA</button>' +
'<div class="tb-sep"></div>' +
'<button type="button" data-act="fullscreen" title="Fullscreen">\u26F6</button>' +
'</div>' +
'<div class="graph-legend">' + legend + '</div>' +
'<div class="graph-breadcrumb" id="kgBreadcrumb">Knowledge Graph \u2014 Fullscreen</div>' +
'<div class="graph-minimap" id="kgMinimap"><canvas id="kgMinimapCanvas" width="320" height="200"></canvas><div class="graph-minimap-viewport" id="kgMinimapViewport"></div></div>' +
'<svg id="kgSvg"></svg>' +
'</div>'
);
}
_renderHowItWorks() {
var S = shared();
if (!S.howItWorks) return '';
return S.howItWorks(
'Knowledge Graph',
'<p style="font-size:12px;color:var(--muted);line-height:1.6">' +
'The knowledge graph visualizes facts lean-ctx has learned about your project. ' +
'Each node represents a knowledge fact, grouped by category. ' +
'Links show relationships: dependencies, support, contradictions, and superseded facts. ' +
'Node size reflects confidence level. Click any node for details.</p>'
);
}
_buildGraph() {
if (typeof d3 === 'undefined') return;
this._destroySimulation();
var facts = this._currentFacts();
if (facts.length === 0) return;
var container = this.querySelector('#kgContainer');
var svgEl = this.querySelector('#kgSvg');
if (!container || !svgEl) return;
var width = container.clientWidth || 900;
var height = container.clientHeight || 600;
var nodes = [];
var links = [];
var nodeMap = {};
var catNodes = {};
for (var i = 0; i < facts.length; i++) {
var f = facts[i];
var id = f.category + '/' + f.key;
var conf = typeof f.confidence === 'number' ? f.confidence : 0.5;
var node = {
id: id,
label: f.key,
category: f.category,
confidence: conf,
radius: 4 + conf * 10,
type: 'fact',
fact: f,
factIndex: i,
};
nodes.push(node);
nodeMap[id] = node;
if (!catNodes[f.category]) {
var catNode = {
id: '__cat__' + f.category,
label: f.category,
category: f.category,
confidence: 1,
radius: 18,
type: 'category',
};
catNodes[f.category] = catNode;
nodes.push(catNode);
nodeMap[catNode.id] = catNode;
}
links.push({
source: catNodes[f.category].id,
target: id,
kind: 'category',
});
}
var edges = this._relations && this._relations.edges ? this._relations.edges : [];
for (var j = 0; j < edges.length; j++) {
var e = edges[j];
var from = e.from || e.source || '';
var to = e.to || e.target || '';
if (nodeMap[from] && nodeMap[to]) {
links.push({ source: from, target: to, kind: e.kind || 'related_to' });
}
}
var svg = d3.select(svgEl)
.attr('width', width)
.attr('height', height)
.attr('viewBox', '0 0 ' + width + ' ' + height);
svg.selectAll('*').remove();
var defs = svg.append('defs');
var catList = Object.keys(catNodes);
for (var ci = 0; ci < catList.length; ci++) {
var cName = catList[ci];
var col = catColor(cName);
var grad = defs.append('radialGradient')
.attr('id', 'glow-' + cName.replace(/[^a-zA-Z0-9]/g, ''))
.attr('cx', '50%').attr('cy', '50%').attr('r', '50%');
grad.append('stop').attr('offset', '0%').attr('stop-color', col).attr('stop-opacity', 0.35);
grad.append('stop').attr('offset', '100%').attr('stop-color', col).attr('stop-opacity', 0);
}
var g = svg.append('g').attr('class', 'kg-root');
var self = this;
var zoom = d3.zoom()
.scaleExtent([0.15, 5])
.on('zoom', function (event) {
g.attr('transform', event.transform);
self._updateMinimap(event.transform, width, height);
});
svg.call(zoom);
this._zoom = zoom;
this._svg = svg;
this._gRoot = g;
this._graphWidth = width;
this._graphHeight = height;
var linkG = g.append('g').attr('class', 'kg-links');
var linkEls = linkG.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', function (d) { return edgeStyle(d.kind).color; })
.attr('stroke-width', function (d) { return d.kind === 'category' ? 0.5 : 1.2; })
.attr('stroke-dasharray', function (d) { return edgeStyle(d.kind).dash; });
var glowG = g.append('g').attr('class', 'kg-glows');
glowG.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', function (d) { return d.radius * 2.5; })
.attr('fill', function (d) {
return 'url(#glow-' + String(d.category).replace(/[^a-zA-Z0-9]/g, '') + ')';
})
.attr('pointer-events', 'none');
var nodeG = g.append('g').attr('class', 'kg-nodes');
var nodeEls = nodeG.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', function (d) { return d.radius; })
.attr('fill', function (d) { return catColor(d.category); })
.attr('stroke', function (d) { return d.type === 'category' ? 'var(--border-light)' : 'var(--border)'; })
.attr('stroke-width', function (d) { return d.type === 'category' ? 1.5 : 0.5; })
.attr('cursor', 'pointer')
.on('mouseover', function (event, d) {
var S = shared();
var html = self._tooltipHtml(d);
if (S.showTooltip) S.showTooltip(event, html);
})
.on('mousemove', function (event) {
var S = shared();
if (S.moveTooltip) S.moveTooltip(event);
})
.on('mouseout', function () {
var S = shared();
if (S.hideTooltip) S.hideTooltip();
})
.on('click', function (event, d) {
self._onNodeClick(d);
});
var drag = d3.drag()
.on('start', function (event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function (event, d) {
d.fx = event.x;
d.fy = event.y;
})
.on('end', function (event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
});
nodeEls.call(drag);
var labelG = g.append('g').attr('class', 'kg-labels');
var labelEls = labelG.selectAll('text')
.data(nodes)
.enter()
.append('text')
.attr('class', 'kg-node-val')
.attr('text-anchor', 'middle')
.attr('dy', function (d) { return d.radius + 12; })
.attr('font-size', function (d) { return d.type === 'category' ? 11 : 9; })
.text(function (d) { return truncLabel(d.label); });
var valLabelG = g.append('g').attr('class', 'kg-val-labels');
var valLabelEls = valLabelG.selectAll('text')
.data(nodes.filter(function (d) { return d.type === 'fact'; }))
.enter()
.append('text')
.attr('class', 'kg-node-val')
.attr('text-anchor', 'middle')
.attr('dy', -3)
.attr('font-size', 8)
.attr('opacity', 0)
.text(function (d) { return Math.round(d.confidence * 100) + '%'; });
this._valLabels = valLabelEls;
var simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(function (d) { return d.id; }).distance(function (d) {
return d.kind === 'category' ? 50 : 80;
}).strength(function (d) {
return d.kind === 'category' ? 0.7 : 0.3;
}))
.force('charge', d3.forceManyBody().strength(function (d) {
return d.type === 'category' ? -250 : -60;
}))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(function (d) { return d.radius + 4; }))
.on('tick', function () {
linkEls
.attr('x1', function (d) { return d.source.x; })
.attr('y1', function (d) { return d.source.y; })
.attr('x2', function (d) { return d.target.x; })
.attr('y2', function (d) { return d.target.y; });
glowG.selectAll('circle')
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
nodeEls
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
labelEls
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; });
valLabelEls
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; });
});
this._simulation = simulation;
this._nodes = nodes;
this._bindToolbar();
this._startMinimap();
}
_tooltipHtml(d) {
var F = fmtLib();
var esc = F.esc || function (s) { return String(s); };
if (d.type === 'category') {
return (
'<div class="nt-title">' + esc(d.label) + '</div>' +
'<div class="nt-row"><span class="nt-label">Type</span><span class="nt-value">Category</span></div>'
);
}
var f = d.fact || {};
var conf = typeof f.confidence === 'number' ? Math.round(f.confidence * 100) + '%' : '\u2014';
var src = f.source_session || f.source || '\u2014';
return (
'<div class="nt-title">' + esc(d.label) + '</div>' +
'<div class="nt-row"><span class="nt-label">Category</span><span class="nt-value">' + esc(f.category || '') + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Confidence</span><span class="nt-value">' + esc(conf) + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Source</span><span class="nt-value">' + esc(src) + '</span></div>'
);
}
_onNodeClick(d) {
if (typeof window.showDetail !== 'function') return;
var F = fmtLib();
var esc = F.esc || function (s) { return String(s); };
if (d.type === 'category') {
var facts = this._currentFacts().filter(function (f) {
return f.category === d.label;
});
var rows = facts.map(function (f, idx) {
var conf = typeof f.confidence === 'number' ? Math.round(f.confidence * 100) + '%' : '\u2014';
return (
'<div class="nt-row" style="padding:6px 0;border-bottom:1px solid var(--border)">' +
'<span class="nt-label" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis">' + esc(f.key) + '</span>' +
'<span class="nt-value">' + esc(conf) + '</span>' +
'</div>'
);
}).join('');
window.showDetail(
d.label + ' (' + facts.length + ' facts)',
'<div style="font-size:12px">' + rows + '</div>'
);
return;
}
var f = d.fact || {};
var conf = typeof f.confidence === 'number' ? Math.round(f.confidence * 100) + '%' : '\u2014';
var learnedAt = f.created_at
? esc(String(f.created_at).replace('T', ' ').slice(0, 19))
: '\u2014';
var lastConf = f.last_confirmed
? esc(String(f.last_confirmed).replace('T', ' ').slice(0, 19))
: '\u2014';
var src = f.source_session || f.source || '\u2014';
var value = f.value || f.fact || '\u2014';
var html =
'<div style="font-size:12px;line-height:1.8">' +
'<div class="nt-row"><span class="nt-label">Category</span><span class="nt-value">' + esc(f.category || '') + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Key</span><span class="nt-value">' + esc(f.key || '') + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Confidence</span><span class="nt-value">' + esc(conf) + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Learned</span><span class="nt-value">' + learnedAt + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Last confirmed</span><span class="nt-value">' + lastConf + '</span></div>' +
'<div class="nt-row"><span class="nt-label">Source</span><span class="nt-value">' + esc(src) + '</span></div>' +
(f.supersedes
? '<div class="nt-row"><span class="nt-label">Supersedes</span><span class="nt-value">' + esc(f.supersedes) + '</span></div>'
: '') +
'<div style="margin-top:12px;padding:10px;background:var(--surface-2);border-radius:8px;font-family:var(--mono);font-size:11px;word-break:break-word;color:var(--text)">' +
esc(value) +
'</div>' +
'</div>';
window.showDetail(f.key || d.label, html);
}
_bindToolbar() {
var self = this;
var toolbar = this.querySelector('#kgToolbar');
if (!toolbar) return;
toolbar.querySelectorAll('button[data-act]').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var act = btn.getAttribute('data-act');
if (act === 'toggle-values') self._toggleValues(btn);
else if (act === 'zoom-in') self._zoomStep(1.4);
else if (act === 'zoom-out') self._zoomStep(1 / 1.4);
else if (act === 'reset') self._zoomReset();
else if (act === 'fullscreen') self._toggleFullscreen();
});
});
}
_toggleValues(btn) {
this._showValues = !this._showValues;
if (btn) btn.classList.toggle('active', this._showValues);
if (this._valLabels) {
this._valLabels.attr('opacity', this._showValues ? 0.85 : 0);
}
}
_zoomStep(factor) {
if (!this._svg || !this._zoom) return;
this._svg.transition().duration(300).call(
this._zoom.scaleBy, factor
);
}
_zoomReset() {
if (!this._svg || !this._zoom) return;
this._svg.transition().duration(500).call(
this._zoom.transform, d3.zoomIdentity
);
}
_toggleFullscreen() {
var container = this.querySelector('#kgContainer');
if (!container) return;
this._isFullscreen = !this._isFullscreen;
container.classList.toggle('graph-fullscreen', this._isFullscreen);
if (this._isFullscreen) {
var w = window.innerWidth;
var h = window.innerHeight;
var svgEl = container.querySelector('svg');
if (svgEl) {
svgEl.setAttribute('width', w);
svgEl.setAttribute('height', h);
svgEl.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
}
this._graphWidth = w;
this._graphHeight = h;
if (this._simulation) {
this._simulation.force('center', d3.forceCenter(w / 2, h / 2));
this._simulation.alpha(0.3).restart();
}
} else {
var cw = container.clientWidth || 900;
var ch = container.clientHeight || 600;
var svgEl2 = container.querySelector('svg');
if (svgEl2) {
svgEl2.setAttribute('width', cw);
svgEl2.setAttribute('height', ch);
svgEl2.setAttribute('viewBox', '0 0 ' + cw + ' ' + ch);
}
this._graphWidth = cw;
this._graphHeight = ch;
if (this._simulation) {
this._simulation.force('center', d3.forceCenter(cw / 2, ch / 2));
this._simulation.alpha(0.3).restart();
}
}
}
_startMinimap() {
var self = this;
if (this._minimapTimer) clearInterval(this._minimapTimer);
this._minimapTimer = setInterval(function () {
self._drawMinimap();
}, 500);
}
_drawMinimap() {
var canvas = this.querySelector('#kgMinimapCanvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
if (!ctx) return;
var nodes = this._nodes;
if (!nodes || nodes.length === 0) return;
var cw = canvas.width;
var ch = canvas.height;
ctx.clearRect(0, 0, cw, ch);
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (n.x == null || n.y == null) continue;
if (n.x < minX) minX = n.x;
if (n.x > maxX) maxX = n.x;
if (n.y < minY) minY = n.y;
if (n.y > maxY) maxY = n.y;
}
var pad = 30;
var rangeX = (maxX - minX) || 1;
var rangeY = (maxY - minY) || 1;
var scaleX = (cw - pad * 2) / rangeX;
var scaleY = (ch - pad * 2) / rangeY;
var scale = Math.min(scaleX, scaleY);
for (var j = 0; j < nodes.length; j++) {
var nd = nodes[j];
if (nd.x == null || nd.y == null) continue;
var mx = pad + (nd.x - minX) * scale;
var my = pad + (nd.y - minY) * scale;
var mr = nd.type === 'category' ? 3 : 1.5;
ctx.beginPath();
ctx.arc(mx, my, mr, 0, Math.PI * 2);
ctx.fillStyle = catColor(nd.category);
ctx.globalAlpha = nd.type === 'category' ? 0.9 : 0.6;
ctx.fill();
}
ctx.globalAlpha = 1;
}
_updateMinimap(transform, gw, gh) {
var vp = this.querySelector('#kgMinimapViewport');
if (!vp) return;
var canvas = this.querySelector('#kgMinimapCanvas');
if (!canvas) return;
var cw = canvas.clientWidth || 160;
var ch = canvas.clientHeight || 100;
var scaleX = cw / gw;
var scaleY = ch / gh;
var k = transform.k || 1;
var vpW = Math.min(cw, cw / k);
var vpH = Math.min(ch, ch / k);
var vpX = -(transform.x || 0) * scaleX / k;
var vpY = -(transform.y || 0) * scaleY / k;
vp.style.width = Math.max(8, vpW) + 'px';
vp.style.height = Math.max(8, vpH) + 'px';
vp.style.left = Math.max(0, vpX) + 'px';
vp.style.top = Math.max(0, vpY) + 'px';
}
}
customElements.define('cockpit-knowledge', CockpitKnowledge);
if (window.LctxRouter && window.LctxRouter.registerLoader) {
window.LctxRouter.registerLoader('knowledge', function () {
var el = document.querySelector('cockpit-knowledge');
if (el && typeof el.loadData === 'function') return el.loadData();
});
}
export { CockpitKnowledge };