more-compute 0.1.3__py3-none-any.whl → 0.2.0__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/app/globals.css +322 -77
- frontend/app/layout.tsx +98 -82
- frontend/components/Cell.tsx +234 -95
- frontend/components/Notebook.tsx +430 -199
- frontend/components/{AddCellButton.tsx → cell/AddCellButton.tsx} +0 -2
- frontend/components/cell/MonacoCell.tsx +726 -0
- frontend/components/layout/ConnectionBanner.tsx +41 -0
- frontend/components/{Sidebar.tsx → layout/Sidebar.tsx} +16 -11
- frontend/components/modals/ConfirmModal.tsx +154 -0
- frontend/components/modals/SuccessModal.tsx +140 -0
- frontend/components/output/MarkdownRenderer.tsx +116 -0
- frontend/components/popups/ComputePopup.tsx +674 -365
- frontend/components/popups/MetricsPopup.tsx +11 -7
- frontend/components/popups/SettingsPopup.tsx +11 -13
- frontend/contexts/PodWebSocketContext.tsx +247 -0
- frontend/eslint.config.mjs +11 -0
- frontend/lib/monaco-themes.ts +160 -0
- frontend/lib/settings.ts +128 -26
- frontend/lib/themes.json +9973 -0
- frontend/lib/websocket-native.ts +19 -8
- frontend/lib/websocket.ts +59 -11
- frontend/next.config.ts +8 -0
- frontend/package-lock.json +1705 -3
- frontend/package.json +8 -1
- frontend/styling_README.md +18 -0
- kernel_run.py +161 -43
- more_compute-0.2.0.dist-info/METADATA +126 -0
- more_compute-0.2.0.dist-info/RECORD +100 -0
- morecompute/__version__.py +1 -0
- morecompute/execution/executor.py +31 -20
- morecompute/execution/worker.py +68 -7
- morecompute/models/__init__.py +31 -0
- morecompute/models/api_models.py +197 -0
- morecompute/notebook.py +50 -7
- morecompute/server.py +574 -94
- morecompute/services/data_manager.py +379 -0
- morecompute/services/lsp_service.py +335 -0
- morecompute/services/pod_manager.py +122 -20
- morecompute/services/pod_monitor.py +138 -0
- morecompute/services/prime_intellect.py +87 -63
- morecompute/utils/config_util.py +59 -0
- morecompute/utils/special_commands.py +11 -5
- morecompute/utils/zmq_util.py +51 -0
- frontend/components/MarkdownRenderer.tsx +0 -84
- frontend/components/popups/PythonPopup.tsx +0 -292
- more_compute-0.1.3.dist-info/METADATA +0 -173
- more_compute-0.1.3.dist-info/RECORD +0 -85
- /frontend/components/{CellButton.tsx → cell/CellButton.tsx} +0 -0
- /frontend/components/{ErrorModal.tsx → modals/ErrorModal.tsx} +0 -0
- /frontend/components/{CellOutput.tsx → output/CellOutput.tsx} +0 -0
- /frontend/components/{ErrorDisplay.tsx → output/ErrorDisplay.tsx} +0 -0
- {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/WHEEL +0 -0
- {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/top_level.txt +0 -0
frontend/components/Notebook.tsx
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import React, {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
useReducer,
|
|
9
|
+
} from "react";
|
|
10
|
+
import dynamic from "next/dynamic";
|
|
11
|
+
import {
|
|
12
|
+
Cell,
|
|
13
|
+
Output,
|
|
14
|
+
StreamOutput,
|
|
15
|
+
ExecuteResultOutput,
|
|
16
|
+
ErrorOutput,
|
|
17
|
+
} from "@/types/notebook";
|
|
18
|
+
import { WebSocketService } from "@/lib/websocket-native";
|
|
19
|
+
import AddCellButton from "./cell/AddCellButton";
|
|
20
|
+
import { loadSettings, applyTheme } from "@/lib/settings";
|
|
21
|
+
|
|
22
|
+
// Dynamically import MonacoCell to avoid SSR issues with Monaco Editor
|
|
23
|
+
const CellComponent = dynamic(() => import("./cell/MonacoCell").then(mod => ({ default: mod.MonacoCell })), {
|
|
24
|
+
ssr: false,
|
|
25
|
+
loading: () => (
|
|
26
|
+
<div style={{
|
|
27
|
+
height: "150px",
|
|
28
|
+
background: "var(--mc-cell-background)",
|
|
29
|
+
border: "2px solid #DBC7A8",
|
|
30
|
+
borderRadius: "0.5px",
|
|
31
|
+
display: "flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
color: "#6b7280",
|
|
35
|
+
fontSize: "14px",
|
|
36
|
+
margin: "20px 0"
|
|
37
|
+
}}>
|
|
38
|
+
Loading Monaco editor...
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
});
|
|
9
42
|
|
|
10
43
|
// --- State Management with useReducer ---
|
|
11
44
|
|
|
@@ -15,15 +48,35 @@ interface NotebookState {
|
|
|
15
48
|
}
|
|
16
49
|
|
|
17
50
|
type NotebookAction =
|
|
18
|
-
| { type:
|
|
19
|
-
| {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
| { type: "NOTEBOOK_LOADED"; payload: { cells: Cell[] } }
|
|
52
|
+
| {
|
|
53
|
+
type: "EXECUTION_START";
|
|
54
|
+
payload: { cell_index: number; execution_count: number };
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: "STREAM_OUTPUT";
|
|
58
|
+
payload: {
|
|
59
|
+
cell_index: number;
|
|
60
|
+
stream: "stdout" | "stderr";
|
|
61
|
+
text: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
type: "EXECUTE_RESULT";
|
|
66
|
+
payload: {
|
|
67
|
+
cell_index: number;
|
|
68
|
+
execution_count: number;
|
|
69
|
+
data: ExecuteResultOutput["data"];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
| { type: "EXECUTION_COMPLETE"; payload: { cell_index: number; result: any } }
|
|
73
|
+
| { type: "EXECUTION_ERROR"; payload: { cell_index: number; error: Output } }
|
|
74
|
+
| { type: "NOTEBOOK_UPDATED"; payload: { cells: Cell[] } }
|
|
75
|
+
| {
|
|
76
|
+
type: "UPDATE_CELL_SOURCE";
|
|
77
|
+
payload: { cell_index: number; source: string };
|
|
78
|
+
}
|
|
79
|
+
| { type: "RESET_KERNEL" };
|
|
27
80
|
|
|
28
81
|
const initialState: NotebookState = {
|
|
29
82
|
cells: [],
|
|
@@ -35,16 +88,16 @@ applyTheme(initialSettings.theme);
|
|
|
35
88
|
|
|
36
89
|
const coerceToString = (value: unknown): string => {
|
|
37
90
|
if (Array.isArray(value)) {
|
|
38
|
-
return value.join(
|
|
91
|
+
return value.join("");
|
|
39
92
|
}
|
|
40
|
-
if (typeof value ===
|
|
93
|
+
if (typeof value === "string") {
|
|
41
94
|
return value;
|
|
42
95
|
}
|
|
43
|
-
return value != null ? String(value) :
|
|
96
|
+
return value != null ? String(value) : "";
|
|
44
97
|
};
|
|
45
98
|
|
|
46
99
|
const normalizeError = (error: any): ErrorOutput | undefined => {
|
|
47
|
-
if (!error || typeof error !==
|
|
100
|
+
if (!error || typeof error !== "object") {
|
|
48
101
|
return undefined;
|
|
49
102
|
}
|
|
50
103
|
|
|
@@ -53,16 +106,16 @@ const normalizeError = (error: any): ErrorOutput | undefined => {
|
|
|
53
106
|
|
|
54
107
|
const traceback = Array.isArray(tracebackRaw)
|
|
55
108
|
? tracebackRaw.map(coerceToString)
|
|
56
|
-
: coerceToString(tracebackRaw).split(
|
|
109
|
+
: coerceToString(tracebackRaw).split("\n");
|
|
57
110
|
|
|
58
111
|
const suggestions = Array.isArray(suggestionsRaw)
|
|
59
112
|
? suggestionsRaw.map(coerceToString)
|
|
60
113
|
: undefined;
|
|
61
114
|
|
|
62
115
|
return {
|
|
63
|
-
output_type:
|
|
64
|
-
ename: coerceToString((error as any).ename ??
|
|
65
|
-
evalue: coerceToString((error as any).evalue ??
|
|
116
|
+
output_type: "error",
|
|
117
|
+
ename: coerceToString((error as any).ename ?? "Error"),
|
|
118
|
+
evalue: coerceToString((error as any).evalue ?? ""),
|
|
66
119
|
traceback,
|
|
67
120
|
error_type: (error as any).error_type,
|
|
68
121
|
suggestions,
|
|
@@ -75,20 +128,20 @@ const normalizeOutputs = (outputs: any): Output[] => {
|
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
return outputs.map((output) => {
|
|
78
|
-
if (!output || typeof output !==
|
|
131
|
+
if (!output || typeof output !== "object") {
|
|
79
132
|
return output as Output;
|
|
80
133
|
}
|
|
81
134
|
|
|
82
|
-
if (output.output_type ===
|
|
83
|
-
const text = coerceToString((output as any).text ??
|
|
135
|
+
if (output.output_type === "stream") {
|
|
136
|
+
const text = coerceToString((output as any).text ?? "");
|
|
84
137
|
return { ...output, text } as StreamOutput;
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
if (output.output_type ===
|
|
140
|
+
if (output.output_type === "execute_result") {
|
|
88
141
|
const data = { ...(output as any).data };
|
|
89
|
-
if (data && typeof data ===
|
|
90
|
-
const plain = coerceToString((data as any)[
|
|
91
|
-
data[
|
|
142
|
+
if (data && typeof data === "object") {
|
|
143
|
+
const plain = coerceToString((data as any)["text/plain"] ?? "");
|
|
144
|
+
data["text/plain"] = plain;
|
|
92
145
|
}
|
|
93
146
|
return {
|
|
94
147
|
...output,
|
|
@@ -96,7 +149,7 @@ const normalizeOutputs = (outputs: any): Output[] => {
|
|
|
96
149
|
} as ExecuteResultOutput;
|
|
97
150
|
}
|
|
98
151
|
|
|
99
|
-
if (output.output_type ===
|
|
152
|
+
if (output.output_type === "error") {
|
|
100
153
|
return normalizeError(output) as ErrorOutput;
|
|
101
154
|
}
|
|
102
155
|
|
|
@@ -105,12 +158,12 @@ const normalizeOutputs = (outputs: any): Output[] => {
|
|
|
105
158
|
};
|
|
106
159
|
|
|
107
160
|
const normalizeCell = (cell: any, index: number): Cell => {
|
|
108
|
-
const source = coerceToString(cell?.source ??
|
|
161
|
+
const source = coerceToString(cell?.source ?? "");
|
|
109
162
|
const outputs = normalizeOutputs(cell?.outputs);
|
|
110
163
|
const error = normalizeError(cell?.error);
|
|
111
164
|
return {
|
|
112
|
-
id: typeof cell?.id ===
|
|
113
|
-
cell_type: cell?.cell_type ||
|
|
165
|
+
id: typeof cell?.id === "string" && cell.id ? cell.id : `cell-${index}`,
|
|
166
|
+
cell_type: cell?.cell_type || "code",
|
|
114
167
|
source,
|
|
115
168
|
outputs,
|
|
116
169
|
metadata: cell?.metadata || {},
|
|
@@ -120,16 +173,21 @@ const normalizeCell = (cell: any, index: number): Cell => {
|
|
|
120
173
|
} as Cell;
|
|
121
174
|
};
|
|
122
175
|
|
|
123
|
-
function notebookReducer(
|
|
176
|
+
function notebookReducer(
|
|
177
|
+
state: NotebookState,
|
|
178
|
+
action: NotebookAction,
|
|
179
|
+
): NotebookState {
|
|
124
180
|
switch (action.type) {
|
|
125
|
-
case
|
|
126
|
-
case
|
|
181
|
+
case "NOTEBOOK_LOADED":
|
|
182
|
+
case "NOTEBOOK_UPDATED":
|
|
127
183
|
return {
|
|
128
184
|
...state,
|
|
129
|
-
cells: action.payload.cells.map((cell, index) =>
|
|
185
|
+
cells: action.payload.cells.map((cell, index) =>
|
|
186
|
+
normalizeCell(cell, index),
|
|
187
|
+
),
|
|
130
188
|
};
|
|
131
189
|
|
|
132
|
-
case
|
|
190
|
+
case "EXECUTION_START": {
|
|
133
191
|
const newExecuting = new Set(state.executingCells);
|
|
134
192
|
newExecuting.add(action.payload.cell_index);
|
|
135
193
|
return {
|
|
@@ -137,13 +195,18 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
|
|
|
137
195
|
executingCells: newExecuting,
|
|
138
196
|
cells: state.cells.map((cell, i) =>
|
|
139
197
|
i === action.payload.cell_index
|
|
140
|
-
? {
|
|
141
|
-
|
|
198
|
+
? {
|
|
199
|
+
...cell,
|
|
200
|
+
outputs: [],
|
|
201
|
+
error: null,
|
|
202
|
+
execution_count: action.payload.execution_count,
|
|
203
|
+
}
|
|
204
|
+
: cell,
|
|
142
205
|
),
|
|
143
206
|
};
|
|
144
207
|
}
|
|
145
208
|
|
|
146
|
-
case
|
|
209
|
+
case "STREAM_OUTPUT": {
|
|
147
210
|
const { cell_index, stream, text } = action.payload;
|
|
148
211
|
return {
|
|
149
212
|
...state,
|
|
@@ -152,7 +215,7 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
|
|
|
152
215
|
|
|
153
216
|
const outputs = [...(cell.outputs || [])];
|
|
154
217
|
const streamIndex = outputs.findIndex(
|
|
155
|
-
(o) => o.output_type ===
|
|
218
|
+
(o) => o.output_type === "stream" && o.name === stream,
|
|
156
219
|
);
|
|
157
220
|
|
|
158
221
|
if (streamIndex > -1) {
|
|
@@ -162,54 +225,59 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
|
|
|
162
225
|
text: streamOutput.text + text,
|
|
163
226
|
};
|
|
164
227
|
} else {
|
|
165
|
-
outputs.push({
|
|
228
|
+
outputs.push({
|
|
229
|
+
output_type: "stream",
|
|
230
|
+
name: stream,
|
|
231
|
+
text,
|
|
232
|
+
} as StreamOutput);
|
|
166
233
|
}
|
|
167
234
|
return { ...cell, outputs };
|
|
168
235
|
}),
|
|
169
236
|
};
|
|
170
237
|
}
|
|
171
238
|
|
|
172
|
-
case
|
|
239
|
+
case "EXECUTE_RESULT": {
|
|
173
240
|
const { cell_index, execution_count, data } = action.payload;
|
|
174
241
|
return {
|
|
175
242
|
...state,
|
|
176
243
|
cells: state.cells.map((cell, i) => {
|
|
177
244
|
if (i !== cell_index) return cell;
|
|
178
245
|
const outputs = [...(cell.outputs || [])];
|
|
179
|
-
|
|
246
|
+
|
|
180
247
|
// Check if this is display data (e.g., matplotlib image)
|
|
181
|
-
const hasImageData =
|
|
182
|
-
|
|
248
|
+
const hasImageData =
|
|
249
|
+
(data as any)?.["image/png"] ||
|
|
250
|
+
(data as any)?.["image/jpeg"] ||
|
|
251
|
+
(data as any)?.["image/svg+xml"];
|
|
252
|
+
|
|
183
253
|
if (hasImageData) {
|
|
184
254
|
// Create display_data output for images (matplotlib plots)
|
|
185
255
|
outputs.push({
|
|
186
|
-
output_type:
|
|
256
|
+
output_type: "display_data",
|
|
187
257
|
data: data || {},
|
|
188
258
|
} as any);
|
|
189
259
|
} else {
|
|
190
260
|
// Create execute_result output for text results
|
|
191
261
|
outputs.push({
|
|
192
|
-
output_type:
|
|
262
|
+
output_type: "execute_result",
|
|
193
263
|
execution_count,
|
|
194
264
|
data: {
|
|
195
265
|
...(data || {}),
|
|
196
|
-
|
|
266
|
+
"text/plain": coerceToString(data?.["text/plain"] ?? ""),
|
|
197
267
|
},
|
|
198
268
|
} as ExecuteResultOutput);
|
|
199
269
|
}
|
|
200
|
-
|
|
270
|
+
|
|
201
271
|
return { ...cell, outputs, execution_count };
|
|
202
272
|
}),
|
|
203
273
|
};
|
|
204
274
|
}
|
|
205
|
-
|
|
206
|
-
case
|
|
275
|
+
|
|
276
|
+
case "EXECUTION_COMPLETE": {
|
|
207
277
|
const payload = action.payload || {};
|
|
208
278
|
const cell_index = payload.cell_index;
|
|
209
279
|
// Support both shapes: { result: {...} } and flat payload {...}
|
|
210
|
-
const result =
|
|
211
|
-
? payload.result
|
|
212
|
-
: payload || {};
|
|
280
|
+
const result = payload && payload.result ? payload.result : payload || {};
|
|
213
281
|
const newExecuting = new Set(state.executingCells);
|
|
214
282
|
newExecuting.delete(cell_index);
|
|
215
283
|
return {
|
|
@@ -217,11 +285,17 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
|
|
|
217
285
|
executingCells: newExecuting,
|
|
218
286
|
cells: state.cells.map((cell, i) => {
|
|
219
287
|
if (i !== cell_index) return cell;
|
|
220
|
-
const normalizedOutputs = normalizeOutputs(
|
|
288
|
+
const normalizedOutputs = normalizeOutputs(
|
|
289
|
+
Array.isArray((result as any).outputs)
|
|
290
|
+
? (result as any).outputs
|
|
291
|
+
: [],
|
|
292
|
+
);
|
|
221
293
|
const normalizedError = normalizeError(result?.error);
|
|
222
294
|
|
|
223
295
|
const existingOutputs = cell.outputs || [];
|
|
224
|
-
const finalNonStream = normalizedOutputs.filter(
|
|
296
|
+
const finalNonStream = normalizedOutputs.filter(
|
|
297
|
+
(o) => (o as any).output_type !== "stream",
|
|
298
|
+
);
|
|
225
299
|
return {
|
|
226
300
|
...cell,
|
|
227
301
|
...result,
|
|
@@ -234,44 +308,48 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
|
|
|
234
308
|
};
|
|
235
309
|
}
|
|
236
310
|
|
|
237
|
-
case
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
311
|
+
case "EXECUTION_ERROR": {
|
|
312
|
+
const { cell_index, error } = action.payload;
|
|
313
|
+
const newExecuting = new Set(state.executingCells);
|
|
314
|
+
newExecuting.delete(cell_index);
|
|
315
|
+
const normalizedError = normalizeError(error);
|
|
316
|
+
return {
|
|
317
|
+
...state,
|
|
318
|
+
executingCells: newExecuting,
|
|
319
|
+
cells: state.cells.map((cell, i) =>
|
|
320
|
+
i === cell_index
|
|
321
|
+
? {
|
|
322
|
+
...cell,
|
|
323
|
+
error: normalizedError,
|
|
324
|
+
outputs: normalizedError
|
|
325
|
+
? [...(cell.outputs || []), normalizedError]
|
|
326
|
+
: cell.outputs || [],
|
|
327
|
+
}
|
|
328
|
+
: cell,
|
|
329
|
+
),
|
|
330
|
+
};
|
|
257
331
|
}
|
|
258
332
|
|
|
259
|
-
case
|
|
333
|
+
case "UPDATE_CELL_SOURCE": {
|
|
260
334
|
const { cell_index, source } = action.payload;
|
|
261
335
|
return {
|
|
262
336
|
...state,
|
|
263
337
|
cells: state.cells.map((cell, i) =>
|
|
264
|
-
i === cell_index ? { ...cell, source } : cell
|
|
338
|
+
i === cell_index ? { ...cell, source } : cell,
|
|
265
339
|
),
|
|
266
340
|
};
|
|
267
341
|
}
|
|
268
342
|
|
|
269
|
-
case
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
343
|
+
case "RESET_KERNEL":
|
|
344
|
+
return {
|
|
345
|
+
...state,
|
|
346
|
+
executingCells: new Set(),
|
|
347
|
+
cells: state.cells.map((cell) => ({
|
|
348
|
+
...cell,
|
|
349
|
+
outputs: [],
|
|
350
|
+
execution_count: null,
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
275
353
|
|
|
276
354
|
default:
|
|
277
355
|
return state;
|
|
@@ -284,17 +362,37 @@ interface NotebookProps {
|
|
|
284
362
|
notebookName?: string;
|
|
285
363
|
}
|
|
286
364
|
|
|
287
|
-
|
|
365
|
+
// Deletion queue for undo functionality
|
|
366
|
+
interface DeletedCell {
|
|
367
|
+
cell: Cell;
|
|
368
|
+
index: number;
|
|
369
|
+
timestamp: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export const Notebook: React.FC<NotebookProps> = ({
|
|
373
|
+
notebookName = "default",
|
|
374
|
+
}) => {
|
|
288
375
|
const [state, dispatch] = useReducer(notebookReducer, initialState);
|
|
289
376
|
const { cells, executingCells } = state;
|
|
290
|
-
|
|
377
|
+
|
|
291
378
|
const [currentCellIndex, setCurrentCellIndex] = useState<number | null>(null);
|
|
292
|
-
const [kernelStatus, setKernelStatus] = useState<
|
|
379
|
+
const [kernelStatus, setKernelStatus] = useState<
|
|
380
|
+
"connecting" | "connected" | "disconnected"
|
|
381
|
+
>("connecting");
|
|
293
382
|
const wsRef = useRef<WebSocketService | null>(null);
|
|
294
383
|
|
|
384
|
+
// Deletion queue for undo functionality (lasts entire session, cleared on close)
|
|
385
|
+
const deletionQueueRef = useRef<DeletedCell[]>([]);
|
|
386
|
+
const [showUndoHint, setShowUndoHint] = useState(false);
|
|
387
|
+
|
|
388
|
+
// DEBUG: Verify new code is loaded
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
console.log("✅ NEW NOTEBOOK CODE LOADED - Undo functionality available");
|
|
391
|
+
}, []);
|
|
392
|
+
|
|
295
393
|
useEffect(() => {
|
|
296
394
|
const body = document.body;
|
|
297
|
-
const pathAttr = body.getAttribute(
|
|
395
|
+
const pathAttr = body.getAttribute("data-notebook-path");
|
|
298
396
|
if (pathAttr) {
|
|
299
397
|
document.title = `MoreCompute – ${pathAttr}`;
|
|
300
398
|
}
|
|
@@ -303,24 +401,24 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
|
|
|
303
401
|
// --- Event Handlers ---
|
|
304
402
|
|
|
305
403
|
const handleNotebookLoaded = useCallback((data: any) => {
|
|
306
|
-
dispatch({ type:
|
|
404
|
+
dispatch({ type: "NOTEBOOK_LOADED", payload: data });
|
|
307
405
|
}, []);
|
|
308
406
|
|
|
309
407
|
const handleExecutionStart = useCallback((data: any) => {
|
|
310
|
-
dispatch({ type:
|
|
408
|
+
dispatch({ type: "EXECUTION_START", payload: data });
|
|
311
409
|
}, []);
|
|
312
|
-
|
|
410
|
+
|
|
313
411
|
const handleStreamOutput = useCallback((data: any) => {
|
|
314
|
-
dispatch({ type:
|
|
412
|
+
dispatch({ type: "STREAM_OUTPUT", payload: data });
|
|
315
413
|
}, []);
|
|
316
414
|
|
|
317
415
|
const handleExecutionComplete = useCallback((data: any) => {
|
|
318
|
-
dispatch({ type:
|
|
416
|
+
dispatch({ type: "EXECUTION_COMPLETE", payload: data });
|
|
319
417
|
}, []);
|
|
320
418
|
|
|
321
419
|
const handleExecuteResult = useCallback((data: any) => {
|
|
322
420
|
dispatch({
|
|
323
|
-
type:
|
|
421
|
+
type: "EXECUTE_RESULT",
|
|
324
422
|
payload: {
|
|
325
423
|
cell_index: data.cell_index,
|
|
326
424
|
execution_count: data.execution_count,
|
|
@@ -330,134 +428,236 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
|
|
|
330
428
|
}, []);
|
|
331
429
|
|
|
332
430
|
const handleExecutionError = useCallback((data: any) => {
|
|
333
|
-
dispatch({ type:
|
|
431
|
+
dispatch({ type: "EXECUTION_ERROR", payload: data });
|
|
334
432
|
}, []);
|
|
335
433
|
|
|
336
434
|
const handleNotebookUpdate = useCallback((data: any) => {
|
|
337
|
-
dispatch({ type:
|
|
435
|
+
dispatch({ type: "NOTEBOOK_UPDATED", payload: data });
|
|
338
436
|
}, []);
|
|
339
|
-
|
|
340
|
-
const handleKernelStatusUpdate = useCallback(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
437
|
+
|
|
438
|
+
const handleKernelStatusUpdate = useCallback(
|
|
439
|
+
(status: "connecting" | "connected" | "disconnected") => {
|
|
440
|
+
setKernelStatus(status);
|
|
441
|
+
const dot = document.getElementById("kernel-status-dot");
|
|
442
|
+
const text = document.getElementById("kernel-status-text");
|
|
443
|
+
|
|
444
|
+
if (dot && text) {
|
|
445
|
+
dot.className = "status-dot"; // Reset classes
|
|
446
|
+
switch (status) {
|
|
447
|
+
case "connecting":
|
|
448
|
+
dot.classList.add("connecting");
|
|
449
|
+
text.textContent = "Connecting...";
|
|
450
|
+
break;
|
|
451
|
+
case "connected":
|
|
452
|
+
dot.classList.add("connected");
|
|
453
|
+
text.textContent = "Kernel Ready";
|
|
454
|
+
break;
|
|
455
|
+
case "disconnected":
|
|
456
|
+
dot.classList.add("disconnected");
|
|
457
|
+
text.textContent = "Kernel Disconnected";
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
360
460
|
}
|
|
361
|
-
}
|
|
362
|
-
|
|
461
|
+
},
|
|
462
|
+
[],
|
|
463
|
+
);
|
|
363
464
|
|
|
364
465
|
useEffect(() => {
|
|
365
466
|
const ws = new WebSocketService();
|
|
366
467
|
wsRef.current = ws;
|
|
367
|
-
handleKernelStatusUpdate(
|
|
468
|
+
handleKernelStatusUpdate("connecting");
|
|
368
469
|
|
|
369
|
-
ws.connect(
|
|
470
|
+
ws.connect("ws://127.0.0.1:8000/ws")
|
|
370
471
|
.then(() => {
|
|
371
|
-
ws.loadNotebook(notebookName ||
|
|
472
|
+
ws.loadNotebook(notebookName || "default");
|
|
372
473
|
})
|
|
373
|
-
.catch(error => {
|
|
374
|
-
console.error(
|
|
375
|
-
handleKernelStatusUpdate(
|
|
474
|
+
.catch((error) => {
|
|
475
|
+
console.error("Failed to connect:", error);
|
|
476
|
+
handleKernelStatusUpdate("disconnected");
|
|
376
477
|
});
|
|
377
478
|
|
|
378
|
-
ws.on(
|
|
379
|
-
ws.on(
|
|
380
|
-
ws.on(
|
|
381
|
-
ws.on(
|
|
382
|
-
ws.on(
|
|
383
|
-
ws.on(
|
|
384
|
-
ws.on(
|
|
385
|
-
ws.on(
|
|
386
|
-
ws.on(
|
|
387
|
-
|
|
479
|
+
ws.on("connect", () => handleKernelStatusUpdate("connected"));
|
|
480
|
+
ws.on("disconnect", () => handleKernelStatusUpdate("disconnected"));
|
|
481
|
+
ws.on("notebook_loaded", handleNotebookLoaded);
|
|
482
|
+
ws.on("notebook_updated", handleNotebookUpdate);
|
|
483
|
+
ws.on("execution_start", handleExecutionStart);
|
|
484
|
+
ws.on("stream_output", handleStreamOutput);
|
|
485
|
+
ws.on("execution_complete", handleExecutionComplete);
|
|
486
|
+
ws.on("execution_result", handleExecuteResult);
|
|
487
|
+
ws.on("execution_error", handleExecutionError);
|
|
488
|
+
|
|
388
489
|
return () => ws.disconnect();
|
|
389
|
-
}, [
|
|
490
|
+
}, [
|
|
491
|
+
notebookName,
|
|
492
|
+
handleKernelStatusUpdate,
|
|
493
|
+
handleNotebookLoaded,
|
|
494
|
+
handleNotebookUpdate,
|
|
495
|
+
handleExecutionStart,
|
|
496
|
+
handleStreamOutput,
|
|
497
|
+
handleExecuteResult,
|
|
498
|
+
handleExecutionComplete,
|
|
499
|
+
handleExecutionError,
|
|
500
|
+
]);
|
|
390
501
|
|
|
391
502
|
// Simplified save management - only save on Ctrl+S or Run
|
|
392
|
-
const [saveState, setSaveState] = useState<
|
|
503
|
+
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">(
|
|
504
|
+
"idle",
|
|
505
|
+
);
|
|
393
506
|
|
|
394
507
|
// Simple save function (defined BEFORE cell actions that use it)
|
|
395
508
|
const saveNotebook = useCallback(() => {
|
|
396
|
-
setSaveState(
|
|
509
|
+
setSaveState("saving");
|
|
397
510
|
wsRef.current?.saveNotebook();
|
|
398
511
|
|
|
399
512
|
// Show "Saving..." for 500ms, then "Saved" for 2 seconds
|
|
400
513
|
setTimeout(() => {
|
|
401
|
-
setSaveState(
|
|
514
|
+
setSaveState("saved");
|
|
402
515
|
setTimeout(() => {
|
|
403
|
-
setSaveState(
|
|
516
|
+
setSaveState("idle");
|
|
404
517
|
}, 2000);
|
|
405
518
|
}, 500);
|
|
406
519
|
}, []);
|
|
407
520
|
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
521
|
+
// --- Cell Actions ---
|
|
522
|
+
|
|
523
|
+
const executeCell = useCallback(
|
|
524
|
+
(index: number) => {
|
|
525
|
+
const cell = cells[index];
|
|
526
|
+
if (!cell) return;
|
|
527
|
+
|
|
528
|
+
if (cell.cell_type === "markdown") {
|
|
529
|
+
// Save before rendering markdown
|
|
413
530
|
saveNotebook();
|
|
531
|
+
// Markdown rendering is handled locally in Cell.tsx now
|
|
532
|
+
return;
|
|
414
533
|
}
|
|
415
|
-
};
|
|
416
534
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
535
|
+
// Save before executing code
|
|
536
|
+
saveNotebook();
|
|
537
|
+
wsRef.current?.executeCell(index, cell.source);
|
|
538
|
+
},
|
|
539
|
+
[cells, saveNotebook],
|
|
540
|
+
);
|
|
420
541
|
|
|
421
|
-
|
|
542
|
+
const interruptCell = useCallback((index: number) => {
|
|
543
|
+
wsRef.current?.interruptKernel(index);
|
|
544
|
+
}, []);
|
|
422
545
|
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
546
|
+
const deleteCell = useCallback(
|
|
547
|
+
(index: number) => {
|
|
548
|
+
const cell = cells[index];
|
|
549
|
+
if (!cell) return;
|
|
426
550
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
551
|
+
// Add to deletion queue before deleting
|
|
552
|
+
const deletedCell: DeletedCell = {
|
|
553
|
+
cell: { ...cell },
|
|
554
|
+
index,
|
|
555
|
+
timestamp: Date.now(),
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// Add to front of queue (most recent first)
|
|
559
|
+
deletionQueueRef.current.unshift(deletedCell);
|
|
560
|
+
|
|
561
|
+
// Show undo hint
|
|
562
|
+
setShowUndoHint(true);
|
|
563
|
+
setTimeout(() => setShowUndoHint(false), 5000); // Hide after 5 seconds
|
|
564
|
+
|
|
565
|
+
wsRef.current?.deleteCell(index);
|
|
566
|
+
},
|
|
567
|
+
[cells],
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const undoDelete = useCallback(() => {
|
|
571
|
+
const deletedCell = deletionQueueRef.current.shift(); // Get most recent deletion
|
|
572
|
+
if (!deletedCell) {
|
|
573
|
+
console.log("No deletions to undo");
|
|
431
574
|
return;
|
|
432
575
|
}
|
|
433
576
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
wsRef.current?.executeCell(index, cell.source);
|
|
437
|
-
}, [cells, saveNotebook]);
|
|
577
|
+
// Hide undo hint when user actually undoes
|
|
578
|
+
setShowUndoHint(false);
|
|
438
579
|
|
|
439
|
-
|
|
440
|
-
wsRef.current?.
|
|
580
|
+
// Add the cell back at its original index with full cell data (outputs, metadata, etc.)
|
|
581
|
+
wsRef.current?.addCell(
|
|
582
|
+
deletedCell.index,
|
|
583
|
+
deletedCell.cell.cell_type,
|
|
584
|
+
deletedCell.cell.source,
|
|
585
|
+
deletedCell.cell, // Pass full cell to restore outputs, execution_count, metadata
|
|
586
|
+
);
|
|
441
587
|
}, []);
|
|
442
588
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
589
|
+
// Keyboard shortcuts (Cmd+S / Ctrl+S for save, Ctrl+Z for undo)
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
592
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
e.stopPropagation();
|
|
595
|
+
saveNotebook();
|
|
596
|
+
}
|
|
597
|
+
// Only intercept Ctrl+Z for cell deletion undo (not editor undo)
|
|
598
|
+
// Check if we have deletions in queue and we're not focused in an editor
|
|
599
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
|
|
600
|
+
const hasDeletions = deletionQueueRef.current.length > 0;
|
|
601
|
+
const target = e.target as HTMLElement;
|
|
602
|
+
const isInEditor =
|
|
603
|
+
target.closest(".cm-editor") ||
|
|
604
|
+
target.tagName === "TEXTAREA" ||
|
|
605
|
+
target.tagName === "INPUT";
|
|
606
|
+
|
|
607
|
+
// If we have deletions and NOT in an editor, intercept for cell undo
|
|
608
|
+
if (hasDeletions && !isInEditor) {
|
|
609
|
+
e.preventDefault();
|
|
610
|
+
e.stopPropagation();
|
|
611
|
+
undoDelete();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Use capture phase to intercept before CodeMirror
|
|
617
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
618
|
+
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
|
619
|
+
}, [saveNotebook, undoDelete]);
|
|
446
620
|
|
|
447
621
|
const updateCell = useCallback((index: number, source: string) => {
|
|
448
|
-
dispatch({
|
|
622
|
+
dispatch({
|
|
623
|
+
type: "UPDATE_CELL_SOURCE",
|
|
624
|
+
payload: { cell_index: index, source },
|
|
625
|
+
});
|
|
449
626
|
wsRef.current?.updateCell(index, source);
|
|
450
627
|
}, []);
|
|
451
628
|
|
|
452
|
-
const addCell = useCallback(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
629
|
+
const addCell = useCallback(
|
|
630
|
+
(type: "code" | "markdown" = "code", index: number) => {
|
|
631
|
+
wsRef.current?.addCell(index, type);
|
|
632
|
+
setCurrentCellIndex(index);
|
|
633
|
+
},
|
|
634
|
+
[],
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const moveUp = useCallback((index: number) => {
|
|
638
|
+
if (index > 0) {
|
|
639
|
+
wsRef.current?.moveCell(index, index - 1);
|
|
640
|
+
// Save after moving
|
|
641
|
+
saveNotebook();
|
|
642
|
+
}
|
|
643
|
+
}, [saveNotebook]);
|
|
644
|
+
|
|
645
|
+
const moveDown = useCallback((index: number) => {
|
|
646
|
+
if (index < cells.length - 1) {
|
|
647
|
+
wsRef.current?.moveCell(index, index + 1);
|
|
648
|
+
// Save after moving
|
|
649
|
+
saveNotebook();
|
|
650
|
+
}
|
|
651
|
+
}, [cells.length, saveNotebook]);
|
|
456
652
|
|
|
457
653
|
const resetKernel = () => {
|
|
458
|
-
if (
|
|
654
|
+
if (
|
|
655
|
+
confirm(
|
|
656
|
+
"Are you sure you want to restart the kernel? All variables will be lost.",
|
|
657
|
+
)
|
|
658
|
+
) {
|
|
459
659
|
wsRef.current?.resetKernel();
|
|
460
|
-
dispatch({ type:
|
|
660
|
+
dispatch({ type: "RESET_KERNEL" });
|
|
461
661
|
}
|
|
462
662
|
};
|
|
463
663
|
|
|
@@ -466,37 +666,66 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
|
|
|
466
666
|
return (
|
|
467
667
|
<>
|
|
468
668
|
{/* Save Status Indicator - show saving and saved states */}
|
|
469
|
-
{saveState !==
|
|
669
|
+
{saveState !== "idle" && (
|
|
470
670
|
<div
|
|
471
671
|
style={{
|
|
472
|
-
position:
|
|
473
|
-
bottom:
|
|
474
|
-
right:
|
|
475
|
-
padding:
|
|
476
|
-
borderRadius:
|
|
477
|
-
fontSize:
|
|
672
|
+
position: "fixed",
|
|
673
|
+
bottom: "20px",
|
|
674
|
+
right: "20px",
|
|
675
|
+
padding: "8px 14px",
|
|
676
|
+
borderRadius: "8px",
|
|
677
|
+
fontSize: "12px",
|
|
478
678
|
fontWeight: 500,
|
|
479
|
-
backgroundColor: saveState ===
|
|
480
|
-
color: saveState ===
|
|
481
|
-
border: `1px solid ${saveState ===
|
|
482
|
-
display:
|
|
483
|
-
alignItems:
|
|
484
|
-
gap:
|
|
679
|
+
backgroundColor: saveState === "saved" ? "#10b98114" : "#3b82f614",
|
|
680
|
+
color: saveState === "saved" ? "#10b981" : "#3b82f6",
|
|
681
|
+
border: `1px solid ${saveState === "saved" ? "#10b981" : "#3b82f6"}`,
|
|
682
|
+
display: "flex",
|
|
683
|
+
alignItems: "center",
|
|
684
|
+
gap: "8px",
|
|
485
685
|
zIndex: 1000,
|
|
486
|
-
boxShadow:
|
|
487
|
-
animation:
|
|
686
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
|
687
|
+
animation: "fadeIn 0.2s ease",
|
|
488
688
|
}}
|
|
489
689
|
>
|
|
490
|
-
{saveState ===
|
|
491
|
-
{saveState ===
|
|
690
|
+
{saveState === "saving" && "⟳ Saving..."}
|
|
691
|
+
{saveState === "saved" && "✓ Saved"}
|
|
492
692
|
</div>
|
|
493
693
|
)}
|
|
494
694
|
|
|
695
|
+
{/* Undo Hint - show when cells are deleted */}
|
|
696
|
+
{showUndoHint && (
|
|
697
|
+
<div
|
|
698
|
+
style={{
|
|
699
|
+
position: "fixed",
|
|
700
|
+
bottom: "20px",
|
|
701
|
+
left: "20px",
|
|
702
|
+
padding: "8px 14px",
|
|
703
|
+
borderRadius: "8px",
|
|
704
|
+
fontSize: "12px",
|
|
705
|
+
fontWeight: 500,
|
|
706
|
+
backgroundColor: "#f59e0b14",
|
|
707
|
+
color: "#f59e0b",
|
|
708
|
+
border: "1px solid #f59e0b",
|
|
709
|
+
display: "flex",
|
|
710
|
+
alignItems: "center",
|
|
711
|
+
gap: "8px",
|
|
712
|
+
zIndex: 1000,
|
|
713
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
|
714
|
+
animation: "fadeIn 0.2s ease",
|
|
715
|
+
}}
|
|
716
|
+
>
|
|
717
|
+
Cell deleted. Press{" "}
|
|
718
|
+
{navigator.platform.includes("Mac") ? "⌘Z" : "Ctrl+Z"} to undo
|
|
719
|
+
</div>
|
|
720
|
+
)}
|
|
721
|
+
|
|
722
|
+
{/* Cell List */}
|
|
495
723
|
{cells.map((cell, index) => (
|
|
496
724
|
<CellComponent
|
|
497
725
|
key={cell.id}
|
|
498
726
|
cell={cell}
|
|
499
727
|
index={index}
|
|
728
|
+
totalCells={cells.length}
|
|
500
729
|
isActive={currentCellIndex === index}
|
|
501
730
|
isExecuting={executingCells.has(index)}
|
|
502
731
|
onExecute={executeCell}
|
|
@@ -505,16 +734,18 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
|
|
|
505
734
|
onUpdate={updateCell}
|
|
506
735
|
onSetActive={setCurrentCellIndex}
|
|
507
736
|
onAddCell={addCell}
|
|
737
|
+
onMoveUp={moveUp}
|
|
738
|
+
onMoveDown={moveDown}
|
|
508
739
|
/>
|
|
509
740
|
))}
|
|
510
741
|
|
|
511
742
|
{cells.length === 0 && (
|
|
512
743
|
<div id="empty-state" className="empty-state">
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
744
|
+
<div className="add-cell-line" data-position="0">
|
|
745
|
+
<AddCellButton onAddCell={(type) => addCell(type, 0)} />
|
|
746
|
+
</div>
|
|
516
747
|
</div>
|
|
517
748
|
)}
|
|
518
749
|
</>
|
|
519
750
|
);
|
|
520
|
-
};
|
|
751
|
+
};
|