more-compute 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frontend/.DS_Store +0 -0
- frontend/.gitignore +41 -0
- frontend/README.md +36 -0
- frontend/__init__.py +1 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +1537 -0
- frontend/app/layout.tsx +173 -0
- frontend/app/page.tsx +11 -0
- frontend/components/AddCellButton.tsx +42 -0
- frontend/components/Cell.tsx +244 -0
- frontend/components/CellButton.tsx +58 -0
- frontend/components/CellOutput.tsx +70 -0
- frontend/components/ErrorDisplay.tsx +208 -0
- frontend/components/ErrorModal.tsx +154 -0
- frontend/components/MarkdownRenderer.tsx +84 -0
- frontend/components/Notebook.tsx +520 -0
- frontend/components/Sidebar.tsx +46 -0
- frontend/components/popups/ComputePopup.tsx +879 -0
- frontend/components/popups/FilterPopup.tsx +427 -0
- frontend/components/popups/FolderPopup.tsx +171 -0
- frontend/components/popups/MetricsPopup.tsx +168 -0
- frontend/components/popups/PackagesPopup.tsx +112 -0
- frontend/components/popups/PythonPopup.tsx +292 -0
- frontend/components/popups/SettingsPopup.tsx +68 -0
- frontend/eslint.config.mjs +25 -0
- frontend/lib/api.ts +469 -0
- frontend/lib/settings.ts +87 -0
- frontend/lib/websocket-native.ts +202 -0
- frontend/lib/websocket.ts +134 -0
- frontend/next-env.d.ts +6 -0
- frontend/next.config.mjs +17 -0
- frontend/next.config.ts +7 -0
- frontend/package-lock.json +5676 -0
- frontend/package.json +41 -0
- frontend/postcss.config.mjs +5 -0
- frontend/public/assets/icons/add.svg +1 -0
- frontend/public/assets/icons/check.svg +1 -0
- frontend/public/assets/icons/copy.svg +1 -0
- frontend/public/assets/icons/folder.svg +1 -0
- frontend/public/assets/icons/metric.svg +1 -0
- frontend/public/assets/icons/packages.svg +1 -0
- frontend/public/assets/icons/play.svg +1 -0
- frontend/public/assets/icons/python.svg +265 -0
- frontend/public/assets/icons/setting.svg +1 -0
- frontend/public/assets/icons/stop.svg +1 -0
- frontend/public/assets/icons/trash.svg +1 -0
- frontend/public/assets/icons/up-down.svg +1 -0
- frontend/public/assets/icons/x.svg +1 -0
- frontend/public/file.svg +1 -0
- frontend/public/fonts/Fira.ttf +0 -0
- frontend/public/fonts/Tiempos.woff2 +0 -0
- frontend/public/fonts/VeraMono.ttf +0 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/tailwind.config.ts +29 -0
- frontend/tsconfig.json +27 -0
- frontend/types/notebook.ts +58 -0
- kernel_run.py +7 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/METADATA +1 -1
- more_compute-0.1.4.dist-info/RECORD +86 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/top_level.txt +1 -0
- morecompute/__version__.py +1 -0
- more_compute-0.1.2.dist-info/RECORD +0 -26
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/WHEEL +0 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/entry_points.txt +0 -0
- {more_compute-0.1.2.dist-info → more_compute-0.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,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;
|