more-compute 0.1.2__py3-none-any.whl → 0.1.3__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 +6 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/METADATA +1 -1
- more_compute-0.1.3.dist-info/RECORD +85 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/top_level.txt +1 -0
- more_compute-0.1.2.dist-info/RECORD +0 -26
- {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/WHEEL +0 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/entry_points.txt +0 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/licenses/LICENSE +0 -0
frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Script from "next/script";
|
|
5
|
+
import Sidebar from "@/components/Sidebar";
|
|
6
|
+
import FolderPopup from "@/components/popups/FolderPopup";
|
|
7
|
+
import PackagesPopup from "@/components/popups/PackagesPopup";
|
|
8
|
+
import PythonPopup from "@/components/popups/PythonPopup";
|
|
9
|
+
import ComputePopup from "@/components/popups/ComputePopup";
|
|
10
|
+
import MetricsPopup from "@/components/popups/MetricsPopup";
|
|
11
|
+
import SettingsPopup from "@/components/popups/SettingsPopup";
|
|
12
|
+
import "./globals.css";
|
|
13
|
+
|
|
14
|
+
export default function RootLayout({
|
|
15
|
+
children,
|
|
16
|
+
}: Readonly<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}>) {
|
|
19
|
+
const [appSettings, setAppSettings] = useState({});
|
|
20
|
+
const [pythonEnvironment, setPythonEnvironment] = useState(null);
|
|
21
|
+
const [activePopup, setActivePopup] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
const handleSettingsChange = (settings: any) => {
|
|
24
|
+
console.log("Settings updated:", settings);
|
|
25
|
+
setAppSettings(settings);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleEnvironmentSwitch = (env: any) => {
|
|
29
|
+
console.log("Switching to environment:", env);
|
|
30
|
+
setPythonEnvironment(env);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const togglePopup = (popupType: string) => {
|
|
34
|
+
setActivePopup((prev) => (prev === popupType ? null : popupType));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const closePopup = () => {
|
|
38
|
+
setActivePopup(null);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const renderPopup = () => {
|
|
42
|
+
if (!activePopup) return null;
|
|
43
|
+
|
|
44
|
+
const props = { onClose: closePopup };
|
|
45
|
+
switch (activePopup) {
|
|
46
|
+
case "folder":
|
|
47
|
+
return <FolderPopup {...props} />;
|
|
48
|
+
case "packages":
|
|
49
|
+
return <PackagesPopup {...props} />;
|
|
50
|
+
case "python":
|
|
51
|
+
return (
|
|
52
|
+
<PythonPopup
|
|
53
|
+
{...props}
|
|
54
|
+
onEnvironmentSwitch={handleEnvironmentSwitch}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
case "compute":
|
|
58
|
+
return <ComputePopup {...props} />;
|
|
59
|
+
case "metrics":
|
|
60
|
+
return <MetricsPopup {...props} />;
|
|
61
|
+
case "settings":
|
|
62
|
+
return (
|
|
63
|
+
<SettingsPopup {...props} onSettingsChange={handleSettingsChange} />
|
|
64
|
+
);
|
|
65
|
+
default:
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getPopupTitle = () => {
|
|
71
|
+
switch (activePopup) {
|
|
72
|
+
case "folder":
|
|
73
|
+
return "Files";
|
|
74
|
+
case "packages":
|
|
75
|
+
return "Packages";
|
|
76
|
+
case "python":
|
|
77
|
+
return "Python Environment";
|
|
78
|
+
case "compute":
|
|
79
|
+
return "Compute Resources";
|
|
80
|
+
case "metrics":
|
|
81
|
+
return "System Metrics";
|
|
82
|
+
case "settings":
|
|
83
|
+
return "Settings";
|
|
84
|
+
default:
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const notebookPath = process.env.NEXT_PUBLIC_NOTEBOOK_PATH || "";
|
|
90
|
+
const notebookRoot = process.env.NEXT_PUBLIC_NOTEBOOK_ROOT || "";
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<html lang="en">
|
|
94
|
+
<head>
|
|
95
|
+
<title>MoreCompute</title>
|
|
96
|
+
<meta name="description" content="Python notebook interface" />
|
|
97
|
+
<link
|
|
98
|
+
rel="stylesheet"
|
|
99
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"
|
|
100
|
+
/>
|
|
101
|
+
<link
|
|
102
|
+
rel="stylesheet"
|
|
103
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/default.min.css"
|
|
104
|
+
/>
|
|
105
|
+
</head>
|
|
106
|
+
<body data-notebook-path={notebookPath} data-notebook-root={notebookRoot}>
|
|
107
|
+
<div id="app">
|
|
108
|
+
<Sidebar onTogglePopup={togglePopup} activePopup={activePopup} />
|
|
109
|
+
<div
|
|
110
|
+
id="popup-overlay"
|
|
111
|
+
className="popup-overlay"
|
|
112
|
+
style={{ display: activePopup ? "flex" : "none" }}
|
|
113
|
+
>
|
|
114
|
+
{activePopup && (
|
|
115
|
+
<div className="popup-content">
|
|
116
|
+
<div className="popup-header">
|
|
117
|
+
<h2 className="popup-title">{getPopupTitle()}</h2>
|
|
118
|
+
<button className="popup-close" onClick={closePopup}>
|
|
119
|
+
×
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="popup-body">{renderPopup()}</div>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
<div
|
|
127
|
+
id="kernel-banner"
|
|
128
|
+
className="kernel-banner"
|
|
129
|
+
style={{ display: "none" }}
|
|
130
|
+
>
|
|
131
|
+
<div className="kernel-message">
|
|
132
|
+
<span className="kernel-status-text">🔴 Kernel Disconnected</span>
|
|
133
|
+
<span className="kernel-subtitle">
|
|
134
|
+
The notebook kernel has stopped running. Restart to continue.
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="kernel-status-bar">
|
|
139
|
+
<div className="kernel-status-indicator">
|
|
140
|
+
<span
|
|
141
|
+
id="kernel-status-dot"
|
|
142
|
+
className="status-dot connecting"
|
|
143
|
+
></span>
|
|
144
|
+
<span id="kernel-status-text" className="status-text">
|
|
145
|
+
Connecting...
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="main-content">{children}</div>
|
|
150
|
+
<div style={{ display: "none" }}>
|
|
151
|
+
<span id="connection-status">Connected</span>
|
|
152
|
+
<span id="kernel-status">Ready</span>
|
|
153
|
+
<img
|
|
154
|
+
id="copy-icon-template"
|
|
155
|
+
src="/assets/icons/copy.svg"
|
|
156
|
+
alt="Copy"
|
|
157
|
+
/>
|
|
158
|
+
<img
|
|
159
|
+
id="check-icon-template"
|
|
160
|
+
src="/assets/icons/check.svg"
|
|
161
|
+
alt="Copied"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<Script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js" />
|
|
166
|
+
<Script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js" />
|
|
167
|
+
<Script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/closebrackets.min.js" />
|
|
168
|
+
<Script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/edit/matchbrackets.min.js" />
|
|
169
|
+
<Script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js" />
|
|
170
|
+
</body>
|
|
171
|
+
</html>
|
|
172
|
+
);
|
|
173
|
+
}
|
frontend/app/page.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { CodeIcon, PlusIcon, TextIcon } from '@radix-ui/react-icons';
|
|
5
|
+
|
|
6
|
+
interface AddCellButtonProps {
|
|
7
|
+
onAddCell: (type: 'code' | 'markdown') => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AddCellButton: React.FC<AddCellButtonProps> = ({ onAddCell }) => {
|
|
11
|
+
const handleAdd = (type: 'code' | 'markdown', e: React.MouseEvent) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
onAddCell(type);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="add-cell-button">
|
|
18
|
+
<div className="cell-type-menu">
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
className="cell-type-option"
|
|
22
|
+
data-type="code"
|
|
23
|
+
onClick={(e) => handleAdd('code', e)}
|
|
24
|
+
>
|
|
25
|
+
<CodeIcon className="w-4 h-4" />
|
|
26
|
+
<span>Code</span>
|
|
27
|
+
</button>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
className="cell-type-option"
|
|
31
|
+
data-type="markdown"
|
|
32
|
+
onClick={(e) => handleAdd('markdown', e)}
|
|
33
|
+
>
|
|
34
|
+
<TextIcon className="w-4 h-4" />
|
|
35
|
+
<span>Text</span>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default AddCellButton;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { Cell as CellType } from '@/types/notebook';
|
|
5
|
+
import CellOutput from './CellOutput';
|
|
6
|
+
import AddCellButton from './AddCellButton';
|
|
7
|
+
import MarkdownRenderer from './MarkdownRenderer';
|
|
8
|
+
import CellButton from './CellButton';
|
|
9
|
+
import { UpdateIcon, LinkBreak2Icon, PlayIcon, RowSpacingIcon } from '@radix-ui/react-icons';
|
|
10
|
+
import { Check, X } from 'lucide-react';
|
|
11
|
+
import { fixIndentation } from '@/lib/api';
|
|
12
|
+
|
|
13
|
+
declare const CodeMirror: any;
|
|
14
|
+
|
|
15
|
+
interface CellProps {
|
|
16
|
+
cell: CellType;
|
|
17
|
+
index: number;
|
|
18
|
+
isActive: boolean;
|
|
19
|
+
isExecuting: boolean;
|
|
20
|
+
onExecute: (index: number) => void;
|
|
21
|
+
onInterrupt: (index: number) => void;
|
|
22
|
+
onDelete: (index: number) => void;
|
|
23
|
+
onUpdate: (index: number, source: string) => void;
|
|
24
|
+
onSetActive: (index: number) => void;
|
|
25
|
+
onAddCell: (type: 'code' | 'markdown', index: number) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Cell: React.FC<CellProps> = ({
|
|
29
|
+
cell,
|
|
30
|
+
index,
|
|
31
|
+
isActive,
|
|
32
|
+
isExecuting,
|
|
33
|
+
onExecute,
|
|
34
|
+
onDelete,
|
|
35
|
+
onInterrupt,
|
|
36
|
+
onUpdate,
|
|
37
|
+
onSetActive,
|
|
38
|
+
onAddCell,
|
|
39
|
+
}) => {
|
|
40
|
+
const editorRef = useRef<HTMLTextAreaElement>(null);
|
|
41
|
+
const codeMirrorInstance = useRef<any>(null);
|
|
42
|
+
// Keep a ref to the latest index to avoid stale closures in event handlers
|
|
43
|
+
const indexRef = useRef<number>(index);
|
|
44
|
+
useEffect(() => { indexRef.current = index; }, [index]);
|
|
45
|
+
|
|
46
|
+
// Execution timer (shows while running and persists final duration afterwards)
|
|
47
|
+
const intervalRef = useRef<any>(null);
|
|
48
|
+
const [elapsedLabel, setElapsedLabel] = useState<string | null>(cell.execution_time ?? null);
|
|
49
|
+
|
|
50
|
+
const formatMs = (ms: number): string => {
|
|
51
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
52
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
53
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
54
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
55
|
+
const seconds = totalSeconds % 60;
|
|
56
|
+
return `${minutes}:${seconds.toString().padStart(2, '0')}s`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const parseExecTime = (s?: string | null): number | null => {
|
|
60
|
+
if (!s) return null;
|
|
61
|
+
// Accept "123.4ms" or "1.2s"
|
|
62
|
+
if (s.endsWith('ms')) return parseFloat(s.replace('ms', ''));
|
|
63
|
+
if (s.endsWith('s')) return parseFloat(s.replace('s', '')) * 1000;
|
|
64
|
+
return null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (isExecuting) {
|
|
69
|
+
const start = Date.now();
|
|
70
|
+
setElapsedLabel('0ms');
|
|
71
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
72
|
+
intervalRef.current = setInterval(() => {
|
|
73
|
+
setElapsedLabel(formatMs(Date.now() - start));
|
|
74
|
+
}, 100);
|
|
75
|
+
} else {
|
|
76
|
+
if (intervalRef.current) {
|
|
77
|
+
clearInterval(intervalRef.current);
|
|
78
|
+
intervalRef.current = null;
|
|
79
|
+
}
|
|
80
|
+
// Persist final time from cell.execution_time if available
|
|
81
|
+
const ms = parseExecTime(cell.execution_time as any);
|
|
82
|
+
if (ms != null) setElapsedLabel(formatMs(ms));
|
|
83
|
+
}
|
|
84
|
+
return () => {
|
|
85
|
+
if (intervalRef.current) {
|
|
86
|
+
clearInterval(intervalRef.current);
|
|
87
|
+
intervalRef.current = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}, [isExecuting, cell.execution_time]);
|
|
91
|
+
const [isEditing, setIsEditing] = useState(() => cell.cell_type === 'code' || !cell.source?.trim());
|
|
92
|
+
|
|
93
|
+
// Determine if this is a markdown cell with content in display mode
|
|
94
|
+
const isMarkdownWithContent = cell.cell_type === 'markdown' && !isEditing && cell.source?.trim();
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (isEditing) {
|
|
98
|
+
if (!codeMirrorInstance.current && editorRef.current && typeof CodeMirror !== 'undefined') {
|
|
99
|
+
const editor = CodeMirror.fromTextArea(editorRef.current, {
|
|
100
|
+
mode: cell.cell_type === 'code' ? 'python' : 'text/plain',
|
|
101
|
+
lineNumbers: cell.cell_type === 'code',
|
|
102
|
+
theme: 'default',
|
|
103
|
+
lineWrapping: true,
|
|
104
|
+
placeholder: cell.cell_type === 'code' ? 'Enter code...' : 'Enter markdown...',
|
|
105
|
+
});
|
|
106
|
+
codeMirrorInstance.current = editor;
|
|
107
|
+
|
|
108
|
+
editor.on('change', (instance: any) => onUpdate(indexRef.current, instance.getValue()));
|
|
109
|
+
editor.on('focus', () => onSetActive(indexRef.current));
|
|
110
|
+
editor.on('blur', () => {
|
|
111
|
+
if (cell.cell_type === 'markdown') setIsEditing(false);
|
|
112
|
+
});
|
|
113
|
+
editor.on('keydown', (instance: any, event: KeyboardEvent) => {
|
|
114
|
+
if (event.shiftKey && event.key === 'Enter') {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
handleExecute();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (editor.getValue() !== cell.source) {
|
|
121
|
+
editor.setValue(cell.source);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (codeMirrorInstance.current) {
|
|
126
|
+
codeMirrorInstance.current.toTextArea();
|
|
127
|
+
codeMirrorInstance.current = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, [isEditing, cell.source]);
|
|
131
|
+
|
|
132
|
+
const handleExecute = () => {
|
|
133
|
+
if (cell.cell_type === 'markdown') {
|
|
134
|
+
onExecute(indexRef.current); // Call onExecute for save logic
|
|
135
|
+
setIsEditing(false);
|
|
136
|
+
} else {
|
|
137
|
+
if (isExecuting) {
|
|
138
|
+
onInterrupt(indexRef.current);
|
|
139
|
+
} else {
|
|
140
|
+
onExecute(indexRef.current);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleCellClick = () => {
|
|
146
|
+
onSetActive(indexRef.current);
|
|
147
|
+
if (cell.cell_type === 'markdown') {
|
|
148
|
+
setIsEditing(true);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleFixIndentation = async () => {
|
|
153
|
+
try {
|
|
154
|
+
const fixedCode = await fixIndentation(cell.source);
|
|
155
|
+
onUpdate(indexRef.current, fixedCode);
|
|
156
|
+
|
|
157
|
+
// Update CodeMirror if it's initialized
|
|
158
|
+
if (codeMirrorInstance.current) {
|
|
159
|
+
codeMirrorInstance.current.setValue(fixedCode);
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error('Failed to fix indentation:', err);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="cell-wrapper">
|
|
168
|
+
{!isMarkdownWithContent && (
|
|
169
|
+
<div className="cell-status-indicator">
|
|
170
|
+
<span className="status-indicator">
|
|
171
|
+
<span className="status-bracket">[</span>
|
|
172
|
+
{isExecuting ? (
|
|
173
|
+
<UpdateIcon className="w-1 h-1" />
|
|
174
|
+
) : cell.error ? (
|
|
175
|
+
<X size={14} color="#dc2626" />
|
|
176
|
+
) : cell.execution_count != null ? (
|
|
177
|
+
<Check size={14} color="#16a34a" />
|
|
178
|
+
) : (
|
|
179
|
+
<span style={{ width: '14px', height: '14px', display: 'inline-block' }}></span>
|
|
180
|
+
)}
|
|
181
|
+
<span className="status-bracket">]</span>
|
|
182
|
+
</span>
|
|
183
|
+
{elapsedLabel && (
|
|
184
|
+
<span className="status-timer" title="Execution time">{elapsedLabel}</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
<div className="add-cell-line add-line-above">
|
|
189
|
+
<AddCellButton onAddCell={(type) => onAddCell(type, indexRef.current)} />
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div
|
|
193
|
+
className={`cell ${isActive ? 'active' : ''} ${isExecuting ? 'executing' : ''} ${isMarkdownWithContent ? 'markdown-display-mode' : ''}`}
|
|
194
|
+
data-cell-index={index}
|
|
195
|
+
>
|
|
196
|
+
{/* Only show hover controls for non-markdown-display cells */}
|
|
197
|
+
{!isMarkdownWithContent && (
|
|
198
|
+
<div className="cell-hover-controls">
|
|
199
|
+
<div className="cell-actions-right">
|
|
200
|
+
<CellButton
|
|
201
|
+
icon={
|
|
202
|
+
<PlayIcon className="w-6 h-6" />
|
|
203
|
+
}
|
|
204
|
+
onClick={(e) => { e.stopPropagation(); handleExecute(); }}
|
|
205
|
+
title={isExecuting ? "Stop execution" : "Run cell"}
|
|
206
|
+
isLoading={isExecuting}
|
|
207
|
+
/>
|
|
208
|
+
<CellButton
|
|
209
|
+
icon={
|
|
210
|
+
<RowSpacingIcon className="w-6 h-6" />
|
|
211
|
+
}
|
|
212
|
+
title="Drag to reorder"
|
|
213
|
+
/>
|
|
214
|
+
<CellButton
|
|
215
|
+
icon={
|
|
216
|
+
<LinkBreak2Icon className="w-5 h-5" />
|
|
217
|
+
}
|
|
218
|
+
onClick={(e) => { e.stopPropagation(); onDelete(indexRef.current); }}
|
|
219
|
+
title="Delete cell"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
<div className={`cell-content ${isMarkdownWithContent ? 'cursor-pointer' : ''}`} onClick={handleCellClick}>
|
|
226
|
+
<div className="cell-input">
|
|
227
|
+
{isEditing || cell.cell_type === 'code' ? (
|
|
228
|
+
<div className={`cell-editor-container ${cell.cell_type === 'markdown' ? 'markdown-editor-container' : 'code-editor-container'}`}>
|
|
229
|
+
<textarea ref={editorRef} defaultValue={cell.source} className={`cell-editor ${cell.cell_type === 'markdown' ? 'markdown-editor' : 'code-editor'}`} />
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<MarkdownRenderer source={cell.source} onClick={() => setIsEditing(true)} />
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
<CellOutput outputs={cell.outputs} error={cell.error} onFixIndentation={handleFixIndentation} />
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="add-cell-line add-line-below">
|
|
240
|
+
<AddCellButton onAddCell={(type) => onAddCell(type, indexRef.current + 1)} />
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface CellButtonProps {
|
|
6
|
+
icon: React.ReactNode;
|
|
7
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
8
|
+
title?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CellButton: React.FC<CellButtonProps> = ({
|
|
15
|
+
icon,
|
|
16
|
+
onClick,
|
|
17
|
+
title,
|
|
18
|
+
disabled = false,
|
|
19
|
+
isLoading = false,
|
|
20
|
+
className = ''
|
|
21
|
+
}) => {
|
|
22
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
23
|
+
|
|
24
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
25
|
+
if (onClick && !disabled && !isLoading) {
|
|
26
|
+
onClick(e);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className={`cell-button ${className}`}
|
|
34
|
+
onClick={handleClick}
|
|
35
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
36
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
37
|
+
disabled={disabled || isLoading}
|
|
38
|
+
title={title}
|
|
39
|
+
style={{
|
|
40
|
+
width: '28px',
|
|
41
|
+
height: '28px',
|
|
42
|
+
border: '1px solid #D9DEE5',
|
|
43
|
+
borderRadius: '4px',
|
|
44
|
+
backgroundColor: isHovered ? '#E6E8EC' : 'rgba(243, 244, 246, 0.5)',
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
cursor: disabled || isLoading ? 'not-allowed' : 'pointer',
|
|
49
|
+
transition: 'background-color 0.15s ease',
|
|
50
|
+
opacity: disabled ? 0.5 : 1
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{icon}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default CellButton;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { FC } from 'react';
|
|
4
|
+
import { Output } from '@/types/notebook';
|
|
5
|
+
import ErrorDisplay from './ErrorDisplay';
|
|
6
|
+
|
|
7
|
+
interface CellOutputProps {
|
|
8
|
+
outputs: Output[];
|
|
9
|
+
error: any;
|
|
10
|
+
onFixIndentation?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CellOutput: FC<CellOutputProps> = ({ outputs, error, onFixIndentation }) => {
|
|
14
|
+
if (error) {
|
|
15
|
+
return <ErrorDisplay error={error} onFixIndentation={onFixIndentation} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!outputs || outputs.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="cell-output">
|
|
24
|
+
<div className="output-content">
|
|
25
|
+
{outputs.map((output, index) => {
|
|
26
|
+
switch (output.output_type) {
|
|
27
|
+
case 'stream':
|
|
28
|
+
return (
|
|
29
|
+
<pre key={index} className={`output-stream ${output.name}`}>
|
|
30
|
+
{output.text}
|
|
31
|
+
</pre>
|
|
32
|
+
);
|
|
33
|
+
case 'execute_result':
|
|
34
|
+
return (
|
|
35
|
+
<pre key={index} className="output-result">
|
|
36
|
+
{output.data?.['text/plain']}
|
|
37
|
+
</pre>
|
|
38
|
+
);
|
|
39
|
+
case 'display_data': {
|
|
40
|
+
const img = (output as any).data?.['image/png'];
|
|
41
|
+
const alt = (output as any).data?.['text/plain'] || 'image/png';
|
|
42
|
+
if (img) {
|
|
43
|
+
return (
|
|
44
|
+
<div key={index} className="output-result">
|
|
45
|
+
<img src={`data:image/png;base64,${img}`} alt={alt} />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<pre key={index} className="output-result">
|
|
51
|
+
{(output as any).data?.['text/plain']}
|
|
52
|
+
</pre>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
case 'error':
|
|
56
|
+
return <ErrorDisplay key={index} error={output} onFixIndentation={onFixIndentation} />;
|
|
57
|
+
default:
|
|
58
|
+
return (
|
|
59
|
+
<pre key={index} className="output-unknown">
|
|
60
|
+
{JSON.stringify(output, null, 2)}
|
|
61
|
+
</pre>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default CellOutput;
|