more-compute 0.1.4__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 +159 -42
- more_compute-0.2.0.dist-info/METADATA +126 -0
- more_compute-0.2.0.dist-info/RECORD +100 -0
- morecompute/__version__.py +1 -1
- 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.4.dist-info/METADATA +0 -173
- more_compute-0.1.4.dist-info/RECORD +0 -86
- /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.4.dist-info → more_compute-0.2.0.dist-info}/WHEEL +0 -0
- {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
4
|
+
import Editor, { Monaco } from "@monaco-editor/react";
|
|
5
|
+
import * as monaco from "monaco-editor";
|
|
6
|
+
import { Cell as CellType } from "@/types/notebook";
|
|
7
|
+
import CellOutput from "../output/CellOutput";
|
|
8
|
+
import AddCellButton from "./AddCellButton";
|
|
9
|
+
import MarkdownRenderer from "../output/MarkdownRenderer";
|
|
10
|
+
import CellButton from "./CellButton";
|
|
11
|
+
import {
|
|
12
|
+
UpdateIcon,
|
|
13
|
+
LinkBreak2Icon,
|
|
14
|
+
PlayIcon,
|
|
15
|
+
ChevronUpIcon,
|
|
16
|
+
ChevronDownIcon,
|
|
17
|
+
} from "@radix-ui/react-icons";
|
|
18
|
+
import { Check, X } from "lucide-react";
|
|
19
|
+
import { fixIndentation } from "@/lib/api";
|
|
20
|
+
import { loadMonacoThemes } from "@/lib/monaco-themes";
|
|
21
|
+
import { loadSettings } from "@/lib/settings";
|
|
22
|
+
|
|
23
|
+
interface CellProps {
|
|
24
|
+
cell: CellType;
|
|
25
|
+
index: number;
|
|
26
|
+
totalCells: number;
|
|
27
|
+
isActive: boolean;
|
|
28
|
+
isExecuting: boolean;
|
|
29
|
+
onExecute: (index: number) => void;
|
|
30
|
+
onInterrupt: (index: number) => void;
|
|
31
|
+
onDelete: (index: number) => void;
|
|
32
|
+
onUpdate: (index: number, source: string) => void;
|
|
33
|
+
onSetActive: (index: number) => void;
|
|
34
|
+
onAddCell: (type: "code" | "markdown", index: number) => void;
|
|
35
|
+
onMoveUp: (index: number) => void;
|
|
36
|
+
onMoveDown: (index: number) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Global registry of cell editors for LSP
|
|
40
|
+
const cellEditors = new Map<string, monaco.editor.IStandaloneCodeEditor>();
|
|
41
|
+
|
|
42
|
+
// Global flag to ensure providers are registered only once
|
|
43
|
+
let providersRegistered = false;
|
|
44
|
+
// Global flag to ensure themes are loaded only once
|
|
45
|
+
let themesLoaded = false;
|
|
46
|
+
|
|
47
|
+
// Load Monaco themes globally once
|
|
48
|
+
function loadMonacoThemesGlobally(monacoInstance: Monaco) {
|
|
49
|
+
if (themesLoaded) return;
|
|
50
|
+
themesLoaded = true;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
loadMonacoThemes(monacoInstance);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('Failed to load Monaco themes:', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Register global LSP providers once
|
|
60
|
+
function registerGlobalLSPProviders(monacoInstance: Monaco) {
|
|
61
|
+
if (providersRegistered) return;
|
|
62
|
+
providersRegistered = true;
|
|
63
|
+
|
|
64
|
+
// Global completion provider that looks up the editor by model URI
|
|
65
|
+
monacoInstance.languages.registerCompletionItemProvider("python", {
|
|
66
|
+
async provideCompletionItems(model, position) {
|
|
67
|
+
try {
|
|
68
|
+
const cellId = extractCellIdFromUri(model.uri.toString());
|
|
69
|
+
if (!cellId) return { suggestions: [] };
|
|
70
|
+
|
|
71
|
+
const source = model.getValue();
|
|
72
|
+
const response = await fetch("/api/lsp/completions", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
cell_id: cellId,
|
|
77
|
+
source,
|
|
78
|
+
line: position.lineNumber - 1,
|
|
79
|
+
character: position.column - 1,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!response.ok) return { suggestions: [] };
|
|
84
|
+
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
const completions = data.completions || [];
|
|
87
|
+
|
|
88
|
+
const suggestions: monaco.languages.CompletionItem[] = completions.map(
|
|
89
|
+
(item: any) => ({
|
|
90
|
+
label: item.label,
|
|
91
|
+
kind: mapCompletionKind(item.kind, monacoInstance),
|
|
92
|
+
detail: item.detail,
|
|
93
|
+
documentation: item.documentation?.value || item.documentation,
|
|
94
|
+
insertText: item.insertText || item.label,
|
|
95
|
+
range: new monacoInstance.Range(
|
|
96
|
+
position.lineNumber,
|
|
97
|
+
position.column,
|
|
98
|
+
position.lineNumber,
|
|
99
|
+
position.column
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return { suggestions };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("LSP completion error:", error);
|
|
107
|
+
return { suggestions: [] };
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
triggerCharacters: [".", "(", "["],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Global hover provider
|
|
114
|
+
monacoInstance.languages.registerHoverProvider("python", {
|
|
115
|
+
async provideHover(model, position) {
|
|
116
|
+
try {
|
|
117
|
+
const cellId = extractCellIdFromUri(model.uri.toString());
|
|
118
|
+
if (!cellId) return null;
|
|
119
|
+
|
|
120
|
+
const source = model.getValue();
|
|
121
|
+
const response = await fetch("/api/lsp/hover", {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
cell_id: cellId,
|
|
126
|
+
source,
|
|
127
|
+
line: position.lineNumber - 1,
|
|
128
|
+
character: position.column - 1,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) return null;
|
|
133
|
+
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
const hover = data.hover;
|
|
136
|
+
|
|
137
|
+
if (!hover || !hover.contents) return null;
|
|
138
|
+
|
|
139
|
+
const contents = Array.isArray(hover.contents)
|
|
140
|
+
? hover.contents
|
|
141
|
+
: [hover.contents];
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
contents: contents.map((content: any) => ({
|
|
145
|
+
value: typeof content === "string" ? content : content.value,
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error("LSP hover error:", error);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractCellIdFromUri(uri: string): string | null {
|
|
157
|
+
// Monaco creates URIs like "inmemory://model/1", "inmemory://model/2", etc.
|
|
158
|
+
// We need to extract the model number and map it to a cell ID
|
|
159
|
+
const match = uri.match(/model\/(\d+)$/);
|
|
160
|
+
if (match) {
|
|
161
|
+
const modelNumber = match[1];
|
|
162
|
+
// Find the editor with this model
|
|
163
|
+
for (const [cellId, editor] of cellEditors.entries()) {
|
|
164
|
+
if (editor.getModel()?.uri.toString().includes(modelNumber)) {
|
|
165
|
+
return cellId;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mapCompletionKind(
|
|
173
|
+
lspKind: number,
|
|
174
|
+
monacoInstance: Monaco
|
|
175
|
+
): monaco.languages.CompletionItemKind {
|
|
176
|
+
const kindMap: { [key: number]: monaco.languages.CompletionItemKind } = {
|
|
177
|
+
1: monacoInstance.languages.CompletionItemKind.Text,
|
|
178
|
+
2: monacoInstance.languages.CompletionItemKind.Method,
|
|
179
|
+
3: monacoInstance.languages.CompletionItemKind.Function,
|
|
180
|
+
4: monacoInstance.languages.CompletionItemKind.Constructor,
|
|
181
|
+
5: monacoInstance.languages.CompletionItemKind.Field,
|
|
182
|
+
6: monacoInstance.languages.CompletionItemKind.Variable,
|
|
183
|
+
7: monacoInstance.languages.CompletionItemKind.Class,
|
|
184
|
+
8: monacoInstance.languages.CompletionItemKind.Interface,
|
|
185
|
+
9: monacoInstance.languages.CompletionItemKind.Module,
|
|
186
|
+
10: monacoInstance.languages.CompletionItemKind.Property,
|
|
187
|
+
11: monacoInstance.languages.CompletionItemKind.Unit,
|
|
188
|
+
12: monacoInstance.languages.CompletionItemKind.Value,
|
|
189
|
+
13: monacoInstance.languages.CompletionItemKind.Enum,
|
|
190
|
+
14: monacoInstance.languages.CompletionItemKind.Keyword,
|
|
191
|
+
15: monacoInstance.languages.CompletionItemKind.Snippet,
|
|
192
|
+
16: monacoInstance.languages.CompletionItemKind.Color,
|
|
193
|
+
17: monacoInstance.languages.CompletionItemKind.File,
|
|
194
|
+
18: monacoInstance.languages.CompletionItemKind.Reference,
|
|
195
|
+
19: monacoInstance.languages.CompletionItemKind.Folder,
|
|
196
|
+
20: monacoInstance.languages.CompletionItemKind.EnumMember,
|
|
197
|
+
21: monacoInstance.languages.CompletionItemKind.Constant,
|
|
198
|
+
22: monacoInstance.languages.CompletionItemKind.Struct,
|
|
199
|
+
23: monacoInstance.languages.CompletionItemKind.Event,
|
|
200
|
+
24: monacoInstance.languages.CompletionItemKind.Operator,
|
|
201
|
+
25: monacoInstance.languages.CompletionItemKind.TypeParameter,
|
|
202
|
+
};
|
|
203
|
+
return kindMap[lspKind] || monacoInstance.languages.CompletionItemKind.Text;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
export const MonacoCell: React.FC<CellProps> = ({
|
|
208
|
+
cell,
|
|
209
|
+
index,
|
|
210
|
+
totalCells,
|
|
211
|
+
isActive,
|
|
212
|
+
isExecuting,
|
|
213
|
+
onExecute,
|
|
214
|
+
onDelete,
|
|
215
|
+
onInterrupt,
|
|
216
|
+
onUpdate,
|
|
217
|
+
onSetActive,
|
|
218
|
+
onAddCell,
|
|
219
|
+
onMoveUp,
|
|
220
|
+
onMoveDown,
|
|
221
|
+
}) => {
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// REFS & STATE
|
|
224
|
+
// ============================================================================
|
|
225
|
+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
226
|
+
const monacoRef = useRef<Monaco | null>(null);
|
|
227
|
+
const wasEditingMarkdown = useRef(false);
|
|
228
|
+
const indexRef = useRef<number>(index);
|
|
229
|
+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
230
|
+
const disposablesRef = useRef<monaco.IDisposable[]>([]);
|
|
231
|
+
const isUnmountingRef = useRef(false);
|
|
232
|
+
|
|
233
|
+
const [isEditing, setIsEditing] = useState(
|
|
234
|
+
() => cell.cell_type === "code" || !cell.source?.trim()
|
|
235
|
+
);
|
|
236
|
+
const [elapsedLabel, setElapsedLabel] = useState<string | null>(
|
|
237
|
+
cell.execution_time ?? null
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// UTILITIES
|
|
242
|
+
// ============================================================================
|
|
243
|
+
const formatMs = (ms: number): string => {
|
|
244
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
245
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
246
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
247
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
248
|
+
const seconds = totalSeconds % 60;
|
|
249
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}s`;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const parseExecTime = (s?: string | null): number | null => {
|
|
253
|
+
if (!s) return null;
|
|
254
|
+
if (s.endsWith("ms")) return parseFloat(s.replace("ms", ""));
|
|
255
|
+
if (s.endsWith("s")) return parseFloat(s.replace("s", "")) * 1000;
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// COMPUTED VALUES
|
|
261
|
+
// ============================================================================
|
|
262
|
+
const isMarkdownWithContent =
|
|
263
|
+
cell.cell_type === "markdown" && !isEditing && cell.source?.trim();
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// HANDLERS
|
|
267
|
+
// ============================================================================
|
|
268
|
+
const handleExecute = useCallback(() => {
|
|
269
|
+
if (cell.cell_type === "markdown") {
|
|
270
|
+
onExecute(indexRef.current);
|
|
271
|
+
setIsEditing(false);
|
|
272
|
+
} else {
|
|
273
|
+
if (isExecuting) {
|
|
274
|
+
onInterrupt(indexRef.current);
|
|
275
|
+
} else {
|
|
276
|
+
onExecute(indexRef.current);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}, [cell.cell_type, isExecuting, onExecute, onInterrupt]);
|
|
280
|
+
|
|
281
|
+
const handleCellClick = () => {
|
|
282
|
+
onSetActive(indexRef.current);
|
|
283
|
+
if (cell.cell_type === "markdown") {
|
|
284
|
+
setIsEditing(true);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleFixIndentation = async () => {
|
|
289
|
+
try {
|
|
290
|
+
const fixedCode = await fixIndentation(cell.source);
|
|
291
|
+
onUpdate(indexRef.current, fixedCode);
|
|
292
|
+
if (editorRef.current) {
|
|
293
|
+
editorRef.current.setValue(fixedCode);
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error("Failed to fix indentation:", err);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// MONACO SETUP
|
|
302
|
+
// ============================================================================
|
|
303
|
+
const handleEditorWillMount = (monacoInstance: Monaco) => {
|
|
304
|
+
// Clean up any previous editor instance before mounting new one
|
|
305
|
+
if (editorRef.current) {
|
|
306
|
+
try {
|
|
307
|
+
editorRef.current.dispose();
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// Ignore
|
|
310
|
+
}
|
|
311
|
+
editorRef.current = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Load themes globally once
|
|
315
|
+
loadMonacoThemesGlobally(monacoInstance);
|
|
316
|
+
|
|
317
|
+
// Apply theme globally before editor is created
|
|
318
|
+
const settings = loadSettings();
|
|
319
|
+
const userTheme = settings.theme;
|
|
320
|
+
|
|
321
|
+
if (userTheme === 'light') {
|
|
322
|
+
monacoInstance.editor.setTheme('vs');
|
|
323
|
+
} else if (userTheme === 'dark') {
|
|
324
|
+
monacoInstance.editor.setTheme('vs-dark');
|
|
325
|
+
} else {
|
|
326
|
+
try {
|
|
327
|
+
monacoInstance.editor.setTheme(userTheme);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
|
|
330
|
+
monacoInstance.editor.setTheme(isDark ? 'vs-dark' : 'vs');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const handleEditorDidMount = (
|
|
336
|
+
editor: monaco.editor.IStandaloneCodeEditor,
|
|
337
|
+
monacoInstance: Monaco
|
|
338
|
+
) => {
|
|
339
|
+
editorRef.current = editor;
|
|
340
|
+
monacoRef.current = monacoInstance;
|
|
341
|
+
|
|
342
|
+
// Register global LSP providers once
|
|
343
|
+
registerGlobalLSPProviders(monacoInstance);
|
|
344
|
+
|
|
345
|
+
// Apply theme again after editor is mounted to ensure it takes effect
|
|
346
|
+
const settings = loadSettings();
|
|
347
|
+
const userTheme = settings.theme;
|
|
348
|
+
|
|
349
|
+
if (userTheme === 'light') {
|
|
350
|
+
monacoInstance.editor.setTheme('vs');
|
|
351
|
+
} else if (userTheme === 'dark') {
|
|
352
|
+
monacoInstance.editor.setTheme('vs-dark');
|
|
353
|
+
} else {
|
|
354
|
+
try {
|
|
355
|
+
monacoInstance.editor.setTheme(userTheme);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
|
|
358
|
+
monacoInstance.editor.setTheme(isDark ? 'vs-dark' : 'vs');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Register this cell's editor in the global map for LSP
|
|
363
|
+
if (cell.cell_type === "code") {
|
|
364
|
+
cellEditors.set(cell.id, editor);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Focus editor if active
|
|
368
|
+
if (isActive) {
|
|
369
|
+
editor.focus();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle content changes
|
|
373
|
+
const changeDisposable = editor.onDidChangeModelContent(() => {
|
|
374
|
+
if (!isUnmountingRef.current) {
|
|
375
|
+
onUpdate(indexRef.current, editor.getValue());
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Handle Shift+Enter to execute
|
|
380
|
+
const keyDisposable = editor.addCommand(
|
|
381
|
+
monacoInstance.KeyMod.Shift | monacoInstance.KeyCode.Enter,
|
|
382
|
+
() => {
|
|
383
|
+
handleExecute();
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Markdown blur behavior
|
|
388
|
+
if (cell.cell_type === "markdown") {
|
|
389
|
+
const blurDisposable = editor.onDidBlurEditorText(() => {
|
|
390
|
+
if (!isUnmountingRef.current) {
|
|
391
|
+
if (cell.source?.trim()) {
|
|
392
|
+
onExecute(indexRef.current);
|
|
393
|
+
setIsEditing(false);
|
|
394
|
+
}
|
|
395
|
+
wasEditingMarkdown.current = false;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
disposablesRef.current.push(blurDisposable);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (changeDisposable && typeof changeDisposable.dispose === 'function') {
|
|
402
|
+
disposablesRef.current.push(changeDisposable);
|
|
403
|
+
}
|
|
404
|
+
// Note: addCommand returns a command ID (string), not a disposable
|
|
405
|
+
// We don't need to track it for cleanup
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// EFFECTS
|
|
410
|
+
// ============================================================================
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
indexRef.current = index;
|
|
413
|
+
}, [index]);
|
|
414
|
+
|
|
415
|
+
// Execution timer
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
if (isExecuting) {
|
|
418
|
+
const start = Date.now();
|
|
419
|
+
setElapsedLabel("0ms");
|
|
420
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
421
|
+
intervalRef.current = setInterval(() => {
|
|
422
|
+
setElapsedLabel(formatMs(Date.now() - start));
|
|
423
|
+
}, 100);
|
|
424
|
+
} else {
|
|
425
|
+
if (intervalRef.current) {
|
|
426
|
+
clearInterval(intervalRef.current);
|
|
427
|
+
intervalRef.current = null;
|
|
428
|
+
}
|
|
429
|
+
const ms = parseExecTime(cell.execution_time as any);
|
|
430
|
+
if (ms != null) setElapsedLabel(formatMs(ms));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return () => {
|
|
434
|
+
if (intervalRef.current) {
|
|
435
|
+
clearInterval(intervalRef.current);
|
|
436
|
+
intervalRef.current = null;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}, [isExecuting, cell.execution_time]);
|
|
440
|
+
|
|
441
|
+
// Track markdown editing
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
if (isActive && cell.cell_type === "markdown" && isEditing) {
|
|
444
|
+
wasEditingMarkdown.current = true;
|
|
445
|
+
}
|
|
446
|
+
}, [isActive, cell.cell_type, isEditing]);
|
|
447
|
+
|
|
448
|
+
// Auto-save markdown on blur
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (
|
|
451
|
+
!isActive &&
|
|
452
|
+
wasEditingMarkdown.current &&
|
|
453
|
+
cell.cell_type === "markdown"
|
|
454
|
+
) {
|
|
455
|
+
if (cell.source?.trim()) {
|
|
456
|
+
onExecute(indexRef.current);
|
|
457
|
+
setIsEditing(false);
|
|
458
|
+
}
|
|
459
|
+
wasEditingMarkdown.current = false;
|
|
460
|
+
}
|
|
461
|
+
}, [isActive, cell.cell_type, cell.source, onExecute]);
|
|
462
|
+
|
|
463
|
+
// Cleanup
|
|
464
|
+
useEffect(() => {
|
|
465
|
+
// Reset unmounting flag on mount
|
|
466
|
+
isUnmountingRef.current = false;
|
|
467
|
+
|
|
468
|
+
return () => {
|
|
469
|
+
// Set unmounting flag to prevent async operations
|
|
470
|
+
isUnmountingRef.current = true;
|
|
471
|
+
|
|
472
|
+
// Dispose the editor instance first
|
|
473
|
+
if (editorRef.current) {
|
|
474
|
+
try {
|
|
475
|
+
const model = editorRef.current.getModel();
|
|
476
|
+
|
|
477
|
+
// Stop any pending operations
|
|
478
|
+
if (editorRef.current) {
|
|
479
|
+
editorRef.current.dispose();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (model && !model.isDisposed()) {
|
|
483
|
+
model.dispose();
|
|
484
|
+
}
|
|
485
|
+
} catch (e) {
|
|
486
|
+
// Silently ignore errors during cleanup
|
|
487
|
+
}
|
|
488
|
+
editorRef.current = null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Remove editor from global registry
|
|
492
|
+
if (cell.cell_type === "code") {
|
|
493
|
+
cellEditors.delete(cell.id);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Dispose all Monaco disposables
|
|
497
|
+
disposablesRef.current.forEach((d) => {
|
|
498
|
+
if (d && typeof d.dispose === 'function') {
|
|
499
|
+
try {
|
|
500
|
+
d.dispose();
|
|
501
|
+
} catch (e) {
|
|
502
|
+
// Silently ignore errors during cleanup
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
disposablesRef.current = [];
|
|
507
|
+
};
|
|
508
|
+
}, [cell.id, cell.cell_type]);
|
|
509
|
+
|
|
510
|
+
// Focus when active
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
if (isActive && editorRef.current) {
|
|
513
|
+
editorRef.current.focus();
|
|
514
|
+
}
|
|
515
|
+
}, [isActive]);
|
|
516
|
+
|
|
517
|
+
// Listen for theme changes and update Monaco editor
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
const handleThemeChange = () => {
|
|
520
|
+
if (!monacoRef.current) return;
|
|
521
|
+
|
|
522
|
+
const settings = loadSettings();
|
|
523
|
+
const userTheme = settings.theme;
|
|
524
|
+
|
|
525
|
+
if (userTheme === 'light') {
|
|
526
|
+
monacoRef.current.editor.setTheme('vs');
|
|
527
|
+
} else if (userTheme === 'dark') {
|
|
528
|
+
monacoRef.current.editor.setTheme('vs-dark');
|
|
529
|
+
} else {
|
|
530
|
+
try {
|
|
531
|
+
monacoRef.current.editor.setTheme(userTheme);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
|
|
534
|
+
monacoRef.current.editor.setTheme(isDark ? 'vs-dark' : 'vs');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Listen for storage events (theme changes from settings)
|
|
540
|
+
window.addEventListener('storage', handleThemeChange);
|
|
541
|
+
// Also listen for custom theme change event
|
|
542
|
+
window.addEventListener('themeChanged', handleThemeChange);
|
|
543
|
+
|
|
544
|
+
return () => {
|
|
545
|
+
window.removeEventListener('storage', handleThemeChange);
|
|
546
|
+
window.removeEventListener('themeChanged', handleThemeChange);
|
|
547
|
+
};
|
|
548
|
+
}, []);
|
|
549
|
+
|
|
550
|
+
// ============================================================================
|
|
551
|
+
// RENDER
|
|
552
|
+
// ============================================================================
|
|
553
|
+
return (
|
|
554
|
+
<div className="cell-wrapper">
|
|
555
|
+
{/* Status Indicator */}
|
|
556
|
+
{!isMarkdownWithContent && (
|
|
557
|
+
<div className="cell-status-indicator">
|
|
558
|
+
<span className="status-indicator">
|
|
559
|
+
<span className="status-bracket">[</span>
|
|
560
|
+
{isExecuting ? (
|
|
561
|
+
<UpdateIcon className="w-1 h-1" />
|
|
562
|
+
) : cell.error ? (
|
|
563
|
+
<X size={14} color="#dc2626" />
|
|
564
|
+
) : cell.execution_count != null ? (
|
|
565
|
+
<Check size={14} color="#16a34a" />
|
|
566
|
+
) : (
|
|
567
|
+
<span
|
|
568
|
+
style={{
|
|
569
|
+
width: "14px",
|
|
570
|
+
height: "14px",
|
|
571
|
+
display: "inline-block",
|
|
572
|
+
}}
|
|
573
|
+
></span>
|
|
574
|
+
)}
|
|
575
|
+
<span className="status-bracket">]</span>
|
|
576
|
+
</span>
|
|
577
|
+
{elapsedLabel && (
|
|
578
|
+
<span className="status-timer" title="Execution time">
|
|
579
|
+
{elapsedLabel}
|
|
580
|
+
</span>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{/* Add Cell Above Button */}
|
|
586
|
+
<div className="add-cell-line add-line-above">
|
|
587
|
+
<AddCellButton onAddCell={(type) => onAddCell(type, indexRef.current)} />
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Main Cell Container */}
|
|
591
|
+
<div
|
|
592
|
+
className={`cell ${isActive ? "active" : ""} ${isExecuting ? "executing" : ""} ${isMarkdownWithContent ? "markdown-display-mode" : ""}`}
|
|
593
|
+
data-cell-index={index}
|
|
594
|
+
>
|
|
595
|
+
{/* Hover Controls */}
|
|
596
|
+
{!isMarkdownWithContent && (
|
|
597
|
+
<div className="cell-hover-controls">
|
|
598
|
+
<div className="cell-actions-right">
|
|
599
|
+
<CellButton
|
|
600
|
+
icon={<PlayIcon className="w-6 h-6" />}
|
|
601
|
+
onClick={(e) => {
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
handleExecute();
|
|
604
|
+
}}
|
|
605
|
+
title={isExecuting ? "Stop execution" : "Run cell"}
|
|
606
|
+
isLoading={isExecuting}
|
|
607
|
+
/>
|
|
608
|
+
<CellButton
|
|
609
|
+
icon={<ChevronUpIcon className="w-6 h-6" />}
|
|
610
|
+
onClick={(e) => {
|
|
611
|
+
e.stopPropagation();
|
|
612
|
+
onMoveUp(indexRef.current);
|
|
613
|
+
}}
|
|
614
|
+
title="Move cell up"
|
|
615
|
+
disabled={index === 0}
|
|
616
|
+
/>
|
|
617
|
+
<CellButton
|
|
618
|
+
icon={<ChevronDownIcon className="w-6 h-6" />}
|
|
619
|
+
onClick={(e) => {
|
|
620
|
+
e.stopPropagation();
|
|
621
|
+
onMoveDown(indexRef.current);
|
|
622
|
+
}}
|
|
623
|
+
title="Move cell down"
|
|
624
|
+
disabled={index === totalCells - 1}
|
|
625
|
+
/>
|
|
626
|
+
<CellButton
|
|
627
|
+
icon={<LinkBreak2Icon className="w-5 h-5" />}
|
|
628
|
+
onClick={(e) => {
|
|
629
|
+
e.stopPropagation();
|
|
630
|
+
onDelete(indexRef.current);
|
|
631
|
+
}}
|
|
632
|
+
title="Delete cell"
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
)}
|
|
637
|
+
|
|
638
|
+
{/* Cell Content */}
|
|
639
|
+
<div
|
|
640
|
+
className={`cell-content ${isMarkdownWithContent ? "cursor-pointer" : ""}`}
|
|
641
|
+
onClick={handleCellClick}
|
|
642
|
+
>
|
|
643
|
+
<div className="cell-input">
|
|
644
|
+
{isEditing || cell.cell_type === "code" ? (
|
|
645
|
+
<div
|
|
646
|
+
className={`monaco-editor-container ${cell.cell_type === "markdown" ? "markdown-editor-container" : "code-editor-container"}`}
|
|
647
|
+
>
|
|
648
|
+
<Editor
|
|
649
|
+
key={`${cell.id}-${index}`}
|
|
650
|
+
height={Math.max((cell.source.split('\n').length * 19) + 40, 100)}
|
|
651
|
+
defaultLanguage={
|
|
652
|
+
cell.cell_type === "code" ? "python" : "markdown"
|
|
653
|
+
}
|
|
654
|
+
defaultValue={cell.source}
|
|
655
|
+
beforeMount={handleEditorWillMount}
|
|
656
|
+
onMount={handleEditorDidMount}
|
|
657
|
+
options={{
|
|
658
|
+
minimap: { enabled: false },
|
|
659
|
+
lineNumbers: cell.cell_type === "code" ? "on" : "off",
|
|
660
|
+
scrollBeyondLastLine: false,
|
|
661
|
+
wordWrap: "on",
|
|
662
|
+
wrappingStrategy: "advanced",
|
|
663
|
+
fontSize: 14,
|
|
664
|
+
fontFamily: "'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
|
|
665
|
+
lineHeight: 24,
|
|
666
|
+
automaticLayout: true,
|
|
667
|
+
tabSize: 4,
|
|
668
|
+
insertSpaces: true,
|
|
669
|
+
quickSuggestions: cell.cell_type === "code",
|
|
670
|
+
suggestOnTriggerCharacters: cell.cell_type === "code",
|
|
671
|
+
acceptSuggestionOnEnter: "on",
|
|
672
|
+
tabCompletion: "on",
|
|
673
|
+
suggest: {
|
|
674
|
+
showIcons: true,
|
|
675
|
+
showSnippets: true,
|
|
676
|
+
showWords: false,
|
|
677
|
+
insertMode: 'replace',
|
|
678
|
+
filterGraceful: true,
|
|
679
|
+
},
|
|
680
|
+
contextmenu: true,
|
|
681
|
+
folding: cell.cell_type === "code",
|
|
682
|
+
glyphMargin: false,
|
|
683
|
+
lineDecorationsWidth: 0,
|
|
684
|
+
lineNumbersMinChars: 3,
|
|
685
|
+
renderLineHighlight: "none", // Remove grey rectangle
|
|
686
|
+
occurrencesHighlight: "off",
|
|
687
|
+
selectionHighlight: false,
|
|
688
|
+
renderLineHighlightOnlyWhenFocus: false,
|
|
689
|
+
hideCursorInOverviewRuler: true,
|
|
690
|
+
overviewRulerBorder: false,
|
|
691
|
+
overviewRulerLanes: 0,
|
|
692
|
+
// NOTE: fixedOverflowWidgets has known positioning bugs in 2024 - disabled
|
|
693
|
+
padding: { top: 8, bottom: 8 }, // All padding managed by Monaco
|
|
694
|
+
scrollbar: {
|
|
695
|
+
vertical: "auto",
|
|
696
|
+
horizontal: "auto",
|
|
697
|
+
verticalScrollbarSize: 10,
|
|
698
|
+
horizontalScrollbarSize: 10,
|
|
699
|
+
},
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
</div>
|
|
703
|
+
) : (
|
|
704
|
+
<MarkdownRenderer
|
|
705
|
+
source={cell.source}
|
|
706
|
+
onClick={() => setIsEditing(true)}
|
|
707
|
+
/>
|
|
708
|
+
)}
|
|
709
|
+
</div>
|
|
710
|
+
<CellOutput
|
|
711
|
+
outputs={cell.outputs}
|
|
712
|
+
error={cell.error}
|
|
713
|
+
onFixIndentation={handleFixIndentation}
|
|
714
|
+
/>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
{/* Add Cell Below Button */}
|
|
719
|
+
<div className="add-cell-line add-line-below">
|
|
720
|
+
<AddCellButton
|
|
721
|
+
onAddCell={(type) => onAddCell(type, indexRef.current + 1)}
|
|
722
|
+
/>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
);
|
|
726
|
+
};
|