more-compute 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. frontend/.DS_Store +0 -0
  2. frontend/.gitignore +41 -0
  3. frontend/README.md +36 -0
  4. frontend/__init__.py +1 -0
  5. frontend/app/favicon.ico +0 -0
  6. frontend/app/globals.css +1537 -0
  7. frontend/app/layout.tsx +173 -0
  8. frontend/app/page.tsx +11 -0
  9. frontend/components/AddCellButton.tsx +42 -0
  10. frontend/components/Cell.tsx +244 -0
  11. frontend/components/CellButton.tsx +58 -0
  12. frontend/components/CellOutput.tsx +70 -0
  13. frontend/components/ErrorDisplay.tsx +208 -0
  14. frontend/components/ErrorModal.tsx +154 -0
  15. frontend/components/MarkdownRenderer.tsx +84 -0
  16. frontend/components/Notebook.tsx +520 -0
  17. frontend/components/Sidebar.tsx +46 -0
  18. frontend/components/popups/ComputePopup.tsx +879 -0
  19. frontend/components/popups/FilterPopup.tsx +427 -0
  20. frontend/components/popups/FolderPopup.tsx +171 -0
  21. frontend/components/popups/MetricsPopup.tsx +168 -0
  22. frontend/components/popups/PackagesPopup.tsx +112 -0
  23. frontend/components/popups/PythonPopup.tsx +292 -0
  24. frontend/components/popups/SettingsPopup.tsx +68 -0
  25. frontend/eslint.config.mjs +25 -0
  26. frontend/lib/api.ts +469 -0
  27. frontend/lib/settings.ts +87 -0
  28. frontend/lib/websocket-native.ts +202 -0
  29. frontend/lib/websocket.ts +134 -0
  30. frontend/next-env.d.ts +6 -0
  31. frontend/next.config.mjs +17 -0
  32. frontend/next.config.ts +7 -0
  33. frontend/package-lock.json +5676 -0
  34. frontend/package.json +41 -0
  35. frontend/postcss.config.mjs +5 -0
  36. frontend/public/assets/icons/add.svg +1 -0
  37. frontend/public/assets/icons/check.svg +1 -0
  38. frontend/public/assets/icons/copy.svg +1 -0
  39. frontend/public/assets/icons/folder.svg +1 -0
  40. frontend/public/assets/icons/metric.svg +1 -0
  41. frontend/public/assets/icons/packages.svg +1 -0
  42. frontend/public/assets/icons/play.svg +1 -0
  43. frontend/public/assets/icons/python.svg +265 -0
  44. frontend/public/assets/icons/setting.svg +1 -0
  45. frontend/public/assets/icons/stop.svg +1 -0
  46. frontend/public/assets/icons/trash.svg +1 -0
  47. frontend/public/assets/icons/up-down.svg +1 -0
  48. frontend/public/assets/icons/x.svg +1 -0
  49. frontend/public/file.svg +1 -0
  50. frontend/public/fonts/Fira.ttf +0 -0
  51. frontend/public/fonts/Tiempos.woff2 +0 -0
  52. frontend/public/fonts/VeraMono.ttf +0 -0
  53. frontend/public/globe.svg +1 -0
  54. frontend/public/next.svg +1 -0
  55. frontend/public/vercel.svg +1 -0
  56. frontend/public/window.svg +1 -0
  57. frontend/tailwind.config.ts +29 -0
  58. frontend/tsconfig.json +27 -0
  59. frontend/types/notebook.ts +58 -0
  60. kernel_run.py +7 -0
  61. {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/METADATA +1 -1
  62. more_compute-0.1.4.dist-info/RECORD +86 -0
  63. {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/top_level.txt +1 -0
  64. morecompute/__version__.py +1 -0
  65. more_compute-0.1.2.dist-info/RECORD +0 -26
  66. {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/WHEEL +0 -0
  67. {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/entry_points.txt +0 -0
  68. {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,520 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useRef, useCallback, useReducer } from 'react';
4
+ import { Cell as CellComponent } from './Cell';
5
+ import { Cell, Output, StreamOutput, ExecuteResultOutput, ErrorOutput } from '@/types/notebook';
6
+ import { WebSocketService } from '@/lib/websocket-native';
7
+ import AddCellButton from './AddCellButton';
8
+ import { loadSettings, applyTheme } from '@/lib/settings';
9
+
10
+ // --- State Management with useReducer ---
11
+
12
+ interface NotebookState {
13
+ cells: Cell[];
14
+ executingCells: Set<number>;
15
+ }
16
+
17
+ type NotebookAction =
18
+ | { type: 'NOTEBOOK_LOADED'; payload: { cells: Cell[] } }
19
+ | { type: 'EXECUTION_START'; payload: { cell_index: number; execution_count: number } }
20
+ | { type: 'STREAM_OUTPUT'; payload: { cell_index: number; stream: 'stdout' | 'stderr'; text: string } }
21
+ | { type: 'EXECUTE_RESULT'; payload: { cell_index: number; execution_count: number; data: ExecuteResultOutput['data'] } }
22
+ | { type: 'EXECUTION_COMPLETE'; payload: { cell_index: number; result: any } }
23
+ | { type: 'EXECUTION_ERROR'; payload: { cell_index: number; error: Output } }
24
+ | { type: 'NOTEBOOK_UPDATED'; payload: { cells: Cell[] } }
25
+ | { type: 'UPDATE_CELL_SOURCE'; payload: { cell_index: number; source: string } }
26
+ | { type: 'RESET_KERNEL' };
27
+
28
+ const initialState: NotebookState = {
29
+ cells: [],
30
+ executingCells: new Set(),
31
+ };
32
+
33
+ const initialSettings = loadSettings();
34
+ applyTheme(initialSettings.theme);
35
+
36
+ const coerceToString = (value: unknown): string => {
37
+ if (Array.isArray(value)) {
38
+ return value.join('');
39
+ }
40
+ if (typeof value === 'string') {
41
+ return value;
42
+ }
43
+ return value != null ? String(value) : '';
44
+ };
45
+
46
+ const normalizeError = (error: any): ErrorOutput | undefined => {
47
+ if (!error || typeof error !== 'object') {
48
+ return undefined;
49
+ }
50
+
51
+ const tracebackRaw = (error as any).traceback;
52
+ const suggestionsRaw = (error as any).suggestions;
53
+
54
+ const traceback = Array.isArray(tracebackRaw)
55
+ ? tracebackRaw.map(coerceToString)
56
+ : coerceToString(tracebackRaw).split('\n');
57
+
58
+ const suggestions = Array.isArray(suggestionsRaw)
59
+ ? suggestionsRaw.map(coerceToString)
60
+ : undefined;
61
+
62
+ return {
63
+ output_type: 'error',
64
+ ename: coerceToString((error as any).ename ?? 'Error'),
65
+ evalue: coerceToString((error as any).evalue ?? ''),
66
+ traceback,
67
+ error_type: (error as any).error_type,
68
+ suggestions,
69
+ };
70
+ };
71
+
72
+ const normalizeOutputs = (outputs: any): Output[] => {
73
+ if (!Array.isArray(outputs)) {
74
+ return [];
75
+ }
76
+
77
+ return outputs.map((output) => {
78
+ if (!output || typeof output !== 'object') {
79
+ return output as Output;
80
+ }
81
+
82
+ if (output.output_type === 'stream') {
83
+ const text = coerceToString((output as any).text ?? '');
84
+ return { ...output, text } as StreamOutput;
85
+ }
86
+
87
+ if (output.output_type === 'execute_result') {
88
+ const data = { ...(output as any).data };
89
+ if (data && typeof data === 'object') {
90
+ const plain = coerceToString((data as any)['text/plain'] ?? '');
91
+ data['text/plain'] = plain;
92
+ }
93
+ return {
94
+ ...output,
95
+ data,
96
+ } as ExecuteResultOutput;
97
+ }
98
+
99
+ if (output.output_type === 'error') {
100
+ return normalizeError(output) as ErrorOutput;
101
+ }
102
+
103
+ return output as Output;
104
+ });
105
+ };
106
+
107
+ const normalizeCell = (cell: any, index: number): Cell => {
108
+ const source = coerceToString(cell?.source ?? '');
109
+ const outputs = normalizeOutputs(cell?.outputs);
110
+ const error = normalizeError(cell?.error);
111
+ return {
112
+ id: typeof cell?.id === 'string' && cell.id ? cell.id : `cell-${index}`,
113
+ cell_type: cell?.cell_type || 'code',
114
+ source,
115
+ outputs,
116
+ metadata: cell?.metadata || {},
117
+ execution_count: cell?.execution_count ?? null,
118
+ execution_time: cell?.execution_time,
119
+ error,
120
+ } as Cell;
121
+ };
122
+
123
+ function notebookReducer(state: NotebookState, action: NotebookAction): NotebookState {
124
+ switch (action.type) {
125
+ case 'NOTEBOOK_LOADED':
126
+ case 'NOTEBOOK_UPDATED':
127
+ return {
128
+ ...state,
129
+ cells: action.payload.cells.map((cell, index) => normalizeCell(cell, index)),
130
+ };
131
+
132
+ case 'EXECUTION_START': {
133
+ const newExecuting = new Set(state.executingCells);
134
+ newExecuting.add(action.payload.cell_index);
135
+ return {
136
+ ...state,
137
+ executingCells: newExecuting,
138
+ cells: state.cells.map((cell, i) =>
139
+ i === action.payload.cell_index
140
+ ? { ...cell, outputs: [], error: null, execution_count: action.payload.execution_count }
141
+ : cell
142
+ ),
143
+ };
144
+ }
145
+
146
+ case 'STREAM_OUTPUT': {
147
+ const { cell_index, stream, text } = action.payload;
148
+ return {
149
+ ...state,
150
+ cells: state.cells.map((cell, i) => {
151
+ if (i !== cell_index) return cell;
152
+
153
+ const outputs = [...(cell.outputs || [])];
154
+ const streamIndex = outputs.findIndex(
155
+ (o) => o.output_type === 'stream' && o.name === stream
156
+ );
157
+
158
+ if (streamIndex > -1) {
159
+ const streamOutput = outputs[streamIndex] as StreamOutput;
160
+ outputs[streamIndex] = {
161
+ ...streamOutput,
162
+ text: streamOutput.text + text,
163
+ };
164
+ } else {
165
+ outputs.push({ output_type: 'stream', name: stream, text } as StreamOutput);
166
+ }
167
+ return { ...cell, outputs };
168
+ }),
169
+ };
170
+ }
171
+
172
+ case 'EXECUTE_RESULT': {
173
+ const { cell_index, execution_count, data } = action.payload;
174
+ return {
175
+ ...state,
176
+ cells: state.cells.map((cell, i) => {
177
+ if (i !== cell_index) return cell;
178
+ const outputs = [...(cell.outputs || [])];
179
+
180
+ // Check if this is display data (e.g., matplotlib image)
181
+ const hasImageData = (data as any)?.['image/png'] || (data as any)?.['image/jpeg'] || (data as any)?.['image/svg+xml'];
182
+
183
+ if (hasImageData) {
184
+ // Create display_data output for images (matplotlib plots)
185
+ outputs.push({
186
+ output_type: 'display_data',
187
+ data: data || {},
188
+ } as any);
189
+ } else {
190
+ // Create execute_result output for text results
191
+ outputs.push({
192
+ output_type: 'execute_result',
193
+ execution_count,
194
+ data: {
195
+ ...(data || {}),
196
+ 'text/plain': coerceToString(data?.['text/plain'] ?? ''),
197
+ },
198
+ } as ExecuteResultOutput);
199
+ }
200
+
201
+ return { ...cell, outputs, execution_count };
202
+ }),
203
+ };
204
+ }
205
+
206
+ case 'EXECUTION_COMPLETE': {
207
+ const payload = action.payload || {};
208
+ const cell_index = payload.cell_index;
209
+ // Support both shapes: { result: {...} } and flat payload {...}
210
+ const result = (payload && payload.result)
211
+ ? payload.result
212
+ : payload || {};
213
+ const newExecuting = new Set(state.executingCells);
214
+ newExecuting.delete(cell_index);
215
+ return {
216
+ ...state,
217
+ executingCells: newExecuting,
218
+ cells: state.cells.map((cell, i) => {
219
+ if (i !== cell_index) return cell;
220
+ const normalizedOutputs = normalizeOutputs(Array.isArray((result as any).outputs) ? (result as any).outputs : []);
221
+ const normalizedError = normalizeError(result?.error);
222
+
223
+ const existingOutputs = cell.outputs || [];
224
+ const finalNonStream = normalizedOutputs.filter(o => (o as any).output_type !== 'stream');
225
+ return {
226
+ ...cell,
227
+ ...result,
228
+ outputs: [...existingOutputs, ...finalNonStream],
229
+ execution_count: result?.execution_count ?? cell.execution_count,
230
+ execution_time: result?.execution_time ?? cell.execution_time,
231
+ error: normalizedError,
232
+ };
233
+ }),
234
+ };
235
+ }
236
+
237
+ case 'EXECUTION_ERROR': {
238
+ const { cell_index, error } = action.payload;
239
+ const newExecuting = new Set(state.executingCells);
240
+ newExecuting.delete(cell_index);
241
+ const normalizedError = normalizeError(error);
242
+ return {
243
+ ...state,
244
+ executingCells: newExecuting,
245
+ cells: state.cells.map((cell, i) =>
246
+ i === cell_index
247
+ ? {
248
+ ...cell,
249
+ error: normalizedError,
250
+ outputs: normalizedError
251
+ ? [...(cell.outputs || []), normalizedError]
252
+ : cell.outputs || [],
253
+ }
254
+ : cell
255
+ ),
256
+ };
257
+ }
258
+
259
+ case 'UPDATE_CELL_SOURCE': {
260
+ const { cell_index, source } = action.payload;
261
+ return {
262
+ ...state,
263
+ cells: state.cells.map((cell, i) =>
264
+ i === cell_index ? { ...cell, source } : cell
265
+ ),
266
+ };
267
+ }
268
+
269
+ case 'RESET_KERNEL':
270
+ return {
271
+ ...state,
272
+ executingCells: new Set(),
273
+ cells: state.cells.map(cell => ({ ...cell, outputs: [], execution_count: null })),
274
+ };
275
+
276
+ default:
277
+ return state;
278
+ }
279
+ }
280
+
281
+ // --- Notebook Component ---
282
+
283
+ interface NotebookProps {
284
+ notebookName?: string;
285
+ }
286
+
287
+ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' }) => {
288
+ const [state, dispatch] = useReducer(notebookReducer, initialState);
289
+ const { cells, executingCells } = state;
290
+
291
+ const [currentCellIndex, setCurrentCellIndex] = useState<number | null>(null);
292
+ const [kernelStatus, setKernelStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
293
+ const wsRef = useRef<WebSocketService | null>(null);
294
+
295
+ useEffect(() => {
296
+ const body = document.body;
297
+ const pathAttr = body.getAttribute('data-notebook-path');
298
+ if (pathAttr) {
299
+ document.title = `MoreCompute – ${pathAttr}`;
300
+ }
301
+ }, []);
302
+
303
+ // --- Event Handlers ---
304
+
305
+ const handleNotebookLoaded = useCallback((data: any) => {
306
+ dispatch({ type: 'NOTEBOOK_LOADED', payload: data });
307
+ }, []);
308
+
309
+ const handleExecutionStart = useCallback((data: any) => {
310
+ dispatch({ type: 'EXECUTION_START', payload: data });
311
+ }, []);
312
+
313
+ const handleStreamOutput = useCallback((data: any) => {
314
+ dispatch({ type: 'STREAM_OUTPUT', payload: data });
315
+ }, []);
316
+
317
+ const handleExecutionComplete = useCallback((data: any) => {
318
+ dispatch({ type: 'EXECUTION_COMPLETE', payload: data });
319
+ }, []);
320
+
321
+ const handleExecuteResult = useCallback((data: any) => {
322
+ dispatch({
323
+ type: 'EXECUTE_RESULT',
324
+ payload: {
325
+ cell_index: data.cell_index,
326
+ execution_count: data.execution_count,
327
+ data: data.data || {},
328
+ },
329
+ });
330
+ }, []);
331
+
332
+ const handleExecutionError = useCallback((data: any) => {
333
+ dispatch({ type: 'EXECUTION_ERROR', payload: data });
334
+ }, []);
335
+
336
+ const handleNotebookUpdate = useCallback((data: any) => {
337
+ dispatch({ type: 'NOTEBOOK_UPDATED', payload: data });
338
+ }, []);
339
+
340
+ const handleKernelStatusUpdate = useCallback((status: 'connecting' | 'connected' | 'disconnected') => {
341
+ setKernelStatus(status);
342
+ const dot = document.getElementById("kernel-status-dot");
343
+ const text = document.getElementById("kernel-status-text");
344
+
345
+ if (dot && text) {
346
+ dot.className = 'status-dot'; // Reset classes
347
+ switch (status) {
348
+ case "connecting":
349
+ dot.classList.add("connecting");
350
+ text.textContent = "Connecting...";
351
+ break;
352
+ case "connected":
353
+ dot.classList.add("connected");
354
+ text.textContent = "Kernel Ready";
355
+ break;
356
+ case "disconnected":
357
+ dot.classList.add("disconnected");
358
+ text.textContent = "Kernel Disconnected";
359
+ break;
360
+ }
361
+ }
362
+ }, []);
363
+
364
+ useEffect(() => {
365
+ const ws = new WebSocketService();
366
+ wsRef.current = ws;
367
+ handleKernelStatusUpdate('connecting');
368
+
369
+ ws.connect('ws://127.0.0.1:8000/ws')
370
+ .then(() => {
371
+ ws.loadNotebook(notebookName || 'default');
372
+ })
373
+ .catch(error => {
374
+ console.error('Failed to connect:', error);
375
+ handleKernelStatusUpdate('disconnected');
376
+ });
377
+
378
+ ws.on('connect', () => handleKernelStatusUpdate('connected'));
379
+ ws.on('disconnect', () => handleKernelStatusUpdate('disconnected'));
380
+ ws.on('notebook_loaded', handleNotebookLoaded);
381
+ ws.on('notebook_updated', handleNotebookUpdate);
382
+ ws.on('execution_start', handleExecutionStart);
383
+ ws.on('stream_output', handleStreamOutput);
384
+ ws.on('execution_complete', handleExecutionComplete);
385
+ ws.on('execution_result', handleExecuteResult);
386
+ ws.on('execution_error', handleExecutionError);
387
+
388
+ return () => ws.disconnect();
389
+ }, [notebookName, handleKernelStatusUpdate, handleNotebookLoaded, handleNotebookUpdate, handleExecutionStart, handleStreamOutput, handleExecuteResult, handleExecutionComplete, handleExecutionError]);
390
+
391
+ // Simplified save management - only save on Ctrl+S or Run
392
+ const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle');
393
+
394
+ // Simple save function (defined BEFORE cell actions that use it)
395
+ const saveNotebook = useCallback(() => {
396
+ setSaveState('saving');
397
+ wsRef.current?.saveNotebook();
398
+
399
+ // Show "Saving..." for 500ms, then "Saved" for 2 seconds
400
+ setTimeout(() => {
401
+ setSaveState('saved');
402
+ setTimeout(() => {
403
+ setSaveState('idle');
404
+ }, 2000);
405
+ }, 500);
406
+ }, []);
407
+
408
+ // Keyboard shortcut (Cmd+S / Ctrl+S)
409
+ useEffect(() => {
410
+ const handleKeyDown = (e: KeyboardEvent) => {
411
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
412
+ e.preventDefault();
413
+ saveNotebook();
414
+ }
415
+ };
416
+
417
+ window.addEventListener('keydown', handleKeyDown);
418
+ return () => window.removeEventListener('keydown', handleKeyDown);
419
+ }, [saveNotebook]);
420
+
421
+ // --- Cell Actions ---
422
+
423
+ const executeCell = useCallback((index: number) => {
424
+ const cell = cells[index];
425
+ if (!cell) return;
426
+
427
+ if (cell.cell_type === 'markdown') {
428
+ // Save before rendering markdown
429
+ saveNotebook();
430
+ // Markdown rendering is handled locally in Cell.tsx now
431
+ return;
432
+ }
433
+
434
+ // Save before executing code
435
+ saveNotebook();
436
+ wsRef.current?.executeCell(index, cell.source);
437
+ }, [cells, saveNotebook]);
438
+
439
+ const interruptCell = useCallback((index: number) => {
440
+ wsRef.current?.interruptKernel(index);
441
+ }, []);
442
+
443
+ const deleteCell = useCallback((index: number) => {
444
+ wsRef.current?.deleteCell(index);
445
+ }, []);
446
+
447
+ const updateCell = useCallback((index: number, source: string) => {
448
+ dispatch({ type: 'UPDATE_CELL_SOURCE', payload: { cell_index: index, source } });
449
+ wsRef.current?.updateCell(index, source);
450
+ }, []);
451
+
452
+ const addCell = useCallback((type: 'code' | 'markdown' = 'code', index: number) => {
453
+ wsRef.current?.addCell(index, type);
454
+ setCurrentCellIndex(index);
455
+ }, []);
456
+
457
+ const resetKernel = () => {
458
+ if (confirm('Are you sure you want to restart the kernel? All variables will be lost.')) {
459
+ wsRef.current?.resetKernel();
460
+ dispatch({ type: 'RESET_KERNEL' });
461
+ }
462
+ };
463
+
464
+ // --- Render ---
465
+
466
+ return (
467
+ <>
468
+ {/* Save Status Indicator - show saving and saved states */}
469
+ {saveState !== 'idle' && (
470
+ <div
471
+ style={{
472
+ position: 'fixed',
473
+ bottom: '20px',
474
+ right: '20px',
475
+ padding: '8px 14px',
476
+ borderRadius: '8px',
477
+ fontSize: '12px',
478
+ fontWeight: 500,
479
+ backgroundColor: saveState === 'saved' ? '#10b98114' : '#3b82f614',
480
+ color: saveState === 'saved' ? '#10b981' : '#3b82f6',
481
+ border: `1px solid ${saveState === 'saved' ? '#10b981' : '#3b82f6'}`,
482
+ display: 'flex',
483
+ alignItems: 'center',
484
+ gap: '8px',
485
+ zIndex: 1000,
486
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
487
+ animation: 'fadeIn 0.2s ease',
488
+ }}
489
+ >
490
+ {saveState === 'saving' && '⟳ Saving...'}
491
+ {saveState === 'saved' && '✓ Saved'}
492
+ </div>
493
+ )}
494
+
495
+ {cells.map((cell, index) => (
496
+ <CellComponent
497
+ key={cell.id}
498
+ cell={cell}
499
+ index={index}
500
+ isActive={currentCellIndex === index}
501
+ isExecuting={executingCells.has(index)}
502
+ onExecute={executeCell}
503
+ onInterrupt={interruptCell}
504
+ onDelete={deleteCell}
505
+ onUpdate={updateCell}
506
+ onSetActive={setCurrentCellIndex}
507
+ onAddCell={addCell}
508
+ />
509
+ ))}
510
+
511
+ {cells.length === 0 && (
512
+ <div id="empty-state" className="empty-state">
513
+ <div className="add-cell-line" data-position="0">
514
+ <AddCellButton onAddCell={(type) => addCell(type, 0)} />
515
+ </div>
516
+ </div>
517
+ )}
518
+ </>
519
+ );
520
+ };
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { Folder, Package, Cpu, Settings, ChartArea, Zap } from "lucide-react";
3
+
4
+ interface SidebarItemData {
5
+ id: string;
6
+ icon: React.ReactNode;
7
+ tooltip: string;
8
+ }
9
+
10
+ const sidebarItems: SidebarItemData[] = [
11
+ { id: "folder", icon: <Folder size={18} />, tooltip: "Files" },
12
+ { id: "packages", icon: <Package size={18} />, tooltip: "Packages" },
13
+ {
14
+ id: "python",
15
+ icon: <img src="assets/icons/python.svg" width={18} height={18} />,
16
+ tooltip: "Python",
17
+ },
18
+ { id: "compute", icon: <Cpu size={18} />, tooltip: "Compute" },
19
+ { id: "metrics", icon: <ChartArea size={18} />, tooltip: "Metrics" },
20
+ { id: "settings", icon: <Settings size={18} />, tooltip: "Settings" },
21
+ ];
22
+
23
+ interface SidebarProps {
24
+ onTogglePopup: (popupType: string) => void;
25
+ activePopup: string | null;
26
+ }
27
+
28
+ const Sidebar: React.FC<SidebarProps> = ({ onTogglePopup, activePopup }) => {
29
+ return (
30
+ <div id="sidebar" className="sidebar">
31
+ {sidebarItems.map((item) => (
32
+ <div
33
+ key={item.id}
34
+ className={`sidebar-item ${activePopup === item.id ? "active" : ""}`}
35
+ data-popup={item.id}
36
+ onClick={() => onTogglePopup(item.id)}
37
+ >
38
+ <span className="sidebar-icon-wrapper">{item.icon}</span>
39
+ <div className="sidebar-tooltip">{item.tooltip}</div>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default Sidebar;