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.
Files changed (67) hide show
  1. frontend/.DS_Store +0 -0
  2. frontend/.gitignore +41 -0
  3. frontend/README.md +36 -0
  4. frontend/__init__.py +1 -0
  5. frontend/app/favicon.ico +0 -0
  6. frontend/app/globals.css +1537 -0
  7. frontend/app/layout.tsx +173 -0
  8. frontend/app/page.tsx +11 -0
  9. frontend/components/AddCellButton.tsx +42 -0
  10. frontend/components/Cell.tsx +244 -0
  11. frontend/components/CellButton.tsx +58 -0
  12. frontend/components/CellOutput.tsx +70 -0
  13. frontend/components/ErrorDisplay.tsx +208 -0
  14. frontend/components/ErrorModal.tsx +154 -0
  15. frontend/components/MarkdownRenderer.tsx +84 -0
  16. frontend/components/Notebook.tsx +520 -0
  17. frontend/components/Sidebar.tsx +46 -0
  18. frontend/components/popups/ComputePopup.tsx +879 -0
  19. frontend/components/popups/FilterPopup.tsx +427 -0
  20. frontend/components/popups/FolderPopup.tsx +171 -0
  21. frontend/components/popups/MetricsPopup.tsx +168 -0
  22. frontend/components/popups/PackagesPopup.tsx +112 -0
  23. frontend/components/popups/PythonPopup.tsx +292 -0
  24. frontend/components/popups/SettingsPopup.tsx +68 -0
  25. frontend/eslint.config.mjs +25 -0
  26. frontend/lib/api.ts +469 -0
  27. frontend/lib/settings.ts +87 -0
  28. frontend/lib/websocket-native.ts +202 -0
  29. frontend/lib/websocket.ts +134 -0
  30. frontend/next-env.d.ts +6 -0
  31. frontend/next.config.mjs +17 -0
  32. frontend/next.config.ts +7 -0
  33. frontend/package-lock.json +5676 -0
  34. frontend/package.json +41 -0
  35. frontend/postcss.config.mjs +5 -0
  36. frontend/public/assets/icons/add.svg +1 -0
  37. frontend/public/assets/icons/check.svg +1 -0
  38. frontend/public/assets/icons/copy.svg +1 -0
  39. frontend/public/assets/icons/folder.svg +1 -0
  40. frontend/public/assets/icons/metric.svg +1 -0
  41. frontend/public/assets/icons/packages.svg +1 -0
  42. frontend/public/assets/icons/play.svg +1 -0
  43. frontend/public/assets/icons/python.svg +265 -0
  44. frontend/public/assets/icons/setting.svg +1 -0
  45. frontend/public/assets/icons/stop.svg +1 -0
  46. frontend/public/assets/icons/trash.svg +1 -0
  47. frontend/public/assets/icons/up-down.svg +1 -0
  48. frontend/public/assets/icons/x.svg +1 -0
  49. frontend/public/file.svg +1 -0
  50. frontend/public/fonts/Fira.ttf +0 -0
  51. frontend/public/fonts/Tiempos.woff2 +0 -0
  52. frontend/public/fonts/VeraMono.ttf +0 -0
  53. frontend/public/globe.svg +1 -0
  54. frontend/public/next.svg +1 -0
  55. frontend/public/vercel.svg +1 -0
  56. frontend/public/window.svg +1 -0
  57. frontend/tailwind.config.ts +29 -0
  58. frontend/tsconfig.json +27 -0
  59. frontend/types/notebook.ts +58 -0
  60. kernel_run.py +6 -0
  61. {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/METADATA +1 -1
  62. more_compute-0.1.3.dist-info/RECORD +85 -0
  63. {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/top_level.txt +1 -0
  64. more_compute-0.1.2.dist-info/RECORD +0 -26
  65. {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/WHEEL +0 -0
  66. {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/entry_points.txt +0 -0
  67. {more_compute-0.1.2.dist-info → more_compute-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,168 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { fetchMetrics, type MetricsSnapshot } from "@/lib/api";
3
+ import {
4
+ Activity,
5
+ Cpu,
6
+ HardDrive,
7
+ MemoryStick,
8
+ Gauge,
9
+ ServerCrash,
10
+ } from "lucide-react";
11
+
12
+ const POLL_MS = 3000;
13
+
14
+ const MetricsPopup: React.FC<{ onClose?: () => void }> = ({ onClose }) => {
15
+ const [metrics, setMetrics] = useState<MetricsSnapshot | null>(null);
16
+ const [history, setHistory] = useState<MetricsSnapshot[]>([]);
17
+ const intervalRef = useRef<number | null>(null);
18
+
19
+ useEffect(() => {
20
+ const load = async () => {
21
+ try {
22
+ const snap = await fetchMetrics();
23
+ setMetrics(snap);
24
+ setHistory((prev) => {
25
+ const arr = [...prev, snap];
26
+ return arr.slice(-100);
27
+ });
28
+ } catch {}
29
+ };
30
+ load();
31
+ intervalRef.current = window.setInterval(load, POLL_MS);
32
+ return () => {
33
+ if (intervalRef.current) window.clearInterval(intervalRef.current);
34
+ };
35
+ }, []);
36
+
37
+ const hasGPU = metrics?.gpu && metrics.gpu.length > 0;
38
+
39
+ return (
40
+ <div className="metrics-container">
41
+ <div className="metrics-grid">
42
+ {metrics?.cpu && (
43
+ <Panel title="CPU Utilization" icon={<Cpu size={14} />}>
44
+ <BigValue
45
+ value={fmtPct(metrics.cpu.percent)}
46
+ subtitle={`${metrics.cpu.cores ?? "-"} cores`}
47
+ />
48
+ <MiniChart data={history.map((h) => h.cpu?.percent ?? 0)} />
49
+ </Panel>
50
+ )}
51
+
52
+ {metrics?.memory && (
53
+ <Panel title="Memory In Use" icon={<MemoryStick size={14} />}>
54
+ <BigValue
55
+ value={fmtPct(metrics.memory.percent)}
56
+ subtitle={`${fmtBytes(metrics.memory.used)} / ${fmtBytes(metrics.memory.total)}`}
57
+ />
58
+ <MiniChart data={history.map((h) => h.memory?.percent ?? 0)} />
59
+ </Panel>
60
+ )}
61
+
62
+ {metrics?.storage && (
63
+ <Panel title="Disk Utilization" icon={<HardDrive size={14} />}>
64
+ <BigValue
65
+ value={fmtPct(metrics.storage.percent)}
66
+ subtitle={`${fmtBytes(metrics.storage.used)} / ${fmtBytes(metrics.storage.total)}`}
67
+ />
68
+ <MiniChart data={history.map((h) => h.storage?.percent ?? 0)} />
69
+ </Panel>
70
+ )}
71
+
72
+ {hasGPU && (
73
+ <Panel title="GPU Utilization" icon={<Gauge size={14} />}>
74
+ <BigValue
75
+ value={fmtPct(metrics!.gpu![0].util_percent)}
76
+ subtitle={`Temp ${metrics!.gpu![0].temperature_c ?? "-"}°C`}
77
+ />
78
+ <MiniChart
79
+ data={history.map((h) => (h.gpu && h.gpu[0]?.util_percent) || 0)}
80
+ />
81
+ </Panel>
82
+ )}
83
+
84
+ {metrics?.network && (
85
+ <Panel title="Network" icon={<Activity size={14} />}>
86
+ <BigValue
87
+ value={`${fmtBytes(metrics.network.bytes_recv)} ↓ / ${fmtBytes(metrics.network.bytes_sent)} ↑`}
88
+ subtitle="total"
89
+ />
90
+ </Panel>
91
+ )}
92
+
93
+ {metrics?.process && (
94
+ <Panel title="Process" icon={<ServerCrash size={14} />}>
95
+ <BigValue
96
+ value={`${fmtBytes(metrics.process.rss)} RSS`}
97
+ subtitle={`${metrics.process.threads ?? "-"} threads`}
98
+ />
99
+ </Panel>
100
+ )}
101
+ </div>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ const Panel: React.FC<{
107
+ title: string;
108
+ icon?: React.ReactNode;
109
+ children: React.ReactNode;
110
+ }> = ({ title, icon, children }) => {
111
+ return (
112
+ <div className="metric-panel">
113
+ <div className="metric-panel-header">
114
+ <span className="metric-panel-title">
115
+ {icon}
116
+ {icon && " "}
117
+ {title}
118
+ </span>
119
+ </div>
120
+ <div className="metric-panel-body">{children}</div>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ const BigValue: React.FC<{ value: string; subtitle?: string }> = ({
126
+ value,
127
+ subtitle,
128
+ }) => (
129
+ <div className="metric-big-value">
130
+ <div className="value">{value}</div>
131
+ {subtitle && <div className="subtitle">{subtitle}</div>}
132
+ </div>
133
+ );
134
+
135
+ const MiniChart: React.FC<{ data: number[] }> = ({ data }) => {
136
+ const width = 220;
137
+ const height = 48;
138
+ const max = Math.max(100, ...data);
139
+ const points = data
140
+ .map((v, i) => {
141
+ const x = (i / Math.max(1, data.length - 1)) * width;
142
+ const y = height - (Math.min(100, Math.max(0, v)) / max) * height;
143
+ return `${x},${y}`;
144
+ })
145
+ .join(" ");
146
+ return (
147
+ <svg width={width} height={height} className="mini-chart">
148
+ <polyline points={points} fill="none" stroke="#3b82f6" strokeWidth="2" />
149
+ </svg>
150
+ );
151
+ };
152
+
153
+ function fmtPct(v?: number | null): string {
154
+ return v == null ? "-" : `${v.toFixed(0)}%`;
155
+ }
156
+ function fmtBytes(v?: number | null): string {
157
+ if (v == null) return "-";
158
+ const units = ["B", "KB", "MB", "GB", "TB"];
159
+ let val = v;
160
+ let u = 0;
161
+ while (val >= 1024 && u < units.length - 1) {
162
+ val /= 1024;
163
+ u++;
164
+ }
165
+ return `${val.toFixed(1)} ${units[u]}`;
166
+ }
167
+
168
+ export default MetricsPopup;
@@ -0,0 +1,112 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Search, CircleHelp } from 'lucide-react';
3
+ import { fetchInstalledPackages } from '@/lib/api';
4
+
5
+ interface Package {
6
+ name: string;
7
+ version: string;
8
+ description: string;
9
+ }
10
+
11
+ interface PackagesPopupProps {
12
+ onClose?: () => void;
13
+ }
14
+
15
+ const PackagesPopup: React.FC<PackagesPopupProps> = ({ onClose }) => {
16
+ const [packages, setPackages] = useState<Package[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState<string | null>(null);
19
+ const [query, setQuery] = useState('');
20
+
21
+ useEffect(() => {
22
+ loadPackages();
23
+ const handler = () => loadPackages(true); // Force refresh when packages updated
24
+ if (typeof window !== 'undefined') {
25
+ window.addEventListener('mc:packages-updated', handler as EventListener);
26
+ }
27
+ return () => {
28
+ if (typeof window !== 'undefined') {
29
+ window.removeEventListener('mc:packages-updated', handler as EventListener);
30
+ }
31
+ };
32
+ }, []);
33
+
34
+ const getPackages = async (forceRefresh: boolean = false): Promise<Package[]> => {
35
+ const pkgs = await fetchInstalledPackages(forceRefresh);
36
+ const seen = new Set<string>();
37
+ const out: Package[] = [];
38
+ for (const p of pkgs) {
39
+ const name = p.name || '';
40
+ const version = p.version || '';
41
+ // ignore base package
42
+ // we might need to remove this? idk double check later
43
+ if (name.toLowerCase() === 'morecompute') continue;
44
+ const key = `${name}@${version}`.toLowerCase();
45
+ if (seen.has(key)) continue; // dedupe exact duplicates
46
+ seen.add(key);
47
+ out.push({ name, version, description: '' });
48
+ }
49
+ out.sort((a, b) => a.name.localeCompare(b.name));
50
+ return out;
51
+ };
52
+
53
+ const loadPackages = async (forceRefresh: boolean = false) => {
54
+ setLoading(true);
55
+ setError(null);
56
+ try {
57
+ const data = await getPackages(forceRefresh);
58
+ setPackages(data);
59
+ } catch (err) {
60
+ setError('Failed to load packages');
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ };
65
+
66
+ const filtered = useMemo(() => {
67
+ if (!query.trim()) return packages;
68
+ const q = query.toLowerCase();
69
+ return packages.filter(p => p.name.toLowerCase().includes(q));
70
+ }, [packages, query]);
71
+
72
+ if (loading) return <div className="packages-list">Loading...</div>;
73
+ if (error) return <div className="packages-list">{error}</div>;
74
+
75
+ return (
76
+ <div className="packages-container">
77
+ <div className="packages-toolbar">
78
+ <div className="packages-search">
79
+ <Search className="packages-search-icon" size={16} />
80
+ <input
81
+ type="text"
82
+ value={query}
83
+ onChange={(e) => setQuery(e.target.value)}
84
+ placeholder="Search packages"
85
+ className="packages-search-input"
86
+ />
87
+ </div>
88
+ <div className="packages-subtext">
89
+ <CircleHelp size={14} />
90
+ <span>Install packages with !pip</span>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="packages-table">
95
+ <div className="packages-table-header">
96
+ <div className="col-name">Name</div>
97
+ <div className="col-version">Version</div>
98
+ </div>
99
+ <div className="packages-list">
100
+ {filtered.map((pkg) => (
101
+ <div key={`${pkg.name}@${pkg.version}`} className="package-row">
102
+ <div className="col-name package-name">{pkg.name}</div>
103
+ <div className="col-version package-version">{pkg.version}</div>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ );
110
+ };
111
+
112
+ export default PackagesPopup;
@@ -0,0 +1,292 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { RotateCw, Cpu } from "lucide-react";
3
+
4
+ interface PythonEnvironment {
5
+ name: string;
6
+ version: string;
7
+ path: string;
8
+ type: string;
9
+ active?: boolean;
10
+ }
11
+
12
+ interface PythonPopupProps {
13
+ onClose?: () => void;
14
+ onEnvironmentSwitch?: (env: PythonEnvironment) => void;
15
+ }
16
+
17
+ const PythonPopup: React.FC<PythonPopupProps> = ({
18
+ onClose,
19
+ onEnvironmentSwitch,
20
+ }) => {
21
+ const [environments, setEnvironments] = useState<PythonEnvironment[]>([]);
22
+ const [currentEnv, setCurrentEnv] = useState<PythonEnvironment | null>(null);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ loadEnvironments();
28
+ }, []);
29
+
30
+ const loadEnvironments = async (full: boolean = true, forceRefresh: boolean = false) => {
31
+ setLoading(true);
32
+ setError(null);
33
+ try {
34
+ const url = `/api/environments?full=${full}${forceRefresh ? '&force_refresh=true' : ''}`;
35
+ const response = await fetch(url);
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to fetch environments: ${response.statusText}`);
38
+ }
39
+
40
+ const data = await response.json();
41
+
42
+ if (data.status === "success") {
43
+ setEnvironments(
44
+ data.environments.map((env: any) => ({
45
+ ...env,
46
+ active: env.path === data.current.path,
47
+ })),
48
+ );
49
+ setCurrentEnv(data.current);
50
+ } else {
51
+ throw new Error(data.message || "Failed to load environments");
52
+ }
53
+ } catch (err: any) {
54
+ setError(err.message || "Failed to load environments");
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ };
59
+
60
+ if (loading) {
61
+ return (
62
+ <div className="runtime-popup-loading">
63
+ Loading runtime environments...
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (error) {
69
+ return <div className="runtime-popup-error">{error}</div>;
70
+ }
71
+
72
+ return (
73
+ <div className="runtime-popup">
74
+ {/* Python Environment Section */}
75
+ <section className="runtime-section">
76
+ <p className="runtime-subtitle">
77
+ Select the Python interpreter for local execution.
78
+ </p>
79
+
80
+ {/* Current Environment */}
81
+ {currentEnv && (
82
+ <div
83
+ style={{
84
+ padding: "12px",
85
+ borderRadius: "8px",
86
+ border: "2px solid var(--accent)",
87
+ backgroundColor: "var(--accent-bg)",
88
+ marginBottom: "16px",
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ display: "flex",
94
+ alignItems: "center",
95
+ marginBottom: "8px",
96
+ fontSize: "10px",
97
+ fontWeight: 600,
98
+ color: "var(--accent)",
99
+ textTransform: "uppercase",
100
+ letterSpacing: "0.5px",
101
+ }}
102
+ >
103
+ <Cpu size={14} style={{ marginRight: "6px" }} />
104
+ Current Environment
105
+ </div>
106
+ <div style={{ fontWeight: 500, fontSize: "12px", marginBottom: "4px" }}>
107
+ {currentEnv.name}
108
+ </div>
109
+ <div style={{ fontSize: "10px", color: "var(--text-secondary)" }}>
110
+ Python {currentEnv.version} • {currentEnv.type}
111
+ </div>
112
+ <div
113
+ style={{
114
+ fontSize: "9px",
115
+ color: "var(--text-tertiary)",
116
+ marginTop: "6px",
117
+ whiteSpace: "nowrap",
118
+ overflow: "hidden",
119
+ textOverflow: "ellipsis",
120
+ }}
121
+ title={currentEnv.path}
122
+ >
123
+ {currentEnv.path}
124
+ </div>
125
+ </div>
126
+ )}
127
+
128
+ {/* Available Environments */}
129
+ <div className="runtime-subsection">
130
+ <div
131
+ style={{
132
+ display: "flex",
133
+ justifyContent: "space-between",
134
+ alignItems: "center",
135
+ marginBottom: "12px",
136
+ }}
137
+ >
138
+ <h4
139
+ style={{
140
+ fontSize: "11px",
141
+ fontWeight: 600,
142
+ margin: 0,
143
+ color: "var(--text)",
144
+ }}
145
+ >
146
+ Available Environments
147
+ </h4>
148
+ <button
149
+ onClick={() => loadEnvironments(true, true)}
150
+ aria-label="Refresh environments"
151
+ style={{
152
+ display: "flex",
153
+ alignItems: "center",
154
+ justifyContent: "center",
155
+ padding: "6px",
156
+ borderRadius: "4px",
157
+ border: "1px solid var(--border-color)",
158
+ backgroundColor: "var(--background)",
159
+ cursor: "pointer",
160
+ transition: "all 0.15s ease",
161
+ }}
162
+ onMouseEnter={(e) => {
163
+ e.currentTarget.style.backgroundColor = "var(--hover-background)";
164
+ e.currentTarget.style.borderColor = "var(--accent)";
165
+ }}
166
+ onMouseLeave={(e) => {
167
+ e.currentTarget.style.backgroundColor = "var(--background)";
168
+ e.currentTarget.style.borderColor = "var(--border-color)";
169
+ }}
170
+ >
171
+ <RotateCw size={12} style={{ color: "var(--text-secondary)" }} />
172
+ </button>
173
+ </div>
174
+
175
+ <div
176
+ style={{
177
+ maxHeight: "320px",
178
+ overflowY: "auto",
179
+ overflowX: "hidden",
180
+ }}
181
+ >
182
+ {environments.map((env, index) => (
183
+ <div
184
+ key={index}
185
+ onClick={() => {
186
+ if (!env.active && onEnvironmentSwitch) {
187
+ onEnvironmentSwitch(env);
188
+ }
189
+ }}
190
+ style={{
191
+ padding: "12px",
192
+ borderRadius: "6px",
193
+ border: env.active
194
+ ? "2px solid var(--accent)"
195
+ : "1.5px solid var(--border-color)",
196
+ marginBottom: "8px",
197
+ cursor: env.active ? "default" : "pointer",
198
+ backgroundColor: env.active
199
+ ? "var(--accent-bg)"
200
+ : "var(--background)",
201
+ transition: "all 0.15s ease",
202
+ position: "relative",
203
+ boxShadow: "0 1px 3px rgba(0, 0, 0, 0.05)",
204
+ }}
205
+ onMouseEnter={(e) => {
206
+ if (!env.active) {
207
+ e.currentTarget.style.backgroundColor =
208
+ "var(--hover-background)";
209
+ e.currentTarget.style.borderColor = "var(--accent)";
210
+ e.currentTarget.style.boxShadow =
211
+ "0 2px 8px rgba(0, 0, 0, 0.1)";
212
+ e.currentTarget.style.transform = "translateY(-1px)";
213
+ }
214
+ }}
215
+ onMouseLeave={(e) => {
216
+ if (!env.active) {
217
+ e.currentTarget.style.backgroundColor =
218
+ "var(--background)";
219
+ e.currentTarget.style.borderColor = "var(--border-color)";
220
+ e.currentTarget.style.boxShadow =
221
+ "0 1px 3px rgba(0, 0, 0, 0.05)";
222
+ e.currentTarget.style.transform = "translateY(0)";
223
+ }
224
+ }}
225
+ >
226
+ <div
227
+ style={{
228
+ display: "flex",
229
+ justifyContent: "space-between",
230
+ alignItems: "flex-start",
231
+ }}
232
+ >
233
+ <div style={{ flex: 1, minWidth: 0 }}>
234
+ <div
235
+ style={{
236
+ fontWeight: 500,
237
+ fontSize: "11px",
238
+ marginBottom: "3px",
239
+ color: env.active ? "var(--accent)" : "var(--text)",
240
+ }}
241
+ >
242
+ {env.name}
243
+ </div>
244
+ <div
245
+ style={{
246
+ fontSize: "10px",
247
+ color: "var(--text-secondary)",
248
+ marginBottom: "4px",
249
+ }}
250
+ >
251
+ Python {env.version} • {env.type}
252
+ </div>
253
+ <div
254
+ style={{
255
+ fontSize: "9px",
256
+ color: "var(--text-tertiary)",
257
+ whiteSpace: "nowrap",
258
+ overflow: "hidden",
259
+ textOverflow: "ellipsis",
260
+ }}
261
+ title={env.path}
262
+ >
263
+ {env.path}
264
+ </div>
265
+ </div>
266
+ {env.active && (
267
+ <div
268
+ style={{
269
+ fontSize: "9px",
270
+ fontWeight: 600,
271
+ color: "var(--accent)",
272
+ backgroundColor: "var(--accent-bg)",
273
+ padding: "2px 6px",
274
+ borderRadius: "4px",
275
+ marginLeft: "8px",
276
+ flexShrink: 0,
277
+ }}
278
+ >
279
+ ACTIVE
280
+ </div>
281
+ )}
282
+ </div>
283
+ </div>
284
+ ))}
285
+ </div>
286
+ </div>
287
+ </section>
288
+ </div>
289
+ );
290
+ };
291
+
292
+ export default PythonPopup;
@@ -0,0 +1,68 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { THEMES, DEFAULT_SETTINGS, loadSettings, saveSettings, applyTheme, type NotebookSettings } from '@/lib/settings';
3
+
4
+ const SettingsPopup: React.FC<{ onClose?: () => void; onSettingsChange?: (settings: NotebookSettings) => void }> = ({ onClose, onSettingsChange }) => {
5
+ const [settings, setSettings] = useState<NotebookSettings>(() => loadSettings());
6
+ const [settingsJson, setSettingsJson] = useState(() => JSON.stringify(loadSettings(), null, 2));
7
+ const [error, setError] = useState<string | null>(null);
8
+
9
+ useEffect(() => {
10
+ setSettingsJson(JSON.stringify(settings, null, 2));
11
+ applyTheme(settings.theme);
12
+ }, []);
13
+
14
+ const persistSettings = useCallback((updated: NotebookSettings) => {
15
+ setSettings(updated);
16
+ setSettingsJson(JSON.stringify(updated, null, 2));
17
+ saveSettings(updated);
18
+ applyTheme(updated.theme);
19
+ onSettingsChange?.(updated);
20
+ }, [onSettingsChange]);
21
+
22
+ const handleSave = () => {
23
+ try {
24
+ const parsed = JSON.parse(settingsJson);
25
+ const merged: NotebookSettings = {
26
+ ...DEFAULT_SETTINGS,
27
+ ...parsed,
28
+ theme: parsed.theme && THEMES[parsed.theme] ? parsed.theme : DEFAULT_SETTINGS.theme,
29
+ };
30
+ setError(null);
31
+ persistSettings(merged);
32
+ } catch (err: any) {
33
+ console.error('Failed to parse settings JSON', err);
34
+ setError('Invalid JSON. Please fix the syntax and try again.');
35
+ }
36
+ };
37
+
38
+ const handleReset = () => {
39
+ setError(null);
40
+ persistSettings(DEFAULT_SETTINGS);
41
+ };
42
+
43
+ return (
44
+ <div className="settings-container">
45
+ <div style={{ marginBottom: '16px', padding: '12px', background: '#f8f9fa', borderRadius: '6px', color: '#6b7280', fontSize: '13px', lineHeight: 1.5 }}>
46
+ <strong>MoreCompute Settings</strong><br />
47
+ Configure your notebook environment. Changes are saved to local storage.
48
+ <br /><br />
49
+ <em>Experiment with themes (e.g. "catppuccin") or customise fonts and autosave.</em>
50
+ </div>
51
+ <textarea
52
+ className="settings-editor"
53
+ value={settingsJson}
54
+ onChange={(e) => setSettingsJson(e.target.value)}
55
+ spellCheck={false}
56
+ />
57
+ {error && (
58
+ <div style={{ color: '#dc2626', fontSize: '12px', marginTop: '8px' }}>{error}</div>
59
+ )}
60
+ <div className="settings-actions">
61
+ <button className="btn btn-secondary" type="button" onClick={handleReset}>Reset to Defaults</button>
62
+ <button className="btn btn-primary" type="button" onClick={handleSave}>Save Settings</button>
63
+ </div>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default SettingsPopup;
@@ -0,0 +1,25 @@
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
14
+ {
15
+ ignores: [
16
+ "node_modules/**",
17
+ ".next/**",
18
+ "out/**",
19
+ "build/**",
20
+ "next-env.d.ts",
21
+ ],
22
+ },
23
+ ];
24
+
25
+ export default eslintConfig;