zeroed-some/bashamole / 9709dd7

Browse files

refactor frontend stable

Authored by espadonne
SHA
9709dd7d62b2c1883aa4fcd48c8dc2b2d41bb706
Parents
1dac56e
Tree
e8c1bfd

2 changed files

StatusFile+-
M frontend/src/components/Game.tsx 93 73
M frontend/src/components/TreeVisualizer.tsx 191 94
frontend/src/components/Game.tsxmodified
@@ -29,6 +29,7 @@ const Game: React.FC = () => {
2929
   const [executing, setExecuting] = useState(false);
3030
   const [showHints, setShowHints] = useState(false);
3131
   const [hints, setHints] = useState<string[]>([]);
32
+  const [terminalMinimized, setTerminalMinimized] = useState(false);
3233
   
3334
   const terminalRef = useRef<HTMLDivElement>(null);
3435
   const inputRef = useRef<HTMLInputElement>(null);
@@ -58,6 +59,7 @@ const Game: React.FC = () => {
5859
       }]);
5960
       setHints([]);
6061
       setShowHints(false);
62
+      setTerminalMinimized(false);
6163
     } catch (error) {
6264
       setGameState({
6365
         ...gameState,
@@ -117,7 +119,6 @@ const Game: React.FC = () => {
117119
           tree: prev.tree ? {
118120
             ...prev.tree,
119121
             is_completed: true,
120
-            // Update tree_data to show mole
121122
             tree_data: updateTreeDataToShowMole(prev.tree!.tree_data, prev.tree!.player_location),
122123
           } : null,
123124
         }));
@@ -213,77 +214,91 @@ const Game: React.FC = () => {
213214
   }
214215
 
215216
   return (
216
-    <div className="min-h-screen bg-gray-900 text-white p-4">
217
-      <div className="max-w-7xl mx-auto">
218
-        {/* Header */}
219
-        <div className="bg-gray-800 rounded-lg shadow-xl p-6 mb-4">
220
-          <div className="flex justify-between items-center">
221
-            <div>
222
-              <h1 className="text-3xl font-bold mb-2">🐭 Bashamole</h1>
223
-              <p className="text-gray-400">
224
-                Current Location: <span className="font-mono text-blue-400">{gameState.tree.player_location}</span>
225
-              </p>
226
-            </div>
227
-            <div className="text-right">
228
-              {gameState.tree.is_completed ? (
229
-                <div className="text-green-400 font-bold text-xl animate-pulse">
230
-                  🎉 You found the mole!
231
-                </div>
232
-              ) : (
233
-                <div className="space-x-2">
234
-                  <button
235
-                    onClick={getHints}
236
-                    className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 transition"
237
-                  >
238
-                    Get Hint 💡
239
-                  </button>
240
-                  <button
241
-                    onClick={startNewGame}
242
-                    className="px-4 py-2 bg-gray-700 text-white rounded hover:bg-gray-600 transition"
243
-                  >
244
-                    New Game
245
-                  </button>
246
-                </div>
247
-              )}
217
+    <div className="relative min-h-screen bg-gray-900 text-white overflow-hidden">
218
+      {/* Tree Canvas - Full Screen Background */}
219
+      <div className="absolute inset-0 bg-gray-900">
220
+        <TreeVisualizer
221
+          treeData={gameState.tree.tree_data}
222
+          playerLocation={gameState.tree.player_location}
223
+          onNodeClick={handleNodeClick}
224
+        />
225
+      </div>
226
+
227
+      {/* Top Header Bar */}
228
+      <div className="absolute top-0 left-0 right-0 bg-gray-900/90 backdrop-blur-sm border-b border-gray-700 p-4 z-20">
229
+        <div className="max-w-7xl mx-auto flex justify-between items-center">
230
+          <div className="flex items-center gap-4">
231
+            <h1 className="text-2xl font-bold">🐭 Bashamole</h1>
232
+            <div className="text-sm text-gray-400">
233
+              Location: <span className="font-mono text-blue-400">{gameState.tree.player_location}</span>
248234
             </div>
249235
           </div>
236
+          
237
+          <div className="flex items-center gap-3">
238
+            {gameState.tree.is_completed ? (
239
+              <div className="text-green-400 font-bold animate-pulse">
240
+                🎉 You found the mole!
241
+              </div>
242
+            ) : (
243
+              <>
244
+                <button
245
+                  onClick={getHints}
246
+                  className="px-3 py-1.5 bg-yellow-600 text-white text-sm rounded hover:bg-yellow-700 transition"
247
+                >
248
+                  Get Hint 💡
249
+                </button>
250
+                <button
251
+                  onClick={startNewGame}
252
+                  className="px-3 py-1.5 bg-gray-700 text-white text-sm rounded hover:bg-gray-600 transition"
253
+                >
254
+                  New Game
255
+                </button>
256
+              </>
257
+            )}
258
+          </div>
250259
         </div>
260
+      </div>
251261
 
252
-        {/* Hints */}
253
-        {showHints && hints.length > 0 && (
254
-          <div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-4">
255
-            <h3 className="text-yellow-400 font-bold mb-2">💡 Hints:</h3>
256
-            {hints.map((hint, index) => (
257
-              <p key={index} className="text-yellow-200">{hint}</p>
258
-            ))}
259
-          </div>
260
-        )}
262
+      {/* Hints Popup */}
263
+      {showHints && hints.length > 0 && (
264
+        <div className="absolute top-20 left-1/2 transform -translate-x-1/2 bg-yellow-900/95 backdrop-blur-sm border border-yellow-600 rounded-lg p-4 max-w-md z-30 shadow-2xl">
265
+          <button
266
+            onClick={() => setShowHints(false)}
267
+            className="absolute top-2 right-2 text-yellow-400 hover:text-yellow-300"
268
+          >
269
+            ✕
270
+          </button>
271
+          <h3 className="text-yellow-400 font-bold mb-2">💡 Hints:</h3>
272
+          {hints.map((hint, index) => (
273
+            <p key={index} className="text-yellow-200 text-sm">{hint}</p>
274
+          ))}
275
+        </div>
276
+      )}
261277
 
262
-        <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
263
-          {/* Tree Visualizer */}
264
-          <div className="bg-gray-800 rounded-lg shadow-xl p-4">
265
-            <h2 className="text-xl font-semibold mb-3 text-gray-300">Filesystem Tree</h2>
266
-            <div className="h-[600px] bg-gray-900 rounded-lg p-2">
267
-              <TreeVisualizer
268
-                treeData={gameState.tree.tree_data}
269
-                playerLocation={gameState.tree.player_location}
270
-                onNodeClick={handleNodeClick}
271
-              />
272
-            </div>
273
-            <p className="text-sm text-gray-500 mt-2">
274
-              Click nodes to navigate • Scroll to zoom • Drag to pan
275
-            </p>
276
-          </div>
278
+      {/* Floating Terminal */}
279
+      <div className={`absolute bottom-4 left-4 bg-gray-800/95 backdrop-blur-sm rounded-lg shadow-2xl border border-gray-700 transition-all duration-300 z-30 ${
280
+        terminalMinimized ? 'w-80' : 'w-[500px]'
281
+      }`}>
282
+        {/* Terminal Header */}
283
+        <div className="flex items-center justify-between bg-gray-700 px-4 py-2 rounded-t-lg">
284
+          <h3 className="text-sm font-semibold text-gray-300">Terminal</h3>
285
+          <button
286
+            onClick={() => setTerminalMinimized(!terminalMinimized)}
287
+            className="text-gray-400 hover:text-white transition"
288
+          >
289
+            {terminalMinimized ? '▲' : '▼'}
290
+          </button>
291
+        </div>
277292
 
278
-          {/* Terminal */}
279
-          <div className="bg-gray-800 rounded-lg shadow-xl p-4">
280
-            <h2 className="text-xl font-semibold mb-3 text-gray-300">Terminal</h2>
293
+        {/* Terminal Content */}
294
+        {!terminalMinimized && (
295
+          <>
281296
             <div 
282297
               ref={terminalRef}
283
-              className="bg-black text-green-400 p-4 rounded font-mono text-sm h-[550px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700"
298
+              className="bg-black text-green-400 p-3 font-mono text-xs h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-700"
284299
             >
285300
               {commandHistory.map((entry, index) => (
286
-                <div key={index} className="mb-3">
301
+                <div key={index} className="mb-2">
287302
                   <div className="text-gray-400">
288303
                     {entry.command.startsWith('🎮') ? (
289304
                       <span className="text-yellow-400">{entry.command}</span>
@@ -300,34 +315,39 @@ const Game: React.FC = () => {
300315
               ))}
301316
             </div>
302317
             
303
-            <form onSubmit={handleSubmit} className="mt-4">
304
-              <div className="flex bg-gray-900 rounded overflow-hidden">
305
-                <span className="bg-gray-800 px-3 py-2 text-green-400 font-mono">$</span>
318
+            <form onSubmit={handleSubmit} className="border-t border-gray-700">
319
+              <div className="flex bg-gray-900">
320
+                <span className="bg-gray-800 px-3 py-2 text-green-400 font-mono text-sm">$</span>
306321
                 <input
307322
                   ref={inputRef}
308323
                   type="text"
309324
                   value={command}
310325
                   onChange={(e) => setCommand(e.target.value)}
311326
                   disabled={executing || gameState.tree.is_completed}
312
-                  className="flex-1 px-3 py-2 bg-gray-900 text-green-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono placeholder-gray-600"
313
-                  placeholder="Enter command (cd, ls, pwd, help, killall moles)"
327
+                  className="flex-1 px-3 py-2 bg-gray-900 text-green-400 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 font-mono placeholder-gray-600"
328
+                  placeholder="cd, ls, pwd, help, killall moles"
314329
                   autoFocus
315330
                 />
316331
                 <button
317332
                   type="submit"
318333
                   disabled={executing || gameState.tree.is_completed}
319
-                  className="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 transition"
334
+                  className="px-4 py-2 bg-blue-600 text-white text-sm hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 transition"
320335
                 >
321336
                   {executing ? '...' : 'Run'}
322337
                 </button>
323338
               </div>
324339
             </form>
340
+          </>
341
+        )}
342
+      </div>
325343
 
326
-            <div className="mt-2 text-xs text-gray-600">
327
-              Pro tip: Type "help" to see all available commands
328
-            </div>
329
-          </div>
330
-        </div>
344
+      {/* Instructions - Bottom Right */}
345
+      <div className="absolute bottom-4 right-4 bg-gray-800/80 backdrop-blur-sm rounded-lg p-3 text-xs text-gray-400 max-w-xs z-20">
346
+        <div className="font-semibold text-gray-300 mb-1">Controls:</div>
347
+        <div>• Click nodes to navigate</div>
348
+        <div>• Scroll to zoom, drag to pan</div>
349
+        <div>• Type commands in terminal</div>
350
+        <div className="mt-1 text-gray-500">Find and eliminate the mole!</div>
331351
       </div>
332352
     </div>
333353
   );
frontend/src/components/TreeVisualizer.tsxmodified
@@ -17,65 +17,113 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
1717
   onNodeClick,
1818
 }) => {
1919
   const svgRef = useRef<SVGSVGElement>(null);
20
+  const containerRef = useRef<HTMLDivElement>(null);
2021
 
2122
   useEffect(() => {
22
-    if (!treeData || !svgRef.current) return;
23
+    if (!treeData || !svgRef.current || !containerRef.current) return;
2324
 
2425
     // Clear previous render
2526
     d3.select(svgRef.current).selectAll('*').remove();
2627
 
27
-    const width = 1200;
28
-    const height = 800;
29
-    const margin = { top: 40, right: 120, bottom: 40, left: 120 };
28
+    // Get container dimensions
29
+    const containerWidth = containerRef.current.clientWidth;
30
+    const containerHeight = containerRef.current.clientHeight;
31
+
32
+    // Set up dimensions with dynamic sizing
33
+    const margin = { top: 100, right: 50, bottom: 100, left: 50 };
34
+    const width = Math.max(containerWidth, 1600);
35
+    const height = Math.max(containerHeight, 1200);
3036
 
3137
     const svg = d3
3238
       .select(svgRef.current)
39
+      .attr('width', containerWidth)
40
+      .attr('height', containerHeight)
3341
       .attr('viewBox', `0 0 ${width} ${height}`)
42
+      .attr('preserveAspectRatio', 'xMidYMid meet');
43
+
44
+    // Add a subtle grid pattern background
45
+    const defs = svg.append('defs');
46
+    
47
+    const pattern = defs.append('pattern')
48
+      .attr('id', 'grid')
49
+      .attr('width', 40)
50
+      .attr('height', 40)
51
+      .attr('patternUnits', 'userSpaceOnUse');
52
+    
53
+    pattern.append('path')
54
+      .attr('d', 'M 40 0 L 0 0 0 40')
55
+      .attr('fill', 'none')
56
+      .attr('stroke', '#1f2937')
57
+      .attr('stroke-width', '1');
58
+
59
+    svg.append('rect')
60
+      .attr('width', '100%')
61
+      .attr('height', '100%')
62
+      .attr('fill', '#111827')
63
+      .style('opacity', 0.95);
64
+    
65
+    svg.append('rect')
3466
       .attr('width', '100%')
35
-      .attr('height', '100%');
67
+      .attr('height', '100%')
68
+      .attr('fill', 'url(#grid)')
69
+      .style('opacity', 0.3);
3670
 
3771
     const g = svg
3872
       .append('g')
39
-      .attr('transform', `translate(${margin.left},${margin.top})`);
73
+      .attr('transform', `translate(${width / 2},${margin.top})`);
4074
 
41
-    // Create tree layout
75
+    // Create tree layout - vertical orientation
4276
     const treeLayout = d3
4377
       .tree<TreeNode>()
44
-      .size([height - margin.top - margin.bottom, width - margin.left - margin.right])
45
-      .separation((a, b) => (a.parent === b.parent ? 1 : 1.5));
78
+      .size([width - margin.left - margin.right, height - margin.top - margin.bottom])
79
+      .separation((a, b) => {
80
+        const aIsLeaf = !a.children || a.children.length === 0;
81
+        const bIsLeaf = !b.children || b.children.length === 0;
82
+        
83
+        if (aIsLeaf && bIsLeaf) {
84
+          return 1.5;
85
+        }
86
+        return a.parent === b.parent ? 1 : 1.2;
87
+      });
4688
 
4789
     // Create hierarchy
4890
     const root = d3.hierarchy(treeData);
4991
     const treeNodes = treeLayout(root);
5092
 
5193
     // Create gradient for links
52
-    const gradient = svg.append('defs')
94
+    const linkGradient = defs
5395
       .append('linearGradient')
54
-      .attr('id', 'link-gradient')
55
-      .attr('gradientUnits', 'userSpaceOnUse');
96
+      .attr('id', 'link-gradient-v')
97
+      .attr('gradientUnits', 'userSpaceOnUse')
98
+      .attr('x1', '0%')
99
+      .attr('y1', '0%')
100
+      .attr('x2', '0%')
101
+      .attr('y2', '100%');
56102
     
57
-    gradient.append('stop')
103
+    linkGradient.append('stop')
58104
       .attr('offset', '0%')
59
-      .attr('stop-color', '#E5E7EB');
105
+      .attr('stop-color', '#4B5563')
106
+      .attr('stop-opacity', 0.6);
60107
     
61
-    gradient.append('stop')
108
+    linkGradient.append('stop')
62109
       .attr('offset', '100%')
63
-      .attr('stop-color', '#9CA3AF');
110
+      .attr('stop-color', '#6B7280')
111
+      .attr('stop-opacity', 0.3);
64112
 
65
-    // Create links with curved paths
113
+    // Create links with vertical layout
66114
     const link = g
67115
       .selectAll('.link')
68116
       .data(treeNodes.links())
69117
       .enter()
70118
       .append('path')
71119
       .attr('class', 'link')
72
-      .attr('d', d3.linkHorizontal<any, any>()
73
-        .x(d => d.y)
74
-        .y(d => d.x))
120
+      .attr('d', d3.linkVertical<any, any>()
121
+        .x(d => d.x)
122
+        .y(d => d.y))
75123
       .style('fill', 'none')
76
-      .style('stroke', 'url(#link-gradient)')
124
+      .style('stroke', 'url(#link-gradient-v)')
77125
       .style('stroke-width', 2)
78
-      .style('opacity', 0.6);
126
+      .style('opacity', 0.8);
79127
 
80128
     // Create node groups
81129
     const node = g
@@ -84,41 +132,56 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
84132
       .enter()
85133
       .append('g')
86134
       .attr('class', 'node')
87
-      .attr('transform', d => `translate(${d.y},${d.x})`);
135
+      .attr('transform', d => `translate(${d.x},${d.y})`);
136
+
137
+    // Add glow effect for interactive nodes
138
+    const glowFilter = defs.append('filter')
139
+      .attr('id', 'glow');
140
+    
141
+    glowFilter.append('feGaussianBlur')
142
+      .attr('stdDeviation', '3')
143
+      .attr('result', 'coloredBlur');
144
+    
145
+    const feMerge = glowFilter.append('feMerge');
146
+    feMerge.append('feMergeNode').attr('in', 'coloredBlur');
147
+    feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
88148
 
89
-    // Add circles for nodes with better styling
149
+    // Add circles for nodes
90150
     node
91151
       .append('circle')
92152
       .attr('r', d => {
93
-        if (d.data.path === '/') return 12; // Root is larger
94
-        if (d.data.path === playerLocation) return 10;
95
-        return 8;
153
+        if (d.data.path === '/') return 14;
154
+        if (d.data.path === playerLocation) return 11;
155
+        return 9;
96156
       })
97157
       .style('fill', d => {
98
-        if (d.data.path === playerLocation) return '#3B82F6'; // Player location - blue
99
-        if (d.data.has_mole) return '#EF4444'; // Mole location - red (only shown after win)
100
-        if (d.data.is_fhs) return '#8B5CF6'; // FHS standard - purple
101
-        return '#10B981'; // Generated directories - green
158
+        if (d.data.path === playerLocation) return '#3B82F6';
159
+        if (d.data.has_mole) return '#EF4444';
160
+        if (d.data.is_fhs) return '#8B5CF6';
161
+        return '#10B981';
102162
       })
103163
       .style('stroke', d => {
104
-        if (d.data.path === playerLocation) return '#1E40AF';
105
-        if (d.data.has_mole) return '#991B1B';
164
+        if (d.data.path === playerLocation) return '#60A5FA';
165
+        if (d.data.has_mole) return '#F87171';
106166
         return '#ffffff';
107167
       })
108168
       .style('stroke-width', 2)
109169
       .style('cursor', 'pointer')
110
-      .style('filter', d => d.data.path === playerLocation ? 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.5))' : 'none')
170
+      .style('filter', d => d.data.path === playerLocation ? 'url(#glow)' : 'none')
171
+      .style('transition', 'all 0.3s ease')
111172
       .on('mouseover', function(event, d) {
112173
         d3.select(this)
113174
           .transition()
114175
           .duration(200)
115
-          .attr('r', d.data.path === '/' ? 14 : 10);
176
+          .attr('r', d.data.path === '/' ? 16 : 12)
177
+          .style('stroke-width', 3);
116178
       })
117179
       .on('mouseout', function(event, d) {
118180
         d3.select(this)
119181
           .transition()
120182
           .duration(200)
121
-          .attr('r', d.data.path === '/' ? 12 : d.data.path === playerLocation ? 10 : 8);
183
+          .attr('r', d.data.path === '/' ? 14 : d.data.path === playerLocation ? 11 : 9)
184
+          .style('stroke-width', 2);
122185
       })
123186
       .on('click', (event, d) => {
124187
         if (onNodeClick) {
@@ -131,96 +194,130 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
131194
       .append('title')
132195
       .text(d => `${d.data.path}\n${d.data.description}\n${d.data.has_mole ? '🐭 Mole is here!' : ''}`);
133196
 
134
-    // Add labels with better positioning
197
+    // Add labels
135198
     node
136199
       .append('text')
137
-      .attr('dy', '.35em')
138
-      .attr('x', d => d.children ? -13 : 13)
139
-      .style('text-anchor', d => d.children ? 'end' : 'start')
140
-      .style('font-size', '13px')
200
+      .attr('dy', d => d.children ? -20 : 25)
201
+      .attr('text-anchor', 'middle')
202
+      .style('font-size', '12px')
141203
       .style('font-weight', d => d.data.path === playerLocation ? '600' : '400')
142
-      .style('fill', d => d.data.path === playerLocation ? '#1E40AF' : '#374151')
204
+      .style('fill', d => d.data.path === playerLocation ? '#93C5FD' : '#E5E7EB')
205
+      .style('text-shadow', '0 0 4px rgba(0,0,0,0.8)')
143206
       .text(d => d.data.name || '/')
144207
       .style('pointer-events', 'none');
145208
 
146
-    // Add player indicator emoji
209
+    // Add player indicator with SVG
147210
     const playerNode = treeNodes.descendants().find(d => d.data.path === playerLocation);
148211
     if (playerNode) {
149
-      node
212
+      const playerGroup = node
150213
         .filter(d => d.data.path === playerLocation)
151
-        .append('text')
152
-        .attr('dy', -20)
153
-        .attr('text-anchor', 'middle')
154
-        .style('font-size', '20px')
155
-        .text('🧑‍💻');
214
+        .append('g')
215
+        .attr('transform', 'translate(0, -30)');
216
+
217
+      // Add pulsing animation
218
+      playerGroup
219
+        .append('circle')
220
+        .attr('r', 15)
221
+        .style('fill', 'none')
222
+        .style('stroke', '#3B82F6')
223
+        .style('stroke-width', 2)
224
+        .style('opacity', 0)
225
+        .append('animate')
226
+        .attr('attributeName', 'r')
227
+        .attr('from', '15')
228
+        .attr('to', '25')
229
+        .attr('dur', '2s')
230
+        .attr('repeatCount', 'indefinite');
231
+
232
+      playerGroup
233
+        .select('circle')
234
+        .append('animate')
235
+        .attr('attributeName', 'opacity')
236
+        .attr('from', '0.8')
237
+        .attr('to', '0')
238
+        .attr('dur', '2s')
239
+        .attr('repeatCount', 'indefinite');
240
+
241
+      // Add player icon
242
+      playerGroup
243
+        .append('image')
244
+        .attr('xlink:href', '/player.svg')
245
+        .attr('width', 24)
246
+        .attr('height', 24)
247
+        .attr('x', -12)
248
+        .attr('y', -12);
156249
     }
157250
 
158
-    // Add mole indicator if game is won
251
+    // Add mole indicator with SVG if game is won
159252
     const moleNode = treeNodes.descendants().find(d => d.data.has_mole);
160253
     if (moleNode) {
161
-      node
254
+      const moleGroup = node
162255
         .filter(d => d.data.has_mole)
163
-        .append('text')
164
-        .attr('dy', -20)
165
-        .attr('text-anchor', 'middle')
166
-        .style('font-size', '20px')
167
-        .text('🐭');
256
+        .append('g')
257
+        .attr('transform', 'translate(0, -30)');
258
+
259
+      // Add celebration animation
260
+      moleGroup
261
+        .append('circle')
262
+        .attr('r', 15)
263
+        .style('fill', 'none')
264
+        .style('stroke', '#EF4444')
265
+        .style('stroke-width', 3)
266
+        .style('opacity', 0)
267
+        .append('animate')
268
+        .attr('attributeName', 'r')
269
+        .attr('from', '15')
270
+        .attr('to', '30')
271
+        .attr('dur', '1s')
272
+        .attr('repeatCount', 'indefinite');
273
+
274
+      moleGroup
275
+        .select('circle')
276
+        .append('animate')
277
+        .attr('attributeName', 'opacity')
278
+        .attr('from', '1')
279
+        .attr('to', '0')
280
+        .attr('dur', '1s')
281
+        .attr('repeatCount', 'indefinite');
282
+
283
+      // Add mole icon
284
+      moleGroup
285
+        .append('image')
286
+        .attr('xlink:href', '/mole.svg')
287
+        .attr('width', 24)
288
+        .attr('height', 24)
289
+        .attr('x', -12)
290
+        .attr('y', -12);
168291
     }
169292
 
170293
     // Add zoom and pan behavior
171294
     const zoom = d3.zoom<SVGSVGElement, unknown>()
172
-      .scaleExtent([0.3, 3])
295
+      .scaleExtent([0.3, 2])
173296
       .on('zoom', (event) => {
174297
         g.attr('transform', event.transform);
175298
       });
176299
 
177300
     svg.call(zoom);
178301
 
179
-    // Center on player location initially
302
+    // Center on player location with animation
180303
     if (playerNode) {
181304
       const scale = 0.8;
182
-      const x = width / 2 - playerNode.y * scale;
183
-      const y = height / 2 - playerNode.x * scale;
305
+      const x = width / 2 - playerNode.x * scale;
306
+      const y = containerHeight / 2 - playerNode.y * scale - margin.top;
184307
       
185
-      svg.call(
186
-        zoom.transform,
187
-        d3.zoomIdentity.translate(x, y).scale(scale)
188
-      );
308
+      svg
309
+        .transition()
310
+        .duration(750)
311
+        .call(
312
+          zoom.transform as any,
313
+          d3.zoomIdentity.translate(x, y).scale(scale)
314
+        );
189315
     }
190316
 
191
-    // Add legend
192
-    const legend = svg.append('g')
193
-      .attr('transform', `translate(20, ${height - 100})`);
194
-
195
-    const legendItems = [
196
-      { color: '#3B82F6', label: 'You are here' },
197
-      { color: '#8B5CF6', label: 'System (FHS)' },
198
-      { color: '#10B981', label: 'User directories' },
199
-      { color: '#EF4444', label: 'Mole location', show: !!moleNode },
200
-    ];
201
-
202
-    legendItems.forEach((item, i) => {
203
-      if (item.show === false) return;
204
-      
205
-      const legendItem = legend.append('g')
206
-        .attr('transform', `translate(0, ${i * 25})`);
207
-      
208
-      legendItem.append('circle')
209
-        .attr('r', 6)
210
-        .style('fill', item.color);
211
-      
212
-      legendItem.append('text')
213
-        .attr('x', 15)
214
-        .attr('y', 5)
215
-        .style('font-size', '12px')
216
-        .style('fill', '#6B7280')
217
-        .text(item.label);
218
-    });
219
-
220317
   }, [treeData, playerLocation, onNodeClick]);
221318
 
222319
   return (
223
-    <div className="w-full h-full bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg shadow-inner overflow-hidden">
320
+    <div ref={containerRef} className="w-full h-full">
224321
       <svg ref={svgRef} className="w-full h-full" />
225322
     </div>
226323
   );