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.
- frontend/.DS_Store +0 -0
- frontend/.gitignore +41 -0
- frontend/README.md +36 -0
- frontend/__init__.py +1 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +1537 -0
- frontend/app/layout.tsx +173 -0
- frontend/app/page.tsx +11 -0
- frontend/components/AddCellButton.tsx +42 -0
- frontend/components/Cell.tsx +244 -0
- frontend/components/CellButton.tsx +58 -0
- frontend/components/CellOutput.tsx +70 -0
- frontend/components/ErrorDisplay.tsx +208 -0
- frontend/components/ErrorModal.tsx +154 -0
- frontend/components/MarkdownRenderer.tsx +84 -0
- frontend/components/Notebook.tsx +520 -0
- frontend/components/Sidebar.tsx +46 -0
- frontend/components/popups/ComputePopup.tsx +879 -0
- frontend/components/popups/FilterPopup.tsx +427 -0
- frontend/components/popups/FolderPopup.tsx +171 -0
- frontend/components/popups/MetricsPopup.tsx +168 -0
- frontend/components/popups/PackagesPopup.tsx +112 -0
- frontend/components/popups/PythonPopup.tsx +292 -0
- frontend/components/popups/SettingsPopup.tsx +68 -0
- frontend/eslint.config.mjs +25 -0
- frontend/lib/api.ts +469 -0
- frontend/lib/settings.ts +87 -0
- frontend/lib/websocket-native.ts +202 -0
- frontend/lib/websocket.ts +134 -0
- frontend/next-env.d.ts +6 -0
- frontend/next.config.mjs +17 -0
- frontend/next.config.ts +7 -0
- frontend/package-lock.json +5676 -0
- frontend/package.json +41 -0
- frontend/postcss.config.mjs +5 -0
- frontend/public/assets/icons/add.svg +1 -0
- frontend/public/assets/icons/check.svg +1 -0
- frontend/public/assets/icons/copy.svg +1 -0
- frontend/public/assets/icons/folder.svg +1 -0
- frontend/public/assets/icons/metric.svg +1 -0
- frontend/public/assets/icons/packages.svg +1 -0
- frontend/public/assets/icons/play.svg +1 -0
- frontend/public/assets/icons/python.svg +265 -0
- frontend/public/assets/icons/setting.svg +1 -0
- frontend/public/assets/icons/stop.svg +1 -0
- frontend/public/assets/icons/trash.svg +1 -0
- frontend/public/assets/icons/up-down.svg +1 -0
- frontend/public/assets/icons/x.svg +1 -0
- frontend/public/file.svg +1 -0
- frontend/public/fonts/Fira.ttf +0 -0
- frontend/public/fonts/Tiempos.woff2 +0 -0
- frontend/public/fonts/VeraMono.ttf +0 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/tailwind.config.ts +29 -0
- frontend/tsconfig.json +27 -0
- frontend/types/notebook.ts +58 -0
- kernel_run.py +7 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/METADATA +1 -1
- more_compute-0.1.4.dist-info/RECORD +86 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/top_level.txt +1 -0
- morecompute/__version__.py +1 -0
- more_compute-0.1.2.dist-info/RECORD +0 -26
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/WHEEL +0 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/entry_points.txt +0 -0
- {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;
|