Spaces:
Running
Running
File size: 6,782 Bytes
aca3d0b 044ec7f bfedc14 aca3d0b 9527547 aca3d0b 9527547 bbe02b6 aca3d0b bfedc14 aca3d0b bbe02b6 aca3d0b bbe02b6 aca3d0b bbe02b6 aca3d0b bbe02b6 aca3d0b 9527547 aca3d0b 044ec7f a9c257b aca3d0b a9c257b aca3d0b a9c257b aca3d0b 044ec7f aca3d0b 044ec7f aca3d0b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
import { useEffect, useRef, useState, useMemo } from 'react';
import ForceGraph2D, { type ForceGraphMethods } from 'react-force-graph-2d';
import { forceRadial } from 'd3-force';
import type { GraphData } from '@/types/graph';
import { getGraphData } from '@/services/api';
import { Loader2, AlertCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
interface GraphViewProps {
onSelectNote: (path: string) => void;
refreshTrigger?: number;
}
export function GraphView({ onSelectNote, refreshTrigger }: GraphViewProps) {
console.log('[GraphView] Component rendered, refreshTrigger:', refreshTrigger);
const [data, setData] = useState<GraphData>({ nodes: [], links: [] });
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const graphRef = useRef<ForceGraphMethods | undefined>(undefined);
// Theme detection would go here, simplified for MVP
const isDark = document.documentElement.classList.contains('dark');
// Load saved view state
useEffect(() => {
if (!isLoading && graphRef.current) {
const savedView = localStorage.getItem('graph-view-state');
if (savedView) {
const { x, y, k } = JSON.parse(savedView);
graphRef.current.centerAt(x, y, 0);
graphRef.current.zoom(k, 0);
}
}
}, [isLoading]);
// Save view state on unmount
useEffect(() => {
return () => {
if (graphRef.current) {
// Note: react-force-graph types might be incomplete for getting state directly
// This is a best-effort implementation.
// Often we can't easily get current x,y,k without internal access or tracking interaction.
// For now, we will skip complex persistence if the library doesn't support getter easily.
// Wait, we can use graphRef.current.zoom() as a getter?
// The docs say zoom(k) sets it. getter? usually yes if arg missing.
try {
// @ts-ignore
const k = graphRef.current.zoom();
// @ts-ignore
const { x, y } = graphRef.current.centerAt();
if (x !== undefined && k !== undefined) {
localStorage.setItem('graph-view-state', JSON.stringify({ x, y, k }));
}
} catch (e) {
// Ignore errors if getters fail
}
}
};
}, []);
useEffect(() => {
console.log('[GraphView] useEffect triggered, refreshTrigger:', refreshTrigger);
const fetchData = async () => {
try {
console.log('[GraphView] Fetching graph data...');
setIsLoading(true);
const graphData = await getGraphData();
console.log('[GraphView] Received graph data:', {
nodes: graphData.nodes.length,
links: graphData.links.length
});
setData(graphData);
setError(null);
} catch (err) {
console.error('[GraphView] Failed to load graph data:', err);
setError('Failed to load graph data. Please try again.');
} finally {
setIsLoading(false);
}
};
fetchData();
}, [refreshTrigger]);
// Configure forces when data is loaded
useEffect(() => {
if (!isLoading && graphRef.current) {
// Configure forces
// Increase repulsion to spread out clusters
graphRef.current.d3Force('charge')?.strength(-400);
// Adjust link distance
graphRef.current.d3Force('link')?.distance(60);
// Add "valence shell" for orphans (nodes with val=1)
// Pulls them to a ring at radius 300
graphRef.current.d3Force(
'valence',
forceRadial(300, 0, 0).strength((node: any) => node.val === 1 ? 0.1 : 0)
);
// Add collision detection to prevent overlap
// @ts-ignore - d3 types might not be fully exposed
if (!graphRef.current.d3Force('collide')) {
// dynamic import of d3 would be needed to create new forces if not default
}
// Warmup the engine
graphRef.current.d3ReheatSimulation();
}
}, [data, isLoading]);
// Calculate max connectivity for gradient normalization
const maxVal = useMemo(() => {
return Math.max(1, ...data.nodes.map(node => node.val || 1));
}, [data.nodes]);
// Node styling based on theme and connectivity
const getNodeColor = (node: any) => {
const val = node.val || 1;
// Normalize value 0..1 (logarithmic scale often looks better for power-law graphs)
const normalized = Math.min(1, (val - 1) / (Math.max(maxVal, 2) - 1));
if (isDark) {
// Dark mode: Slate-400 (#94a3b8) to Lime-300 (#bef264)
// Simple linear interpolation for RGB
const r = 148 + (190 - 148) * normalized;
const g = 163 + (242 - 163) * normalized;
const b = 184 + (100 - 184) * normalized;
return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
} else {
// Light mode: Slate-500 (#64748b) to Lime-500 (#84cc16)
const r = 100 + (132 - 100) * normalized;
const g = 116 + (204 - 116) * normalized;
const b = 139 + (22 - 139) * normalized;
return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
}
};
const linkColor = isDark ? '#334155' : '#e2e8f0';
const backgroundColor = isDark ? '#020817' : '#ffffff';
const handleNodeClick = (node: any) => {
if (node && node.id) {
onSelectNote(node.id);
}
};
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center bg-background text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-8 w-8 animate-spin" />
<p>Loading graph...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="p-8">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="h-full w-full overflow-hidden bg-background">
<ForceGraph2D
ref={graphRef}
graphData={data}
nodeLabel="label"
nodeColor={getNodeColor}
linkColor={() => linkColor}
linkWidth={2} //width of links between nodes
backgroundColor={backgroundColor}
onNodeClick={handleNodeClick}
nodeRelSize={6}
linkDirectionalParticles={2}
linkDirectionalParticleWidth={7}
linkDirectionalParticleSpeed={0.0025}
width={window.innerWidth * 0.75} // Approximate width, needs resize observer for true responsiveness
height={window.innerHeight - 60} // Approximate height minus header
/>
</div>
);
}
|