@@ -230,91 +230,92 @@ export default function KnowledgeGraph({
230230 // ── Fetch graph data ────────────────────────────────────────────────────────
231231
232232 const fetchGraph = useCallback ( async ( ) => {
233- setLoading ( true ) ;
234- setError ( null ) ;
235- setSelectedNode ( null ) ;
236-
237- let data : GraphData | null = null ;
238- let fetchError : string | null = null ;
239-
240- try {
241- data = await api . get < GraphData > ( `/api/v1/graph/${ documentId } ` ) ;
242- } catch ( err ) {
243- fetchError = err instanceof Error ? err . message : "Failed to load knowledge graph" ;
244- }
245-
246- // All setState calls happen after the await, satisfying the linter
247- if ( fetchError || ! data ) {
248- setError ( fetchError ?? "No data returned" ) ;
249- setNodes ( [ ] ) ;
250- setEdges ( [ ] ) ;
233+ setLoading ( true ) ;
234+ setError ( null ) ;
235+ setSelectedNode ( null ) ;
236+
237+ let data : GraphData | null = null ;
238+ let fetchError : string | null = null ;
239+
240+ try {
241+ data = await api . get < GraphData > ( `/api/v1/graph/${ documentId } ` ) ;
242+ } catch ( err ) {
243+ fetchError =
244+ err instanceof Error ? err . message : "Failed to load knowledge graph" ;
245+ }
246+
247+ // All setState calls happen after the await, satisfying the linter
248+ if ( fetchError || ! data ) {
249+ setError ( fetchError ?? "No data returned" ) ;
250+ setNodes ( [ ] ) ;
251+ setEdges ( [ ] ) ;
252+ setLoading ( false ) ;
253+ return ;
254+ }
255+
256+ setGraphData ( data ) ;
257+
258+ const maxMentions = Math . max ( ...data . nodes . map ( ( n ) => n . mentions ) , 1 ) ;
259+ const radius = Math . min ( Math . max ( data . nodes . length * 18 , 200 ) , 500 ) ;
260+
261+ const rfNodes : Node [ ] = data . nodes . map ( ( n , i ) => {
262+ const colour = colourFor ( n . label ) ;
263+ const size = 36 + Math . round ( ( n . mentions / maxMentions ) * 36 ) ;
264+ const pos = radialPosition ( i , data ! . nodes . length , radius ) ;
265+ return {
266+ id : n . id ,
267+ position : pos ,
268+ data : {
269+ label : (
270+ < div
271+ className = "flex items-center justify-center text-center font-semibold leading-tight px-1"
272+ style = { { fontSize : Math . max ( 9 , size * 0.22 ) , color : colour . text } }
273+ title = { n . name }
274+ >
275+ { n . name . length > 14 ? n . name . slice ( 0 , 13 ) + "…" : n . name }
276+ </ div >
277+ ) ,
278+ _raw : n ,
279+ } ,
280+ style : {
281+ width : size ,
282+ height : size ,
283+ borderRadius : "50%" ,
284+ backgroundColor : colour . bg ,
285+ border : `2px solid ${ colour . border } ` ,
286+ display : "flex" ,
287+ alignItems : "center" ,
288+ justifyContent : "center" ,
289+ cursor : "pointer" ,
290+ boxShadow : "0 1px 4px rgba(0,0,0,0.12)" ,
291+ } ,
292+ } ;
293+ } ) ;
294+
295+ const maxWeight = Math . max ( ...data . edges . map ( ( e ) => e . weight ) , 1 ) ;
296+ const rfEdges : Edge [ ] = data . edges . map ( ( e , i ) => {
297+ const opacity = 0.2 + 0.6 * ( e . weight / maxWeight ) ;
298+ const strokeWidth = 1 + Math . round ( ( e . weight / maxWeight ) * 3 ) ;
299+ return {
300+ id : `e-${ i } ` ,
301+ source : e . source ,
302+ target : e . target ,
303+ animated : false ,
304+ style : { stroke : `rgba(100,116,139,${ opacity } )` , strokeWidth } ,
305+ data : { _raw : e } ,
306+ } ;
307+ } ) ;
308+
309+ setNodes ( rfNodes ) ;
310+ setEdges ( rfEdges ) ;
251311 setLoading ( false ) ;
252- return ;
253- }
254-
255- setGraphData ( data ) ;
256-
257- const maxMentions = Math . max ( ...data . nodes . map ( ( n ) => n . mentions ) , 1 ) ;
258- const radius = Math . min ( Math . max ( data . nodes . length * 18 , 200 ) , 500 ) ;
259-
260- const rfNodes : Node [ ] = data . nodes . map ( ( n , i ) => {
261- const colour = colourFor ( n . label ) ;
262- const size = 36 + Math . round ( ( n . mentions / maxMentions ) * 36 ) ;
263- const pos = radialPosition ( i , data ! . nodes . length , radius ) ;
264- return {
265- id : n . id ,
266- position : pos ,
267- data : {
268- label : (
269- < div
270- className = "flex items-center justify-center text-center font-semibold leading-tight px-1"
271- style = { { fontSize : Math . max ( 9 , size * 0.22 ) , color : colour . text } }
272- title = { n . name }
273- >
274- { n . name . length > 14 ? n . name . slice ( 0 , 13 ) + "…" : n . name }
275- </ div >
276- ) ,
277- _raw : n ,
278- } ,
279- style : {
280- width : size ,
281- height : size ,
282- borderRadius : "50%" ,
283- backgroundColor : colour . bg ,
284- border : `2px solid ${ colour . border } ` ,
285- display : "flex" ,
286- alignItems : "center" ,
287- justifyContent : "center" ,
288- cursor : "pointer" ,
289- boxShadow : "0 1px 4px rgba(0,0,0,0.12)" ,
290- } ,
291- } ;
292- } ) ;
293-
294- const maxWeight = Math . max ( ...data . edges . map ( ( e ) => e . weight ) , 1 ) ;
295- const rfEdges : Edge [ ] = data . edges . map ( ( e , i ) => {
296- const opacity = 0.2 + 0.6 * ( e . weight / maxWeight ) ;
297- const strokeWidth = 1 + Math . round ( ( e . weight / maxWeight ) * 3 ) ;
298- return {
299- id : `e-${ i } ` ,
300- source : e . source ,
301- target : e . target ,
302- animated : false ,
303- style : { stroke : `rgba(100,116,139,${ opacity } )` , strokeWidth } ,
304- data : { _raw : e } ,
305- } ;
306- } ) ;
307-
308- setNodes ( rfNodes ) ;
309- setEdges ( rfEdges ) ;
310- setLoading ( false ) ;
311- } , [ documentId , setNodes , setEdges ] ) ;
312-
313- useEffect ( ( ) => {
314- fetchGraph ( ) ;
315- // fetchGraph is stable (memoised on documentId) — safe to omit from deps
316- // eslint-disable-next-line react-hooks/exhaustive-deps
317- } , [ documentId ] ) ;
312+ } , [ documentId , setNodes , setEdges ] ) ;
313+
314+ useEffect ( ( ) => {
315+ fetchGraph ( ) ;
316+ // fetchGraph is stable (memoised on documentId) — safe to omit from deps
317+ // eslint-disable-next-line react-hooks/exhaustive-deps
318+ } , [ documentId ] ) ;
318319
319320 // ── Node click → show detail panel ─────────────────────────────────────────
320321
0 commit comments