Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

441 lines
17 KiB
JavaScript

// Semantic-map view — a meaning-space scatter of the graph.
//
// Each node is pinned at the 2-D projection of its embedding
// (window._semanticPositions, from the layout's __SEMANTIC_POSITIONS__ token),
// shaded by cluster (__SEMANTIC_CLUSTERS__). Positions are computed once in
// Python and never simulated here (layout-once rule) except under the explicit
// Structural toggle. Hovering a node lights up its precomputed nearest
// neighbors and lists its graph relations. Node detail is read from
// window._vizNodeById / window._vizLinks (exposed by story_view.js).
(function () {
'use strict';
const CLUSTERS = __SEMANTIC_CLUSTERS__;
// Recall events from the session layer (same token the Memory tab uses). Each
// 'search' event carries the node_ids a recall query retrieved — the overlay
// lights up where that query landed in meaning-space.
const SEARCH_EVENTS = __SEARCH_EVENTS__;
function nodeById(id) {
return (window._vizNodeById && window._vizNodeById[id]) || null;
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── Pure view decisions (no DOM, no d3) ──────────────────────────────────
// Linear map with no clamping — matches d3.scaleLinear's default extrapolation.
function mapLinear(v, d0, d1, r0, r1) {
return r0 + ((v - d0) / (d1 - d0)) * (r1 - r0);
}
// Pinned embedding coords ([-1.2, 1.2] box) -> screen pixels. y is inverted.
function screenPositions(positions, width, height, pad) {
const out = {};
Object.keys(positions).forEach((id) => {
out[id] = {
x: mapLinear(positions[id].x, -1.2, 1.2, pad, width - pad),
y: mapLinear(positions[id].y, -1.2, 1.2, height - pad, pad),
};
});
return out;
}
function isolationOpacity(id, ctx) {
if (state.colorBy === 'type') {
if (state.isolatedType == null) return 1;
return ctx.typeById[id] === state.isolatedType ? 1 : 0.12;
}
return state.isolatedCluster == null || ctx.nodeCluster[id] === state.isolatedCluster
? 1
: 0.12;
}
// Per-node visual attributes. The recall overlay (state.recall is a Set) wins:
// retrieved nodes get a red ring and full opacity, everything else dims. With
// no overlay, the legend isolation applies.
function styleFor(id, ctx) {
const hit = !!(state.recall && state.recall.has(id));
return {
opacity: state.recall ? (hit ? 1 : 0.06) : isolationOpacity(id, ctx),
stroke: hit ? '#ff3b3b' : 'rgba(0,0,0,0.25)',
strokeWidth: hit ? 2.5 : 0.5,
r: hit ? 7 : 5,
};
}
function fillFor(id, ctx) {
if (state.colorBy === 'type') return ctx.colorById[id] || '#8a8a8a';
const cid = ctx.nodeCluster[id];
return cid == null ? '#8a8a8a' : ctx.clusterColors[cid];
}
function truncate(s, n) {
return s.length > n ? s.slice(0, n - 1) + '…' : s;
}
// On-screen centroid + display label per non-empty cluster.
function clusterCentroids(clusters, screenPos) {
const out = [];
clusters.forEach((c) => {
const pts = (c.node_ids || []).map((id) => screenPos[id]).filter(Boolean);
if (!pts.length) return;
let sx = 0;
let sy = 0;
pts.forEach((p) => {
sx += p.x;
sy += p.y;
});
out.push({ id: c.id, x: sx / pts.length, y: sy / pts.length, label: truncate(c.label, 34) });
});
return out;
}
// Legend rows follow the color mode. Each row carries what the click handler
// needs: kind ('type'|'cluster') + value (the type name or cluster id).
function legendModel(clusters, ids, ctx) {
if (state.colorBy === 'type') {
const typeColor = {};
ids.forEach((id) => {
const t = ctx.typeById[id];
if (t && !(t in typeColor)) typeColor[t] = ctx.colorById[id] || '#8a8a8a';
});
return Object.keys(typeColor)
.sort()
.map((t) => ({ kind: 'type', value: t, color: typeColor[t], text: t }));
}
return clusters.map((c) => ({
kind: 'cluster',
value: c.id,
color: ctx.clusterColors[c.id],
text: c.label,
}));
}
function recallQueries(events) {
return (events || []).filter((e) => e && e.kind === 'search' && (e.node_ids || []).length);
}
function recallOnMap(query, positions) {
return (query.node_ids || []).filter((id) => positions[String(id)]).length;
}
// ── View state + d3/DOM mechanics ────────────────────────────────────────
// Single mutable view state; the pure helpers read it, never mutate it.
const state = {
colorBy: 'cluster',
layoutMode: 'semantic',
isolatedCluster: null,
isolatedType: null,
recall: null, // Set of node_ids from the active recall overlay, or null
};
let zoomBehavior = null;
// Retained render artifacts so overlay/isolation changes restyle in place
// (no full re-render, no force-sim re-run) instead of via a back-pointer.
let currentCircles = null;
let currentCtx = null;
// Structural layout: relax the same nodes over graph topology (window._vizLinks)
// with a bounded, synchronous force sim. This is the one place a sim is allowed —
// it explicitly leaves the pinned semantic view. Bounded by the capped node
// payload and a fixed tick count, seeded from the semantic screen positions.
function structuralPositions(ids, semanticScreen, width, height) {
const idSet = new Set(ids);
const nodes = ids.map((id) => ({ id, x: semanticScreen[id].x, y: semanticScreen[id].y }));
const links = (window._vizLinks || [])
.filter((l) => idSet.has(String(l.source)) && idSet.has(String(l.target)))
.map((l) => ({ source: String(l.source), target: String(l.target) }));
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id((d) => d.id).distance(40))
.force('charge', d3.forceManyBody().strength(-30))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(8))
.stop();
for (let i = 0; i < 200; i++) sim.tick();
const out = {};
nodes.forEach((n) => { out[n.id] = { x: n.x, y: n.y }; });
return out;
}
function clusterColor(clusters) {
// Deterministic palette indexed by cluster id.
const map = {};
const n = Math.max(1, clusters.length);
clusters.forEach((c, i) => {
map[c.id] = d3.interpolateTurbo((i + 0.5) / n);
});
return map;
}
function relationsFor(id) {
const links = window._vizLinks || [];
const out = [];
for (const l of links) {
const s = String(l.source), t = String(l.target);
if (s === id) out.push({ other: t, rel: l.relation || l.label || 'related' });
else if (t === id) out.push({ other: s, rel: l.relation || l.label || 'related' });
if (out.length >= 8) break;
}
return out;
}
function showPanel(id, clusterColors, nodeCluster) {
const panel = document.getElementById('semantic-panel');
if (!panel) return;
const nd = nodeById(id) || {};
const cid = nodeCluster[id];
const cluster = (CLUSTERS.clusters || []).find((c) => c.id === cid);
const neighbors = (CLUSTERS.neighbors && CLUSTERS.neighbors[id]) || [];
const rels = relationsFor(id);
let html = '<div style="font-size:15px;font-weight:700;margin-bottom:6px;">' + esc(nd.name || id) + '</div>';
html += '<div style="font-size:12px;color:var(--text2);margin-bottom:12px;">' + esc(nd.type || 'node') + '</div>';
if (cluster) {
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">'
+ '<span style="width:11px;height:11px;border-radius:50%;background:' + clusterColors[cid] + ';display:inline-block;"></span>'
+ '<span style="font-size:12.5px;">' + esc(cluster.label) + '</span></div>';
}
if (neighbors.length) {
html += '<div style="font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:var(--text2);margin-bottom:6px;">Nearest in meaning</div>';
html += '<div style="margin-bottom:14px;">';
neighbors.forEach((nid) => {
const nn = nodeById(nid) || {};
html += '<div style="font-size:12.5px;padding:2px 0;">' + esc(nn.name || nid) + '</div>';
});
html += '</div>';
}
if (rels.length) {
html += '<div style="font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:var(--text2);margin-bottom:6px;">Relations</div>';
rels.forEach((r) => {
const on = nodeById(r.other) || {};
html += '<div style="font-size:12px;padding:2px 0;color:var(--text2);">'
+ esc(r.rel) + ' &rarr; <span style="color:var(--text);">' + esc(on.name || r.other) + '</span></div>';
});
}
panel.innerHTML = html;
panel.style.display = 'block';
}
// Restyle the retained circles from current state. A no-op before the first
// render (currentCircles is null); render() calls it at the end, so a state
// change made while hidden still lands on next render.
function repaint() {
if (!currentCircles) return;
currentCircles.each(function (id) {
const s = styleFor(id, currentCtx);
d3.select(this)
.attr('opacity', s.opacity)
.attr('stroke', s.stroke)
.attr('stroke-width', s.strokeWidth)
.attr('r', s.r);
});
}
function render(preserve) {
const svgEl = document.getElementById('semantic-svg');
const empty = document.getElementById('semantic-empty');
const positions = window._semanticPositions || null;
const ids = positions ? Object.keys(positions) : [];
if (!positions || ids.length === 0 || !CLUSTERS) {
if (empty) empty.style.display = 'flex';
if (svgEl) svgEl.style.display = 'none';
return;
}
if (empty) empty.style.display = 'none';
if (svgEl) svgEl.style.display = 'block';
const svg = d3.select(svgEl);
const width = svgEl.clientWidth || 800;
const height = svgEl.clientHeight || 600;
const pad = 60;
const clusters = CLUSTERS.clusters || [];
const nodeCluster = CLUSTERS.node_cluster || {};
const clusterColors = clusterColor(clusters);
// Per-id maps precomputed once per render; the pure helpers read these.
const typeById = {};
const colorById = {};
ids.forEach((id) => {
const nd = nodeById(id) || {};
typeById[id] = nd.type;
colorById[id] = nd.color;
});
const ctx = { nodeCluster, typeById, colorById, clusterColors };
const semanticScreen = screenPositions(positions, width, height, pad);
const screenPos =
state.layoutMode === 'structural'
? structuralPositions(ids, semanticScreen, width, height)
: semanticScreen;
const prevTransform = preserve ? d3.zoomTransform(svgEl) : d3.zoomIdentity;
svg.selectAll('*').remove();
const g = svg.append('g');
// Cluster labels at the on-screen centroid of each cluster's members.
clusterCentroids(clusters, screenPos).forEach((c) => {
g.append('text')
.attr('x', c.x).attr('y', c.y)
.attr('text-anchor', 'middle')
.attr('fill', clusterColors[c.id])
.attr('font-size', 13).attr('font-weight', 700)
.attr('opacity', 0.85)
.attr('pointer-events', 'none')
.text(c.label);
});
const circles = g.selectAll('circle').data(ids, (d) => d).enter().append('circle')
.attr('cx', (id) => screenPos[id].x)
.attr('cy', (id) => screenPos[id].y)
.attr('r', 5)
.attr('fill', (id) => fillFor(id, ctx))
.attr('stroke', 'rgba(0,0,0,0.25)').attr('stroke-width', 0.5)
.style('cursor', 'pointer');
currentCircles = circles;
currentCtx = ctx;
circles.on('mouseover', function (event, id) {
const nbrs = new Set([(id), ...((CLUSTERS.neighbors && CLUSTERS.neighbors[id]) || [])]);
circles.attr('opacity', (d) => (nbrs.has(d) ? 1 : 0.15));
d3.select(this).attr('r', 8);
showPanel(id, clusterColors, nodeCluster);
}).on('mouseout', function () {
repaint();
const panel = document.getElementById('semantic-panel');
if (panel) panel.style.display = 'none';
});
zoomBehavior = d3.zoom().scaleExtent([0.2, 12]).on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoomBehavior);
svg.call(zoomBehavior.transform, prevTransform);
repaint();
renderLegend(clusters, ids, ctx);
const status = document.getElementById('semantic-status');
if (status) {
status.textContent = ids.length + ' nodes · ' + clusters.length + ' clusters';
}
}
function legendRow(color, text, onClick) {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:8px;cursor:pointer;padding:1px 0;';
row.innerHTML = '<span style="width:10px;height:10px;border-radius:50%;background:'
+ color + ';display:inline-block;flex:0 0 auto;"></span>'
+ '<span>' + esc(truncate(text, 30)) + '</span>';
row.addEventListener('click', onClick);
return row;
}
function renderLegend(clusters, ids, ctx) {
const legend = document.getElementById('semantic-legend');
if (!legend) return;
legend.innerHTML = '';
// Rows follow the color mode (cluster vs type); clicking one isolates it.
legendModel(clusters, ids, ctx).forEach((rowModel) => {
legend.appendChild(legendRow(rowModel.color, rowModel.text, () => {
if (rowModel.kind === 'type') {
state.isolatedType = state.isolatedType === rowModel.value ? null : rowModel.value;
} else {
state.isolatedCluster = state.isolatedCluster === rowModel.value ? null : rowModel.value;
}
repaint();
}));
});
}
window._renderSemanticView = function (preserve) {
render(preserve);
};
function setActive(group, btn) {
document.querySelectorAll(group).forEach((b) => {
b.classList.remove('active');
b.style.background = 'var(--surface)'; b.style.color = 'var(--text2)'; b.style.border = '1px solid var(--border)';
});
btn.classList.add('active');
btn.style.background = 'var(--accent)'; btn.style.color = '#fff'; btn.style.border = 'none';
}
// Color-mode toggle (Cluster / Type).
document.querySelectorAll('.sem-color-btn').forEach((btn) => {
btn.addEventListener('click', () => {
setActive('.sem-color-btn', btn);
state.colorBy = btn.dataset.colorby;
state.isolatedCluster = null; state.isolatedType = null; // isolation is per-mode
render(true);
});
});
// Layout toggle (Semantic / Structural).
document.querySelectorAll('.sem-layout-btn').forEach((btn) => {
btn.addEventListener('click', () => {
setActive('.sem-layout-btn', btn);
state.layoutMode = btn.dataset.layout;
render(true);
});
});
// Zoom controls.
function zoomBy(factor) {
const svgEl = document.getElementById('semantic-svg');
if (svgEl && zoomBehavior) d3.select(svgEl).transition().duration(150).call(zoomBehavior.scaleBy, factor);
}
const zi = document.getElementById('semantic-zoom-in');
const zo = document.getElementById('semantic-zoom-out');
const zf = document.getElementById('semantic-zoom-fit');
if (zi) zi.addEventListener('click', () => zoomBy(1.4));
if (zo) zo.addEventListener('click', () => zoomBy(1 / 1.4));
if (zf) zf.addEventListener('click', () => {
const svgEl = document.getElementById('semantic-svg');
if (svgEl && zoomBehavior) d3.select(svgEl).transition().duration(200).call(zoomBehavior.transform, d3.zoomIdentity);
});
// Recall overlay: pick a past recall query, light up the nodes it retrieved.
(function initRecall() {
const wrap = document.getElementById('semantic-recall-wrap');
const select = document.getElementById('semantic-recall');
const note = document.getElementById('semantic-recall-note');
if (!select) return;
const queries = recallQueries(SEARCH_EVENTS);
if (!queries.length) {
if (wrap) wrap.style.display = 'none'; // nothing to overlay
return;
}
queries.forEach((e, i) => {
const opt = document.createElement('option');
opt.value = String(i);
const q = (e.question || 'recall ' + (i + 1)).trim();
opt.textContent = truncate(q, 40);
select.appendChild(opt);
});
select.addEventListener('change', () => {
const idx = select.value;
if (idx === '') {
state.recall = null;
if (note) note.textContent = '';
} else {
const evt = queries[+idx];
state.recall = new Set((evt.node_ids || []).map(String));
const positions = window._semanticPositions || {};
if (note) note.textContent = recallOnMap(evt, positions) + ' of ' + (evt.node_ids || []).length + ' on map';
}
repaint();
});
})();
// Deep link: #semantic opens the tab on load.
if (window.location.hash === '#semantic') {
const btn = document.querySelector('.tab-btn[data-view="semantic"]');
if (btn) btn.click();
}
})();