11"use client" ;
22
3- import { useEffect , useRef , useState } from 'react' ;
4- import {
5- ReactFlow ,
6- ReactFlowProvider ,
7- Background ,
8- Controls ,
9- MarkerType ,
10- useNodesState ,
11- useEdgesState ,
12- useReactFlow ,
13- } from '@xyflow/react' ;
14- import '@xyflow/react/dist/style.css' ;
3+ import { useState } from 'react' ;
4+ import { ReactFlowProvider } from '@xyflow/react' ;
155import Navbar from '@/components/navbar' ;
16- import {
17- PRESETS ,
18- toFlow ,
19- adjacency ,
20- bfsActions ,
21- dfsActions ,
22- newNodeId ,
23- edgeId ,
24- } from '@/lib/algorithms/graph' ;
25- import GraphNode from './graph-node' ;
26- import FloatingEdge from './floating-edge' ;
27- import Menu from './menu' ;
28-
29- const nodeTypes = { graphNode : GraphNode } ;
30- const edgeTypes = { floating : FloatingEdge } ;
31- const ARROW = { type : MarkerType . ArrowClosed , color : '#64748b' , width : 16 , height : 16 } ;
32-
33- const toDelay = ( s ) => 1100 - s * 10 ;
34-
35- function initialGraph ( presetIndex = 0 ) {
36- const { nodes, edges } = toFlow ( PRESETS [ presetIndex ] ) ;
37- if ( nodes [ 0 ] ) nodes [ 0 ] = { ...nodes [ 0 ] , data : { ...nodes [ 0 ] . data , role : 'start' } } ;
38- return { nodes, edges, startId : nodes [ 0 ] ?. id ?? null } ;
39- }
40-
41- const MODE_HINT = {
42- idle : 'Click a node to select · N add node · E add edge' ,
43- 'add-node' : 'Add node — click anywhere on the canvas' ,
44- 'add-edge' : 'Add edge — click two nodes' ,
45- } ;
6+ import { PRESETS , adjacency , bfsActions , dfsActions } from '@/lib/algorithms/graph' ;
7+ import { useGraphEditor } from '@/components/graph/use-graph-editor' ;
8+ import GraphCanvas from '@/components/graph/graph-canvas' ;
9+ import GraphMenu from '@/components/graph/graph-menu' ;
4610
4711function GraphInner ( ) {
48- const [ initial ] = useState ( ( ) => initialGraph ( 0 ) ) ;
49- const [ nodes , setNodes , onNodesChange ] = useNodesState ( initial . nodes ) ;
50- const [ edges , setEdges , onEdgesChange ] = useEdgesState ( initial . edges ) ;
51- const [ directed , setDirected ] = useState ( false ) ;
52- const [ mode , setMode ] = useState ( 'idle' ) ;
53- const [ isRunning , setIsRunning ] = useState ( false ) ;
54-
55- const { screenToFlowPosition } = useReactFlow ( ) ;
56-
57- // refs read inside async loop / global key handler
58- const nodesRef = useRef ( initial . nodes ) ;
59- const edgesRef = useRef ( initial . edges ) ;
60- const isRunningRef = useRef ( false ) ;
61- const speedRef = useRef ( toDelay ( 50 ) ) ;
62- const modeRef = useRef ( 'idle' ) ;
63- const pendingEdgeRef = useRef ( null ) ;
64- const directedRef = useRef ( false ) ;
65- const algorithmRef = useRef ( 0 ) ;
66- const startIdRef = useRef ( initial . startId ) ;
67- const finishIdRef = useRef ( null ) ;
68- const labelRef = useRef ( 0 ) ; // monotonic label counter for user-added nodes
69-
70- useEffect ( ( ) => { nodesRef . current = nodes ; } , [ nodes ] ) ;
71- useEffect ( ( ) => { edgesRef . current = edges ; } , [ edges ] ) ;
72- const setModeBoth = ( m ) => { modeRef . current = m ; setMode ( m ) ; } ;
73-
74- // --- visual action appliers ---
75- const markNode = ( id , state ) =>
76- setNodes ( ( ns ) => ns . map ( ( n ) => ( n . id === id ? { ...n , data : { ...n . data , state } } : n ) ) ) ;
77- const markEdge = ( id , state , to ) =>
78- setEdges ( ( es ) => es . map ( ( e ) => ( e . id === id ? { ...e , data : { ...e . data , state, travelTo : to ?? null } } : e ) ) ) ;
79- const clearMarks = ( ) => {
80- setNodes ( ( ns ) => ns . map ( ( n ) => ( { ...n , data : { ...n . data , state : 'normal' } } ) ) ) ;
81- setEdges ( ( es ) => es . map ( ( e ) => ( { ...e , data : { ...e . data , state : 'normal' , travelTo : null } } ) ) ) ;
82- } ;
83-
84- const applyAction = ( action ) => {
85- if ( action . type === 'markNode' ) markNode ( action . id , action . state ) ;
86- else if ( action . type === 'markEdge' ) markEdge ( action . id , action . state , action . to ) ;
87- else if ( action . type === 'clear' ) clearMarks ( ) ;
88- } ;
89-
90- const runActions = async ( actions ) => {
91- if ( ! actions . length || isRunningRef . current ) return ;
92- isRunningRef . current = true ;
93- setIsRunning ( true ) ;
94- clearMarks ( ) ;
95- for ( const action of actions ) {
96- applyAction ( action ) ;
97- await sleep ( speedRef . current ) ;
98- }
99- isRunningRef . current = false ;
100- setIsRunning ( false ) ;
101- } ;
102-
103- const handleVisualize = ( ) => {
104- const adj = adjacency ( edgesRef . current , directedRef . current ) ;
105- const planner = algorithmRef . current === 1 ? dfsActions : bfsActions ;
106- runActions ( planner ( adj , startIdRef . current , finishIdRef . current ) ) ;
107- } ;
108-
109- // --- editing ---
110- const addNodeAt = ( position ) => {
111- // Compute id/label outside the updater: both have side effects and the
112- // updater can run twice under React strict mode.
113- const id = newNodeId ( ) ;
114- labelRef . current += 1 ;
115- const label = String ( labelRef . current ) ;
116- setNodes ( ( ns ) => [
117- ...ns ,
118- { id, type : 'graphNode' , position, data : { label, state : 'normal' , role : null } } ,
119- ] ) ;
120- } ;
121-
122- const addEdge = ( a , b ) => {
123- if ( a === b ) return ;
124- const exists = edgesRef . current . some (
125- ( e ) =>
126- ( e . source === a && e . target === b ) ||
127- ( ! directedRef . current && e . source === b && e . target === a ) ,
128- ) ;
129- if ( exists ) return ;
130- setEdges ( ( es ) => [
131- ...es ,
132- {
133- id : edgeId ( a , b ) ,
134- source : a ,
135- target : b ,
136- type : 'floating' ,
137- data : { state : 'normal' } ,
138- markerEnd : directedRef . current ? ARROW : undefined ,
139- } ,
140- ] ) ;
141- } ;
142-
143- const setRole = ( id , role ) => {
144- if ( role === 'start' ) { startIdRef . current = id ; if ( finishIdRef . current === id ) finishIdRef . current = null ; }
145- if ( role === 'finish' ) { finishIdRef . current = id ; if ( startIdRef . current === id ) startIdRef . current = null ; }
146- setNodes ( ( ns ) =>
147- ns . map ( ( n ) => {
148- if ( n . id === id ) return { ...n , data : { ...n . data , role } } ;
149- if ( n . data . role === role ) return { ...n , data : { ...n . data , role : null } } ;
150- return n ;
151- } ) ,
152- ) ;
153- } ;
154-
155- const onPaneClick = ( event ) => {
156- if ( isRunningRef . current ) return ;
157- if ( modeRef . current === 'add-node' ) {
158- addNodeAt ( screenToFlowPosition ( { x : event . clientX , y : event . clientY } ) ) ;
159- setModeBoth ( 'idle' ) ;
160- }
161- pendingEdgeRef . current = null ;
162- } ;
163-
164- const onNodeClick = ( _event , node ) => {
165- if ( isRunningRef . current ) return ;
166- if ( modeRef . current === 'add-edge' ) {
167- if ( pendingEdgeRef . current == null ) {
168- pendingEdgeRef . current = node . id ;
169- } else {
170- addEdge ( pendingEdgeRef . current , node . id ) ;
171- pendingEdgeRef . current = null ;
172- setModeBoth ( 'idle' ) ;
173- }
174- }
175- } ;
12+ const g = useGraphEditor ( { initialPreset : PRESETS [ 0 ] } ) ;
13+ const [ algo , setAlgo ] = useState ( 0 ) ;
17614
177- const onNodesDelete = ( deleted ) => {
178- for ( const n of deleted ) {
179- if ( n . id === startIdRef . current ) startIdRef . current = null ;
180- if ( n . id === finishIdRef . current ) finishIdRef . current = null ;
181- }
182- } ;
183-
184- // global keyboard shortcuts
185- useEffect ( ( ) => {
186- const onKey = ( e ) => {
187- if ( isRunningRef . current ) return ;
188- const el = e . target ;
189- if ( el && el . closest && el . closest ( 'input,select,textarea,[role="combobox"],button' ) ) return ;
190-
191- const selected = nodesRef . current . find ( ( n ) => n . selected ) ;
192- const k = e . key . toLowerCase ( ) ;
193- if ( k === 'n' ) { setModeBoth ( 'add-node' ) ; pendingEdgeRef . current = null ; }
194- else if ( k === 'e' ) { setModeBoth ( 'add-edge' ) ; pendingEdgeRef . current = null ; }
195- else if ( k === 's' && selected ) setRole ( selected . id , 'start' ) ;
196- else if ( k === 'f' && selected ) setRole ( selected . id , 'finish' ) ;
197- else if ( e . key === 'Escape' ) { setModeBoth ( 'idle' ) ; pendingEdgeRef . current = null ; }
198- } ;
199- window . addEventListener ( 'keydown' , onKey ) ;
200- return ( ) => window . removeEventListener ( 'keydown' , onKey ) ;
201- // eslint-disable-next-line react-hooks/exhaustive-deps
202- } , [ ] ) ;
203-
204- // --- menu callbacks ---
205- const onDirectedChange = ( val ) => {
206- directedRef . current = val ;
207- setDirected ( val ) ;
208- setEdges ( ( es ) => es . map ( ( e ) => ( { ...e , markerEnd : val ? ARROW : undefined } ) ) ) ;
209- } ;
210- const onPresetChange = ( idx ) => {
211- const g = initialGraph ( idx ) ;
212- nodesRef . current = g . nodes ; edgesRef . current = g . edges ;
213- startIdRef . current = g . startId ; finishIdRef . current = null ;
214- labelRef . current = 0 ;
215- setNodes ( g . nodes ) ; setEdges ( g . edges ) ;
216- setModeBoth ( 'idle' ) ; pendingEdgeRef . current = null ;
217- } ;
218- const onClear = ( ) => {
219- if ( isRunningRef . current ) return ;
220- nodesRef . current = [ ] ; edgesRef . current = [ ] ;
221- startIdRef . current = null ; finishIdRef . current = null ;
222- labelRef . current = 0 ;
223- setNodes ( [ ] ) ; setEdges ( [ ] ) ;
224- setModeBoth ( 'idle' ) ; pendingEdgeRef . current = null ;
15+ const onVisualize = ( ) => {
16+ const { edges, directed, startId, finishId } = g . getContext ( ) ;
17+ const adj = adjacency ( edges , directed ) ;
18+ g . run ( algo === 1 ? dfsActions ( adj , startId , finishId ) : bfsActions ( adj , startId , finishId ) ) ;
22519 } ;
22620
22721 return (
22822 < div className = "flex flex-col h-screen" >
22923 < Navbar />
23024 < div className = "flex flex-1 overflow-hidden" >
231- < Menu
232- disabled = { isRunning }
233- onDirectedChange = { onDirectedChange }
234- onAlgorithmChange = { ( a ) => { algorithmRef . current = a ; } }
235- onPresetChange = { onPresetChange }
236- onSpeedChange = { ( s ) => { speedRef . current = toDelay ( s ) ; } }
237- onVisualize = { handleVisualize }
238- onClear = { onClear }
25+ < GraphMenu
26+ title = "Graph Traversal"
27+ algorithms = { [ 'BFS' , 'DFS' ] }
28+ presets = { PRESETS }
29+ disabled = { g . isRunning }
30+ onAlgorithmChange = { setAlgo }
31+ onDirectedChange = { g . setDirected }
32+ onPresetChange = { ( i ) => g . loadPreset ( PRESETS [ i ] ) }
33+ onSpeedChange = { g . setSpeed }
34+ onVisualize = { onVisualize }
35+ onClear = { g . clear }
23936 />
240- < div className = "relative flex-1" >
241- < div className = "absolute top-3 left-3 z-10 rounded-md bg-white/90 px-3 py-1.5 text-xs font-medium text-gray-700 shadow" >
242- { MODE_HINT [ mode ] }
243- </ div >
244- < ReactFlow
245- nodes = { nodes }
246- edges = { edges }
247- onNodesChange = { onNodesChange }
248- onEdgesChange = { onEdgesChange }
249- onNodesDelete = { onNodesDelete }
250- onPaneClick = { onPaneClick }
251- onNodeClick = { onNodeClick }
252- nodeTypes = { nodeTypes }
253- edgeTypes = { edgeTypes }
254- nodesConnectable = { false }
255- fitView
256- >
257- < Background />
258- < Controls />
259- </ ReactFlow >
260- </ div >
37+ < GraphCanvas editor = { g } />
26138 </ div >
26239 </ div >
26340 ) ;
@@ -270,7 +47,3 @@ export default function Graph() {
27047 </ ReactFlowProvider >
27148 ) ;
27249}
273-
274- function sleep ( ms ) {
275- return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
276- }
0 commit comments