fix(graph): resolve multi-node selection bug by enforcing unique concept IDs and hardening JS logic

This commit is contained in:
2026-05-09 09:01:47 +02:00
parent c77ff6f347
commit f319f27786
2 changed files with 8 additions and 5 deletions
@@ -17,7 +17,8 @@ public static class PromptRegistry
"You are an expert at information architecture. Extract key concepts and their relationships from the text to build a knowledge graph. " + "You are an expert at information architecture. Extract key concepts and their relationships from the text to build a knowledge graph. " +
"CRITICAL: Restrict 'label' to a maximum of 3 words. " + "CRITICAL: Restrict 'label' to a maximum of 3 words. " +
"CRITICAL: Extract a MAXIMUM of 15 key concepts/plot points and their relationships. " + "CRITICAL: Extract a MAXIMUM of 15 key concepts/plot points and their relationships. " +
"CRITICAL: Each paragraph in the user text starts with [ID: some-id]. You MUST use these exact IDs as the 'id' for the nodes representing those blocks. " + "CRITICAL: Each paragraph in the user text starts with [ID: some-id]. Use these IDs ONLY for nodes representing the blocks. " +
"CRITICAL: All other extracted 'concept' nodes MUST have unique, slug-style IDs based on their labels (e.g., 'dependency-injection'). " +
"Include a 'current' node representing the block content itself if applicable. " + "Include a 'current' node representing the block content itself if applicable. " +
"CRITICAL: Limit the result to a MAXIMUM of 15 most relevant connections. " + "CRITICAL: Limit the result to a MAXIMUM of 15 most relevant connections. " +
"Return ONLY minified JSON. Schema: { \"graph\": { \"nodes\": [ { \"id\": \"string\", \"label\": \"string\", \"group\": \"concept|current\" } ], \"links\": [ { \"source\": \"string\", \"target\": \"string\", \"value\": 1 } ] } }"; "Return ONLY minified JSON. Schema: { \"graph\": { \"nodes\": [ { \"id\": \"string\", \"label\": \"string\", \"group\": \"concept|current\" } ], \"links\": [ { \"source\": \"string\", \"target\": \"string\", \"value\": 1 } ] } }";
@@ -247,6 +247,7 @@ function drag(simulation) {
export function setActiveNode(nodeId) { export function setActiveNode(nodeId) {
if (!svgElement || !node) return; if (!svgElement || !node) return;
// Safety check: ensure we only target the first occurrence if IDs are duplicated
const targetNode = node.filter(d => d.id === nodeId); const targetNode = node.filter(d => d.id === nodeId);
if (targetNode.empty()) { if (targetNode.empty()) {
dimNodes(null); dimNodes(null);
@@ -254,20 +255,21 @@ export function setActiveNode(nodeId) {
return; return;
} }
const d = targetNode.datum(); const firstMatch = targetNode.filter((d, i) => i === 0);
const d = firstMatch.datum();
// Reset all active classes // Reset all active classes
rootGroup.selectAll(".node-pill").classed("nexus-node-active", false); rootGroup.selectAll(".node-pill").classed("nexus-node-active", false);
targetNode.select(".node-pill").classed("nexus-node-active", true); firstMatch.select(".node-pill").classed("nexus-node-active", true);
// Position badge // Position badge
badge.style("display", "block").datum(d); badge.style("display", "block").datum(d);
badge.attr("transform", `translate(${d.x},${d.y})`); badge.attr("transform", `translate(${d.x},${d.y})`);
// Dim others // Dim others (only exact matches for nodeId will be fully opaque)
dimNodes(nodeId); dimNodes(nodeId);
// Smooth transition // Smooth transition to the first matching node
svgElement.transition().duration(1000).call( svgElement.transition().duration(1000).call(
zoomBehavior.transform, zoomBehavior.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(1.2).translate(-d.x, -d.y) d3.zoomIdentity.translate(width / 2, height / 2).scale(1.2).translate(-d.x, -d.y)