mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
Merge pull request #25 from abhigyanpatwari/Schema_Experiments
blast radius tool implemented
This commit is contained in:
commit
8f004dab62
5 changed files with 293 additions and 15 deletions
|
|
@ -21,6 +21,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
highlightedNodeIds,
|
||||
aiCitationHighlightedNodeIds,
|
||||
aiToolHighlightedNodeIds,
|
||||
blastRadiusNodeIds,
|
||||
isAIHighlightsEnabled,
|
||||
toggleAIHighlights,
|
||||
} = useAppState();
|
||||
|
|
@ -31,9 +32,16 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
const next = new Set(highlightedNodeIds);
|
||||
for (const id of aiCitationHighlightedNodeIds) next.add(id);
|
||||
for (const id of aiToolHighlightedNodeIds) next.add(id);
|
||||
// Note: blast radius nodes are handled separately with red color
|
||||
return next;
|
||||
}, [highlightedNodeIds, aiCitationHighlightedNodeIds, aiToolHighlightedNodeIds, isAIHighlightsEnabled]);
|
||||
|
||||
|
||||
// Blast radius nodes (only when AI highlights enabled)
|
||||
const effectiveBlastRadiusNodeIds = useMemo(() => {
|
||||
if (!isAIHighlightsEnabled) return new Set<string>();
|
||||
return blastRadiusNodeIds;
|
||||
}, [blastRadiusNodeIds, isAIHighlightsEnabled]);
|
||||
|
||||
const handleNodeClick = useCallback((nodeId: string) => {
|
||||
if (!graph) return;
|
||||
const node = graph.nodes.find(n => n.id === nodeId);
|
||||
|
|
@ -76,6 +84,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
onNodeHover: handleNodeHover,
|
||||
onStageClick: handleStageClick,
|
||||
highlightedNodeIds: effectiveHighlightedNodeIds,
|
||||
blastRadiusNodeIds: effectiveBlastRadiusNodeIds,
|
||||
});
|
||||
|
||||
// Expose focusNode to parent via ref
|
||||
|
|
@ -104,10 +113,10 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
useEffect(() => {
|
||||
const sigma = sigmaRef.current;
|
||||
if (!sigma) return;
|
||||
|
||||
|
||||
const sigmaGraph = sigma.getGraph() as Graph<SigmaNodeAttributes, SigmaEdgeAttributes>;
|
||||
if (sigmaGraph.order === 0) return; // Don't filter empty graph
|
||||
|
||||
|
||||
filterGraphByDepth(sigmaGraph, appSelectedNode?.id || null, depthFilter, visibleLabels);
|
||||
sigma.refresh();
|
||||
}, [visibleLabels, depthFilter, appSelectedNode, sigmaRef]);
|
||||
|
|
@ -139,7 +148,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
<div className="relative w-full h-full bg-void">
|
||||
{/* Background gradient */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: `
|
||||
|
|
@ -151,8 +160,8 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
</div>
|
||||
|
||||
{/* Sigma container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="sigma-container w-full h-full cursor-grab active:cursor-grabbing"
|
||||
/>
|
||||
|
||||
|
|
@ -205,10 +214,10 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
>
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-border-subtle my-1" />
|
||||
|
||||
|
||||
{/* Focus on selected */}
|
||||
{appSelectedNode && (
|
||||
<button
|
||||
|
|
@ -219,7 +228,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
<Focus className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Clear selection */}
|
||||
{sigmaSelectedNode && (
|
||||
<button
|
||||
|
|
@ -230,17 +239,17 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-border-subtle my-1" />
|
||||
|
||||
|
||||
{/* Layout control */}
|
||||
<button
|
||||
onClick={isLayoutRunning ? stopLayout : startLayout}
|
||||
className={`
|
||||
w-9 h-9 flex items-center justify-center border rounded-md transition-all
|
||||
${isLayoutRunning
|
||||
? 'bg-accent border-accent text-white shadow-glow animate-pulse'
|
||||
${isLayoutRunning
|
||||
? 'bg-accent border-accent text-white shadow-glow animate-pulse'
|
||||
: 'bg-elevated border-border-subtle text-text-secondary hover:bg-hover hover:text-text-primary'
|
||||
}
|
||||
`}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ You are an investigator. For each question:
|
|||
- **\`grep\`** — Regex search. Best for exact strings, TODOs, error codes.
|
||||
- **\`read\`** — Read file content. Always use after search/grep to see full code.
|
||||
- **\`highlight\`** — Highlight nodes in the visual graph.
|
||||
- **\`blastRadius\`** — Impact analysis. Output is graph-verified (trusted). Run optional grep for dynamic patterns if thoroughness needed.
|
||||
|
||||
## 📊 GRAPH SCHEMA
|
||||
Nodes: File, Folder, Function, Class, Interface, Method, CodeElement
|
||||
|
|
@ -83,6 +84,7 @@ Cypher examples:
|
|||
- \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
|
||||
|
||||
## 📝CRITICAL RULES
|
||||
- **blastRadius output is trusted.** Do NOT re-validate with cypher. Optionally run the suggested grep commands for dynamic patterns.
|
||||
- **Cite or retract.** Never state something you can't ground.
|
||||
- **Read before concluding.** Don't guess from names alone.
|
||||
- **Retry on failure.** If a tool fails, fix the input and try again.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* Graph RAG Tools for LangChain Agent
|
||||
*
|
||||
* Consolidated tools (5 total):
|
||||
* Consolidated tools (6 total):
|
||||
* - search: Hybrid search (BM25 + semantic + RRF) with 1-hop expansion
|
||||
* - cypher: Execute Cypher queries (auto-embeds {{QUERY_VECTOR}} if present)
|
||||
* - grep: Regex pattern search across files
|
||||
* - read: Read file content by path
|
||||
* - highlight: Highlight nodes in graph UI
|
||||
* - blastRadius: Impact analysis (what depends on / is affected by changes)
|
||||
*/
|
||||
|
||||
import { tool } from '@langchain/core/tools';
|
||||
|
|
@ -382,6 +383,206 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 6: BLAST RADIUS (Impact analysis)
|
||||
// ============================================================================
|
||||
|
||||
const blastRadiusTool = tool(
|
||||
async ({ target, direction, maxDepth }: {
|
||||
target: string;
|
||||
direction: 'upstream' | 'downstream';
|
||||
maxDepth?: number;
|
||||
}) => {
|
||||
const depth = Math.min(maxDepth ?? 3, 10);
|
||||
|
||||
// Determine the traversal direction
|
||||
const directionArrow = direction === 'upstream' ? '<-' : '->';
|
||||
const directionLabel = direction === 'upstream'
|
||||
? 'what depends on this (callers, importers, child classes)'
|
||||
: 'what this depends on (callees, imports, parent classes)';
|
||||
|
||||
// Try to find the target node first
|
||||
const findTargetQuery = `
|
||||
MATCH (n)
|
||||
WHERE n.name = '${target.replace(/'/g, "''")}'
|
||||
RETURN n.id AS id, label(n) AS nodeType, n.filePath AS filePath
|
||||
LIMIT 5
|
||||
`;
|
||||
|
||||
let targetResults;
|
||||
try {
|
||||
targetResults = await executeQuery(findTargetQuery);
|
||||
} catch (error) {
|
||||
return `Error finding target "${target}": ${error}`;
|
||||
}
|
||||
|
||||
if (!targetResults || targetResults.length === 0) {
|
||||
return `Could not find "${target}" in the codebase. Try using the search tool first to find the exact name.`;
|
||||
}
|
||||
|
||||
// Use the first match
|
||||
const targetNode = targetResults[0];
|
||||
const targetId = Array.isArray(targetNode) ? targetNode[0] : targetNode.id;
|
||||
const targetType = Array.isArray(targetNode) ? targetNode[1] : targetNode.nodeType;
|
||||
|
||||
// Note: KuzuDB doesn't support [r IN relationships(path) | r.type] list comprehension
|
||||
// So we query each depth level separately for accurate depth tracking
|
||||
|
||||
// Get depth info with separate simpler queries for each depth level
|
||||
const depthQueries: Promise<any[]>[] = [];
|
||||
for (let d = 1; d <= Math.min(depth, 3); d++) {
|
||||
const dQuery = direction === 'upstream'
|
||||
? `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (affected)-[:CodeRelation*${d}]->(target)
|
||||
RETURN DISTINCT affected.id AS id, affected.name AS name, label(affected) AS nodeType, affected.filePath AS filePath, ${d} AS depth
|
||||
LIMIT 100
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
MATCH (target)-[:CodeRelation*${d}]->(affected)
|
||||
RETURN DISTINCT affected.id AS id, affected.name AS name, label(affected) AS nodeType, affected.filePath AS filePath, ${d} AS depth
|
||||
LIMIT 100
|
||||
`;
|
||||
depthQueries.push(executeQuery(dQuery).catch(() => []));
|
||||
}
|
||||
|
||||
// Wait for all depth queries
|
||||
const depthResults = await Promise.all(depthQueries);
|
||||
|
||||
// Combine results by depth
|
||||
const byDepth: Map<number, any[]> = new Map();
|
||||
const allNodeIds: string[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
depthResults.forEach((results, idx) => {
|
||||
const d = idx + 1;
|
||||
results.forEach((row: any) => {
|
||||
const nodeId = Array.isArray(row) ? row[0] : row.id;
|
||||
// Avoid duplicates (a node might appear at multiple depths)
|
||||
if (nodeId && !seenIds.has(nodeId)) {
|
||||
seenIds.add(nodeId);
|
||||
if (!byDepth.has(d)) byDepth.set(d, []);
|
||||
byDepth.get(d)!.push(row);
|
||||
allNodeIds.push(nodeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const totalAffected = allNodeIds.length;
|
||||
|
||||
if (totalAffected === 0) {
|
||||
return `No ${direction} dependencies found for "${target}". This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.`;
|
||||
}
|
||||
|
||||
// Build tiered output
|
||||
const lines: string[] = [
|
||||
`🔴 BLAST RADIUS: ${target}`,
|
||||
``,
|
||||
`Direction: ${direction} (${directionLabel})`,
|
||||
`Total affected: ${totalAffected} components`,
|
||||
``,
|
||||
];
|
||||
|
||||
// Depth 1 - Critical
|
||||
const depth1 = byDepth.get(1) || [];
|
||||
if (depth1.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 1 — WILL BREAK (${depth1.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth1.slice(0, 20).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
const filePath = Array.isArray(r) ? r[3] : r.filePath;
|
||||
const fileName = filePath?.split('/').pop() || '';
|
||||
lines.push(`• ${name} (${nodeType}) at ${fileName}`);
|
||||
});
|
||||
if (depth1.length > 20) lines.push(` ... and ${depth1.length - 20} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Depth 2 - High impact
|
||||
const depth2 = byDepth.get(2) || [];
|
||||
if (depth2.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 2 — LIKELY AFFECTED (${depth2.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth2.slice(0, 15).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
const filePath = Array.isArray(r) ? r[3] : r.filePath;
|
||||
const fileName = filePath?.split('/').pop() || '';
|
||||
lines.push(`• ${name} (${nodeType}) at ${fileName}`);
|
||||
});
|
||||
if (depth2.length > 15) lines.push(` ... and ${depth2.length - 15} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Depth 3 - Transitive
|
||||
const depth3 = byDepth.get(3) || [];
|
||||
if (depth3.length > 0) {
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`DEPTH 3 — MAY NEED TESTING (${depth3.length} components):`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
depth3.slice(0, 5).forEach((r: any) => {
|
||||
const name = Array.isArray(r) ? r[1] : r.name;
|
||||
const nodeType = Array.isArray(r) ? r[2] : r.nodeType;
|
||||
lines.push(`• ${name} (${nodeType})`);
|
||||
});
|
||||
if (depth3.length > 5) lines.push(` ... and ${depth3.length - 5} more`);
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Trusted analysis marker
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`✅ GRAPH ANALYSIS COMPLETE (trusted)`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`The above results are verified from code graph traversal.`);
|
||||
lines.push(`No additional validation needed for static dependencies.`);
|
||||
lines.push(``);
|
||||
|
||||
// Optional dynamic detection
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`⚠️ OPTIONAL: Dynamic Pattern Check`);
|
||||
lines.push(`═══════════════════════════════════════════════`);
|
||||
lines.push(`The graph cannot track: event listeners, DI, dynamic imports.`);
|
||||
lines.push(`If thoroughness is needed, run:`);
|
||||
lines.push(`• grep({ pattern: "${target}", fileFilter: "*.ts" })`);
|
||||
lines.push(`• grep({ pattern: "${target}", fileFilter: "*.json" })`);
|
||||
lines.push(``);
|
||||
|
||||
// Add the marker for UI highlighting
|
||||
const marker = `[BLAST_RADIUS:${allNodeIds.join(',')}]`;
|
||||
lines.push(marker);
|
||||
|
||||
return lines.join('\n');
|
||||
},
|
||||
{
|
||||
name: 'blastRadius',
|
||||
description: `Analyze the blast radius (impact) of changing a function, class, or file.
|
||||
|
||||
Use this when users ask:
|
||||
- "What would break if I changed X?"
|
||||
- "What depends on X?"
|
||||
- "Impact analysis for X"
|
||||
- "Blast radius of X"
|
||||
|
||||
Direction:
|
||||
- upstream: Find what CALLS/IMPORTS/EXTENDS this target (what would break)
|
||||
- downstream: Find what this target CALLS/IMPORTS/EXTENDS (dependencies)
|
||||
|
||||
Results are grouped by depth:
|
||||
- Depth 1: Direct dependencies (will definitely break)
|
||||
- Depth 2: Indirect (likely affected)
|
||||
- Depth 3+: Transitive (may need testing)`,
|
||||
schema: z.object({
|
||||
target: z.string().describe('Name of the function, class, or file to analyze'),
|
||||
direction: z.enum(['upstream', 'downstream']).describe('upstream = what depends on this; downstream = what this depends on'),
|
||||
maxDepth: z.number().optional().nullable().describe('Max traversal depth (default: 3, max: 10)'),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// RETURN ALL TOOLS
|
||||
// ============================================================================
|
||||
|
|
@ -392,5 +593,6 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
grepTool,
|
||||
readTool,
|
||||
highlightTool,
|
||||
blastRadiusTool,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,9 +77,11 @@ interface AppState {
|
|||
// AI highlights (toggable)
|
||||
aiCitationHighlightedNodeIds: Set<string>;
|
||||
aiToolHighlightedNodeIds: Set<string>;
|
||||
blastRadiusNodeIds: Set<string>;
|
||||
isAIHighlightsEnabled: boolean;
|
||||
toggleAIHighlights: () => void;
|
||||
clearAIToolHighlights: () => void;
|
||||
clearBlastRadius: () => void;
|
||||
queryResult: QueryResult | null;
|
||||
setQueryResult: (result: QueryResult | null) => void;
|
||||
clearQueryHighlights: () => void;
|
||||
|
|
@ -183,6 +185,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
// AI highlights (separate from user/query highlights)
|
||||
const [aiCitationHighlightedNodeIds, setAICitationHighlightedNodeIds] = useState<Set<string>>(new Set());
|
||||
const [aiToolHighlightedNodeIds, setAIToolHighlightedNodeIds] = useState<Set<string>>(new Set());
|
||||
const [blastRadiusNodeIds, setBlastRadiusNodeIds] = useState<Set<string>>(new Set());
|
||||
const [isAIHighlightsEnabled, setAIHighlightsEnabled] = useState(true);
|
||||
|
||||
const toggleAIHighlights = useCallback(() => {
|
||||
|
|
@ -193,6 +196,10 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
setAIToolHighlightedNodeIds(new Set());
|
||||
}, []);
|
||||
|
||||
const clearBlastRadius = useCallback(() => {
|
||||
setBlastRadiusNodeIds(new Set());
|
||||
}, []);
|
||||
|
||||
const clearQueryHighlights = useCallback(() => {
|
||||
setHighlightedNodeIds(new Set());
|
||||
setQueryResult(null);
|
||||
|
|
@ -812,6 +819,35 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
setAIToolHighlightedNodeIds(new Set(rawIds));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blast radius marker from tool results
|
||||
const blastMatch = tc.result.match(/\[BLAST_RADIUS:([^\]]+)\]/);
|
||||
if (blastMatch) {
|
||||
const rawIds = blastMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean);
|
||||
if (rawIds.length > 0 && graph) {
|
||||
const matchedIds = new Set<string>();
|
||||
const graphNodeIds = graph.nodes.map(n => n.id);
|
||||
|
||||
for (const rawId of rawIds) {
|
||||
if (graphNodeIds.includes(rawId)) {
|
||||
matchedIds.add(rawId);
|
||||
} else {
|
||||
const found = graphNodeIds.find(gid =>
|
||||
gid.endsWith(rawId) || gid.endsWith(':' + rawId)
|
||||
);
|
||||
if (found) {
|
||||
matchedIds.add(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedIds.size > 0) {
|
||||
setBlastRadiusNodeIds(matchedIds);
|
||||
}
|
||||
} else if (rawIds.length > 0) {
|
||||
setBlastRadiusNodeIds(new Set(rawIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -908,9 +944,11 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
setHighlightedNodeIds,
|
||||
aiCitationHighlightedNodeIds,
|
||||
aiToolHighlightedNodeIds,
|
||||
blastRadiusNodeIds,
|
||||
isAIHighlightsEnabled,
|
||||
toggleAIHighlights,
|
||||
clearAIToolHighlights,
|
||||
clearBlastRadius,
|
||||
queryResult,
|
||||
setQueryResult,
|
||||
clearQueryHighlights,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ interface UseSigmaOptions {
|
|||
onNodeHover?: (nodeId: string | null) => void;
|
||||
onStageClick?: () => void;
|
||||
highlightedNodeIds?: Set<string>;
|
||||
blastRadiusNodeIds?: Set<string>;
|
||||
}
|
||||
|
||||
interface UseSigmaReturn {
|
||||
|
|
@ -126,14 +127,16 @@ export const useSigma = (options: UseSigmaOptions = {}): UseSigmaReturn => {
|
|||
const layoutRef = useRef<FA2Layout | null>(null);
|
||||
const selectedNodeRef = useRef<string | null>(null);
|
||||
const highlightedRef = useRef<Set<string>>(new Set());
|
||||
const blastRadiusRef = useRef<Set<string>>(new Set());
|
||||
const layoutTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isLayoutRunning, setIsLayoutRunning] = useState(false);
|
||||
const [selectedNode, setSelectedNodeState] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
highlightedRef.current = options.highlightedNodeIds || new Set();
|
||||
blastRadiusRef.current = options.blastRadiusNodeIds || new Set();
|
||||
sigmaRef.current?.refresh();
|
||||
}, [options.highlightedNodeIds]);
|
||||
}, [options.highlightedNodeIds, options.blastRadiusNodeIds]);
|
||||
|
||||
const setSelectedNode = useCallback((nodeId: string | null) => {
|
||||
selectedNodeRef.current = nodeId;
|
||||
|
|
@ -242,8 +245,32 @@ export const useSigma = (options: UseSigmaOptions = {}): UseSigmaReturn => {
|
|||
|
||||
const currentSelected = selectedNodeRef.current;
|
||||
const highlighted = highlightedRef.current;
|
||||
const blastRadius = blastRadiusRef.current;
|
||||
const hasHighlights = highlighted.size > 0;
|
||||
const hasBlastRadius = blastRadius.size > 0;
|
||||
const isQueryHighlighted = highlighted.has(node);
|
||||
const isBlastRadiusNode = blastRadius.has(node);
|
||||
|
||||
// Blast radius takes priority (red highlighting)
|
||||
if (hasBlastRadius && !currentSelected) {
|
||||
if (isBlastRadiusNode) {
|
||||
res.color = '#ef4444'; // Red for blast radius
|
||||
res.size = (data.size || 8) * 1.8;
|
||||
res.zIndex = 3;
|
||||
res.highlighted = true;
|
||||
} else if (isQueryHighlighted) {
|
||||
// Regular cyan highlight for non-blast-radius nodes
|
||||
res.color = '#06b6d4';
|
||||
res.size = (data.size || 8) * 1.4;
|
||||
res.zIndex = 2;
|
||||
res.highlighted = true;
|
||||
} else {
|
||||
res.color = dimColor(data.color, 0.15);
|
||||
res.size = (data.size || 8) * 0.4;
|
||||
res.zIndex = 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (hasHighlights && !currentSelected) {
|
||||
if (isQueryHighlighted) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue