@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic'
66import ReactMarkdown from 'react-markdown'
77import { NEGATIVE_FEEDBACK_REASON_CODES } from '../lib/feedback.js'
88
9- const ForceGraph2D = dynamic ( ( ) => import ( 'react-force-graph-2d' ) , { ssr : false } )
9+ const cytoscape = typeof window !== 'undefined' ? require ( 'cytoscape' ) : null
1010
1111const FEEDBACK_REASON_LABELS = {
1212 helpful : 'Helpful' ,
@@ -55,7 +55,8 @@ function getGraphNodeRole(stats = {}, directed = true) {
5555
5656const BasicGraphView = memo ( function BasicGraphView ( { graph } ) {
5757 const containerRef = useRef ( null )
58- const fgRef = useRef ( null )
58+ const graphRef = useRef ( null )
59+ const cyRef = useRef ( null )
5960 const [ dimensions , setDimensions ] = useState ( { width : 640 , height : 400 } )
6061
6162 const nodes = useMemo ( ( ) => ( Array . isArray ( graph ?. nodes ) ? graph . nodes : [ ] ) , [ graph ?. nodes ] )
@@ -146,30 +147,36 @@ const BasicGraphView = memo(function BasicGraphView({ graph }) {
146147 }
147148 } , [ nodes , edges , isDirected ] )
148149
149- const graphData = useMemo ( ( ) => {
150+ const elements = useMemo ( ( ) => {
150151 const nodeIds = new Set ( nodes . map ( n => String ( n . id ) ) )
151- return {
152- nodes : nodes . map ( n => {
153- const id = String ( n . id )
154- const visualGroup = visualGrouping . byNodeId [ id ] || { }
155- return {
152+ const maxWeight = Math . max ( 1 , ...edges . map ( e => Number ( e . weight ) || 1 ) )
153+ const cyNodes = nodes . map ( n => {
154+ const id = String ( n . id )
155+ const visualGroup = visualGrouping . byNodeId [ id ] || { }
156+ return {
157+ data : {
156158 id,
157159 label : n . label || n . id ,
158160 group : visualGroup . label || normalizeGraphGroup ( n . group ) ,
159161 originalGroup : normalizeGraphGroup ( n . group ) ,
160162 color : visualGroup . color || n . color || GRAPH_PALETTE [ hashString ( n . label || n . id ) % GRAPH_PALETTE . length ] ,
161- size : n . size || 1
163+ size : Math . max ( 20 , 20 + ( n . size || 1 ) * 10 )
162164 }
163- } ) ,
164- links : edges
165- . filter ( e => nodeIds . has ( String ( e . source ) ) && nodeIds . has ( String ( e . target ) ) )
166- . map ( e => ( {
165+ }
166+ } )
167+ const cyEdges = edges
168+ . filter ( e => nodeIds . has ( String ( e . source ) ) && nodeIds . has ( String ( e . target ) ) )
169+ . map ( ( e , i ) => ( {
170+ data : {
171+ id : `e${ i } ` ,
167172 source : String ( e . source ) ,
168173 target : String ( e . target ) ,
169174 label : e . label || ( Number . isFinite ( Number ( e . weight ) ) ? String ( e . weight ) : '' ) ,
170- weight : Number ( e . weight ) || 1
171- } ) )
172- }
175+ weight : Number ( e . weight ) || 1 ,
176+ width : Math . max ( 1 , 1 + ( ( Number ( e . weight ) || 1 ) / maxWeight ) * 4 )
177+ }
178+ } ) )
179+ return [ ...cyNodes , ...cyEdges ]
173180 } , [ nodes , edges , visualGrouping ] )
174181
175182 // Measure container width
@@ -185,17 +192,102 @@ const BasicGraphView = memo(function BasicGraphView({ graph }) {
185192 return ( ) => ro . disconnect ( )
186193 } , [ ] )
187194
188- // Zoom to fit after initial layout settles
195+ // Initialize and update Cytoscape instance
196+ useEffect ( ( ) => {
197+ if ( ! graphRef . current || ! cytoscape || elements . length === 0 ) return
198+
199+ if ( cyRef . current ) {
200+ cyRef . current . destroy ( )
201+ }
202+
203+ const cy = cytoscape ( {
204+ container : graphRef . current ,
205+ elements : elements ,
206+ style : [
207+ {
208+ selector : 'node' ,
209+ style : {
210+ 'background-color' : 'data(color)' ,
211+ 'label' : 'data(label)' ,
212+ 'width' : 'data(size)' ,
213+ 'height' : 'data(size)' ,
214+ 'font-size' : '11px' ,
215+ 'color' : '#e5e7eb' ,
216+ 'text-valign' : 'bottom' ,
217+ 'text-margin-y' : 5 ,
218+ 'text-outline-color' : '#0f0f12' ,
219+ 'text-outline-width' : 2 ,
220+ 'border-width' : 1 ,
221+ 'border-color' : '#1a1a2e'
222+ }
223+ } ,
224+ {
225+ selector : 'edge' ,
226+ style : {
227+ 'width' : 'data(width)' ,
228+ 'line-color' : '#4b5563' ,
229+ 'target-arrow-color' : '#4b5563' ,
230+ 'target-arrow-shape' : isDirected ? 'triangle' : 'none' ,
231+ 'curve-style' : 'bezier' ,
232+ 'label' : 'data(label)' ,
233+ 'font-size' : '9px' ,
234+ 'color' : '#8f9aad' ,
235+ 'text-outline-color' : '#0f0f12' ,
236+ 'text-outline-width' : 1.5 ,
237+ 'text-rotation' : 'autorotate'
238+ }
239+ } ,
240+ {
241+ selector : 'node:selected' ,
242+ style : {
243+ 'border-width' : 3 ,
244+ 'border-color' : '#fff'
245+ }
246+ } ,
247+ {
248+ selector : 'edge:selected' ,
249+ style : {
250+ 'line-color' : '#9ecbff' ,
251+ 'target-arrow-color' : '#9ecbff' ,
252+ 'width' : 4
253+ }
254+ }
255+ ] ,
256+ layout : {
257+ name : isDirected ? 'breadthfirst' : 'cose' ,
258+ directed : isDirected ,
259+ spacingFactor : 1.2 ,
260+ animate : true ,
261+ animationDuration : 500 ,
262+ padding : 30 ,
263+ nodeDimensionsIncludeLabels : true ,
264+ ...( isDirected ? { } : {
265+ idealEdgeLength : 100 ,
266+ nodeRepulsion : 4500 ,
267+ gravity : 0.25
268+ } )
269+ } ,
270+ userZoomingEnabled : true ,
271+ userPanningEnabled : true ,
272+ boxSelectionEnabled : false
273+ } )
274+
275+ cy . on ( 'layoutstop' , ( ) => cy . fit ( undefined , 30 ) )
276+ cyRef . current = cy
277+
278+ return ( ) => {
279+ cy . destroy ( )
280+ cyRef . current = null
281+ }
282+ } , [ elements , isDirected ] )
283+
284+ // Resize cytoscape when dimensions change
189285 useEffect ( ( ) => {
190- const timer = setTimeout ( ( ) => {
191- if ( fgRef . current ) fgRef . current . zoomToFit ( 300 , 40 )
192- } , 800 )
193- return ( ) => clearTimeout ( timer )
194- } , [ graphData ] )
286+ if ( cyRef . current ) cyRef . current . resize ( )
287+ } , [ dimensions ] )
195288
196289 if ( nodes . length === 0 || edges . length === 0 ) return null
197290
198- const maxWeight = Math . max ( 1 , ...graphData . links . map ( l => l . weight ) )
199291 const legendEntries = visualGrouping . legend
200292
201293 return (
@@ -232,58 +324,9 @@ const BasicGraphView = memo(function BasicGraphView({ graph }) {
232324 ) ) }
233325 </ div >
234326 ) }
235- < ForceGraph2D
236- ref = { fgRef }
237- graphData = { graphData }
238- width = { dimensions . width - 20 }
239- height = { dimensions . height }
240- backgroundColor = "#0f0f12"
241- nodeRelSize = { 6 }
242- nodeVal = { n => Math . max ( 1 , ( n . size || 1 ) * 1.5 ) }
243- nodeColor = { n => n . color }
244- nodeLabel = { n => {
245- const details = [ ]
246- if ( n . originalGroup ) details . push ( `type: ${ n . originalGroup } ` )
247- if ( visualGrouping . useStructuralColoring && n . group && n . group !== n . originalGroup ) {
248- details . push ( `role: ${ n . group } ` )
249- }
250- return details . length > 0 ? `${ n . label } (${ details . join ( '; ' ) } )` : n . label
251- } }
252- nodeCanvasObject = { ( node , ctx , globalScale ) => {
253- const r = Math . max ( 3 , 4 + ( node . size || 1 ) * 2 )
254- ctx . beginPath ( )
255- ctx . arc ( node . x , node . y , r , 0 , 2 * Math . PI )
256- ctx . fillStyle = node . color
257- ctx . fill ( )
258- ctx . strokeStyle = '#1a1a2e'
259- ctx . lineWidth = 0.5
260- ctx . stroke ( )
261- // Draw label when zoomed in enough
262- if ( globalScale > 0.7 ) {
263- const label = node . label || node . id
264- const fontSize = Math . max ( 3 , 10 / globalScale )
265- ctx . font = `${ fontSize } px sans-serif`
266- ctx . textAlign = 'center'
267- ctx . textBaseline = 'top'
268- ctx . fillStyle = '#e5e7eb'
269- ctx . fillText ( label , node . x , node . y + r + 2 )
270- }
271- } }
272- linkColor = { ( ) => '#4b5563' }
273- linkWidth = { link => Math . max ( 0.5 , 1 + ( link . weight / maxWeight ) * 3 ) }
274- linkDirectionalArrowLength = { isDirected ? 5 : 0 }
275- linkDirectionalArrowRelPos = { 1 }
276- linkLabel = { link => link . label }
277- linkCurvature = { link => {
278- // Curve parallel edges between same node pairs
279- const key = [ link . source ?. id || link . source , link . target ?. id || link . target ] . sort ( ) . join ( '-' )
280- const rev = [ link . target ?. id || link . target , link . source ?. id || link . source ] . sort ( ) . join ( '-' )
281- return key === rev ? 0 : 0.15
282- } }
283- d3VelocityDecay = { 0.3 }
284- cooldownTicks = { 80 }
285- enableZoomInteraction = { true }
286- enablePanInteraction = { true }
327+ < div
328+ ref = { graphRef }
329+ style = { { width : dimensions . width - 20 , height : dimensions . height , backgroundColor : '#0f0f12' } }
287330 />
288331 </ div >
289332 )
0 commit comments