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
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Zap,
|
|
4
|
+
ExternalLink,
|
|
5
|
+
Plus,
|
|
6
|
+
Activity,
|
|
7
|
+
Search,
|
|
8
|
+
Filter,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import {
|
|
11
|
+
fetchGpuPods,
|
|
12
|
+
fetchGpuConfig,
|
|
13
|
+
setGpuApiKey,
|
|
14
|
+
fetchGpuAvailability,
|
|
15
|
+
createGpuPod,
|
|
16
|
+
deleteGpuPod,
|
|
17
|
+
connectToPod,
|
|
18
|
+
disconnectFromPod,
|
|
19
|
+
getPodConnectionStatus,
|
|
20
|
+
PodResponse,
|
|
21
|
+
PodsListParams,
|
|
22
|
+
GpuAvailability,
|
|
23
|
+
GpuAvailabilityParams,
|
|
24
|
+
CreatePodRequest,
|
|
25
|
+
PodConnectionStatus,
|
|
26
|
+
} from "@/lib/api";
|
|
27
|
+
import ErrorModal from "@/components/ErrorModal";
|
|
28
|
+
import FilterPopup from "./FilterPopup";
|
|
29
|
+
|
|
30
|
+
interface GPUPod {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
status: "running" | "stopped" | "starting";
|
|
34
|
+
gpuType: string;
|
|
35
|
+
region: string;
|
|
36
|
+
costPerHour: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ComputePopupProps {
|
|
40
|
+
onClose?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
44
|
+
const [gpuPods, setGpuPods] = useState<GPUPod[]>([]);
|
|
45
|
+
const [loading, setLoading] = useState(false);
|
|
46
|
+
const [kernelStatus, setKernelStatus] = useState(false);
|
|
47
|
+
const [apiConfigured, setApiConfigured] = useState<boolean | null>(null);
|
|
48
|
+
const [apiKey, setApiKey] = useState("");
|
|
49
|
+
const [saving, setSaving] = useState(false);
|
|
50
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
51
|
+
|
|
52
|
+
// GPU Availability state
|
|
53
|
+
const [showBrowseGPUs, setShowBrowseGPUs] = useState(false);
|
|
54
|
+
const [availableGPUs, setAvailableGPUs] = useState<GpuAvailability[]>([]);
|
|
55
|
+
const [loadingAvailability, setLoadingAvailability] = useState(false);
|
|
56
|
+
const [filters, setFilters] = useState<GpuAvailabilityParams>({});
|
|
57
|
+
const [creatingPodId, setCreatingPodId] = useState<string | null>(null);
|
|
58
|
+
const [podCreationError, setPodCreationError] = useState<string | null>(null);
|
|
59
|
+
const [connectingPodId, setConnectingPodId] = useState<string | null>(null);
|
|
60
|
+
const [connectedPodId, setConnectedPodId] = useState<string | null>(null);
|
|
61
|
+
const [deletingPodId, setDeletingPodId] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
// Filter popup state
|
|
64
|
+
const [showFilterPopup, setShowFilterPopup] = useState(false);
|
|
65
|
+
|
|
66
|
+
// Error modal state
|
|
67
|
+
const [errorModal, setErrorModal] = useState<{
|
|
68
|
+
isOpen: boolean;
|
|
69
|
+
title: string;
|
|
70
|
+
message: string;
|
|
71
|
+
actionLabel?: string;
|
|
72
|
+
actionUrl?: string;
|
|
73
|
+
}>({
|
|
74
|
+
isOpen: false,
|
|
75
|
+
title: "",
|
|
76
|
+
message: "",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const checkApiConfig = async () => {
|
|
81
|
+
try {
|
|
82
|
+
const config = await fetchGpuConfig();
|
|
83
|
+
setApiConfigured(config.configured);
|
|
84
|
+
if (config.configured) {
|
|
85
|
+
await loadGPUPods();
|
|
86
|
+
// Check if already connected to a pod
|
|
87
|
+
const status: PodConnectionStatus = await getPodConnectionStatus();
|
|
88
|
+
if (status.connected && status.pod) {
|
|
89
|
+
setConnectedPodId(status.pod.id);
|
|
90
|
+
setKernelStatus(true); // Kernel is running when connected to pod
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error("Failed to check GPU config:", err);
|
|
95
|
+
setApiConfigured(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
checkApiConfig();
|
|
99
|
+
|
|
100
|
+
// Poll pod list every 10 seconds if configured
|
|
101
|
+
const pollInterval = setInterval(async () => {
|
|
102
|
+
if (apiConfigured) {
|
|
103
|
+
await loadGPUPods();
|
|
104
|
+
}
|
|
105
|
+
}, 10000);
|
|
106
|
+
|
|
107
|
+
return () => clearInterval(pollInterval);
|
|
108
|
+
}, [apiConfigured]);
|
|
109
|
+
|
|
110
|
+
const loadGPUPods = async (params?: PodsListParams) => {
|
|
111
|
+
setLoading(true);
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetchGpuPods(params || { limit: 100 });
|
|
114
|
+
const pods = (response.data || []).map((pod: PodResponse) => {
|
|
115
|
+
// Map API status to UI status
|
|
116
|
+
let uiStatus: "running" | "stopped" | "starting" = "stopped";
|
|
117
|
+
if (pod.status === "ACTIVE") {
|
|
118
|
+
uiStatus = "running";
|
|
119
|
+
} else if (pod.status === "PROVISIONING" || pod.status === "PENDING") {
|
|
120
|
+
uiStatus = "starting";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
id: pod.id,
|
|
125
|
+
name: pod.name,
|
|
126
|
+
status: uiStatus,
|
|
127
|
+
gpuType: pod.gpuName,
|
|
128
|
+
region: "Unknown", //look at later
|
|
129
|
+
costPerHour: pod.priceHr,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
setGpuPods(pods);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error("Failed to load GPU pods:", err);
|
|
135
|
+
} finally {
|
|
136
|
+
setLoading(false);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const loadAvailableGPUs = async () => {
|
|
141
|
+
setLoadingAvailability(true);
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetchGpuAvailability(filters);
|
|
144
|
+
const gpuList: GpuAvailability[] = [];
|
|
145
|
+
Object.values(response).forEach((gpus) => {
|
|
146
|
+
gpuList.push(...gpus);
|
|
147
|
+
});
|
|
148
|
+
setAvailableGPUs(gpuList);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("Failed to load GPU availability:", err);
|
|
151
|
+
} finally {
|
|
152
|
+
setLoadingAvailability(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const createPodFromGpu = async (gpu: GpuAvailability) => {
|
|
157
|
+
setCreatingPodId(gpu.cloudId);
|
|
158
|
+
setPodCreationError(null);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Generate a pod name based on GPU type and timestamp
|
|
162
|
+
const timestamp = new Date()
|
|
163
|
+
.toISOString()
|
|
164
|
+
.slice(0, 19)
|
|
165
|
+
.replace(/[:-]/g, "");
|
|
166
|
+
const podName = `${gpu.gpuType.toLowerCase()}-${timestamp}`;
|
|
167
|
+
|
|
168
|
+
const podRequest: CreatePodRequest = {
|
|
169
|
+
pod: {
|
|
170
|
+
name: podName,
|
|
171
|
+
cloudId: gpu.cloudId,
|
|
172
|
+
gpuType: gpu.gpuType,
|
|
173
|
+
socket: gpu.socket,
|
|
174
|
+
gpuCount: gpu.gpuCount,
|
|
175
|
+
diskSize: gpu.disk?.defaultCount || 100,
|
|
176
|
+
vcpus: gpu.vcpu?.defaultCount || 16,
|
|
177
|
+
memory: gpu.memory?.defaultCount || 128,
|
|
178
|
+
image: gpu.images?.[0] || "ubuntu_22_cuda_12",
|
|
179
|
+
security: gpu.security,
|
|
180
|
+
dataCenterId: gpu.dataCenter || undefined,
|
|
181
|
+
country: gpu.country || undefined,
|
|
182
|
+
},
|
|
183
|
+
provider: {
|
|
184
|
+
type: gpu.provider.toLowerCase(),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const newPod = await createGpuPod(podRequest);
|
|
189
|
+
|
|
190
|
+
// Refresh the pods list
|
|
191
|
+
await loadGPUPods();
|
|
192
|
+
|
|
193
|
+
// Close browse section and show success
|
|
194
|
+
setShowBrowseGPUs(false);
|
|
195
|
+
alert(
|
|
196
|
+
`Pod "${newPod.name}" created successfully! Wait for provisioning (~2-5 min).`,
|
|
197
|
+
);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
let errorMsg = "Failed to create pod";
|
|
200
|
+
|
|
201
|
+
if (err instanceof Error) {
|
|
202
|
+
errorMsg = err.message;
|
|
203
|
+
|
|
204
|
+
// Parse specific error cases
|
|
205
|
+
if (
|
|
206
|
+
errorMsg.includes("402") ||
|
|
207
|
+
errorMsg.includes("Insufficient funds")
|
|
208
|
+
) {
|
|
209
|
+
errorMsg =
|
|
210
|
+
"Insufficient funds. Please add credits to your Prime Intellect wallet:\nhttps://app.primeintellect.ai/dashboard/billing";
|
|
211
|
+
} else if (errorMsg.includes("401") || errorMsg.includes("403")) {
|
|
212
|
+
errorMsg = "Authentication failed. Check your API key configuration.";
|
|
213
|
+
} else if (errorMsg.includes("data_center_id")) {
|
|
214
|
+
errorMsg =
|
|
215
|
+
"Pod configuration error: Missing data center ID. Try a different GPU or provider.";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setPodCreationError(errorMsg);
|
|
220
|
+
|
|
221
|
+
// Show error in modal with link to billing if insufficient funds
|
|
222
|
+
if (errorMsg.includes("Insufficient funds")) {
|
|
223
|
+
setErrorModal({
|
|
224
|
+
isOpen: true,
|
|
225
|
+
title: "Insufficient Funds",
|
|
226
|
+
message: errorMsg,
|
|
227
|
+
actionLabel: "Add Credits",
|
|
228
|
+
actionUrl: "https://app.primeintellect.ai/dashboard/billing",
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
setErrorModal({
|
|
232
|
+
isOpen: true,
|
|
233
|
+
title: "Failed to Create Pod",
|
|
234
|
+
message: errorMsg,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
setCreatingPodId(null);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handleConnectToPod = async (podId: string) => {
|
|
243
|
+
setConnectingPodId(podId);
|
|
244
|
+
try {
|
|
245
|
+
const result = await connectToPod(podId);
|
|
246
|
+
if (result.status === "ok") {
|
|
247
|
+
setConnectedPodId(podId);
|
|
248
|
+
setKernelStatus(true); // Mark kernel as running
|
|
249
|
+
setErrorModal({
|
|
250
|
+
isOpen: true,
|
|
251
|
+
title: "✓ Connected!",
|
|
252
|
+
message:
|
|
253
|
+
"Successfully connected to GPU pod. You can now run code on the remote GPU.",
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
// Show detailed error message from backend
|
|
257
|
+
let errorMsg = result.message || "Connection failed";
|
|
258
|
+
|
|
259
|
+
// Check for SSH key issues
|
|
260
|
+
if (
|
|
261
|
+
errorMsg.includes("SSH authentication") ||
|
|
262
|
+
errorMsg.includes("SSH public key")
|
|
263
|
+
) {
|
|
264
|
+
setErrorModal({
|
|
265
|
+
isOpen: true,
|
|
266
|
+
title: "SSH Key Required",
|
|
267
|
+
message: errorMsg,
|
|
268
|
+
actionLabel: "Add SSH Key",
|
|
269
|
+
actionUrl: "https://app.primeintellect.ai/dashboard/tokens",
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
setErrorModal({
|
|
273
|
+
isOpen: true,
|
|
274
|
+
title: "Connection Failed",
|
|
275
|
+
message: errorMsg,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const errorMsg =
|
|
281
|
+
err instanceof Error ? err.message : "Failed to connect to pod";
|
|
282
|
+
setErrorModal({
|
|
283
|
+
isOpen: true,
|
|
284
|
+
title: "Connection Error",
|
|
285
|
+
message: errorMsg,
|
|
286
|
+
});
|
|
287
|
+
} finally {
|
|
288
|
+
setConnectingPodId(null);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const handleDisconnect = async () => {
|
|
293
|
+
try {
|
|
294
|
+
await disconnectFromPod();
|
|
295
|
+
setConnectedPodId(null);
|
|
296
|
+
setKernelStatus(false); // Mark kernel as not running
|
|
297
|
+
alert("Disconnected from pod");
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const errorMsg =
|
|
300
|
+
err instanceof Error ? err.message : "Failed to disconnect";
|
|
301
|
+
alert(`Disconnect error: ${errorMsg}`);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleDeletePod = async (podId: string, podName: string) => {
|
|
306
|
+
if (!confirm(`Are you sure you want to terminate pod "${podName}"?`)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setDeletingPodId(podId);
|
|
311
|
+
try {
|
|
312
|
+
// Disconnect if this is the connected pod
|
|
313
|
+
if (connectedPodId === podId) {
|
|
314
|
+
await disconnectFromPod();
|
|
315
|
+
setConnectedPodId(null);
|
|
316
|
+
setKernelStatus(false);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await deleteGpuPod(podId);
|
|
320
|
+
alert(`Pod "${podName}" terminated successfully`);
|
|
321
|
+
await loadGPUPods();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
const errorMsg =
|
|
324
|
+
err instanceof Error ? err.message : "Failed to terminate pod";
|
|
325
|
+
alert(`Terminate error: ${errorMsg}`);
|
|
326
|
+
} finally {
|
|
327
|
+
setDeletingPodId(null);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleConnectToPrimeIntellect = () => {
|
|
332
|
+
window.open("https://app.primeintellect.ai/dashboard/tokens", "_blank");
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const handleSaveApiKey = async () => {
|
|
336
|
+
if (!apiKey.trim()) {
|
|
337
|
+
setSaveError("API key cannot be empty");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setSaving(true);
|
|
342
|
+
setSaveError(null);
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await setGpuApiKey(apiKey);
|
|
346
|
+
setApiConfigured(true);
|
|
347
|
+
setApiKey("");
|
|
348
|
+
await loadGPUPods();
|
|
349
|
+
} catch (err) {
|
|
350
|
+
setSaveError(
|
|
351
|
+
err instanceof Error ? err.message : "Failed to save API key",
|
|
352
|
+
);
|
|
353
|
+
} finally {
|
|
354
|
+
setSaving(false);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<>
|
|
360
|
+
<ErrorModal
|
|
361
|
+
isOpen={errorModal.isOpen}
|
|
362
|
+
onClose={() => setErrorModal({ ...errorModal, isOpen: false })}
|
|
363
|
+
title={errorModal.title}
|
|
364
|
+
message={errorModal.message}
|
|
365
|
+
actionLabel={errorModal.actionLabel}
|
|
366
|
+
actionUrl={errorModal.actionUrl}
|
|
367
|
+
/>
|
|
368
|
+
<div className="runtime-popup">
|
|
369
|
+
{/* Kernel Status Section */}
|
|
370
|
+
<section
|
|
371
|
+
className="runtime-section"
|
|
372
|
+
style={{ padding: "6px 12px", marginBottom: "16px" }}
|
|
373
|
+
>
|
|
374
|
+
<div
|
|
375
|
+
style={{
|
|
376
|
+
display: "flex",
|
|
377
|
+
justifyContent: "space-between",
|
|
378
|
+
alignItems: "center",
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
<h3 className="runtime-section-title" style={{ fontSize: "12px" }}>
|
|
382
|
+
Kernel:{" "}
|
|
383
|
+
<span
|
|
384
|
+
className={
|
|
385
|
+
kernelStatus
|
|
386
|
+
? "kernel-status-active"
|
|
387
|
+
: "kernel-status-inactive"
|
|
388
|
+
}
|
|
389
|
+
>
|
|
390
|
+
{kernelStatus ? "running" : "not running"}
|
|
391
|
+
</span>
|
|
392
|
+
</h3>
|
|
393
|
+
<button
|
|
394
|
+
className="runtime-btn runtime-btn-secondary"
|
|
395
|
+
style={{ fontSize: "11px", padding: "3px 8px" }}
|
|
396
|
+
>
|
|
397
|
+
Stop kernel
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
400
|
+
</section>
|
|
401
|
+
|
|
402
|
+
{/* Compute Profile Section */}
|
|
403
|
+
<section className="runtime-section" style={{ padding: "6px 12px" }}>
|
|
404
|
+
<div
|
|
405
|
+
style={{
|
|
406
|
+
display: "flex",
|
|
407
|
+
justifyContent: "space-between",
|
|
408
|
+
alignItems: "center",
|
|
409
|
+
marginBottom: "3px",
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
<h3 className="runtime-section-title" style={{ fontSize: "12px" }}>
|
|
413
|
+
Compute profile
|
|
414
|
+
</h3>
|
|
415
|
+
<span className="runtime-cost" style={{ fontSize: "11px" }}>
|
|
416
|
+
$0.00 / hour
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
{/* GPU Pods Section */}
|
|
421
|
+
<div className="runtime-subsection" style={{ marginTop: "30px" }}>
|
|
422
|
+
<div
|
|
423
|
+
className="runtime-subsection-header"
|
|
424
|
+
style={{ marginBottom: "4px" }}
|
|
425
|
+
>
|
|
426
|
+
<h4
|
|
427
|
+
className="runtime-subsection-title"
|
|
428
|
+
style={{ fontSize: "11px" }}
|
|
429
|
+
>
|
|
430
|
+
Remote GPU Pods
|
|
431
|
+
</h4>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{apiConfigured === false ? (
|
|
435
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
436
|
+
<p
|
|
437
|
+
style={{
|
|
438
|
+
marginBottom: "4px",
|
|
439
|
+
color: "var(--text-secondary)",
|
|
440
|
+
fontSize: "10px",
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
Enter API key to enable GPU pods
|
|
444
|
+
</p>
|
|
445
|
+
<div style={{ marginBottom: "4px", width: "100%" }}>
|
|
446
|
+
<input
|
|
447
|
+
type="password"
|
|
448
|
+
placeholder="API key"
|
|
449
|
+
value={apiKey}
|
|
450
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
451
|
+
onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
|
|
452
|
+
style={{
|
|
453
|
+
width: "100%",
|
|
454
|
+
padding: "4px 6px",
|
|
455
|
+
borderRadius: "3px",
|
|
456
|
+
border: "1px solid var(--border-color)",
|
|
457
|
+
backgroundColor: "var(--background)",
|
|
458
|
+
color: "var(--text)",
|
|
459
|
+
fontSize: "11px",
|
|
460
|
+
marginBottom: "3px",
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
{saveError && (
|
|
464
|
+
<p
|
|
465
|
+
style={{
|
|
466
|
+
color: "var(--error-color)",
|
|
467
|
+
fontSize: "10px",
|
|
468
|
+
marginBottom: "4px",
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
{saveError}
|
|
472
|
+
</p>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
<div style={{ display: "flex", gap: "4px", width: "100%" }}>
|
|
476
|
+
<button
|
|
477
|
+
className="runtime-btn runtime-btn-primary"
|
|
478
|
+
onClick={handleSaveApiKey}
|
|
479
|
+
disabled={saving}
|
|
480
|
+
style={{ flex: 1, fontSize: "11px", padding: "4px 8px" }}
|
|
481
|
+
>
|
|
482
|
+
{saving ? "Saving..." : "Save"}
|
|
483
|
+
</button>
|
|
484
|
+
<button
|
|
485
|
+
className="runtime-btn runtime-btn-secondary"
|
|
486
|
+
onClick={handleConnectToPrimeIntellect}
|
|
487
|
+
style={{ fontSize: "11px", padding: "4px 8px" }}
|
|
488
|
+
>
|
|
489
|
+
<ExternalLink size={10} style={{ marginRight: "3px" }} />
|
|
490
|
+
Get Key
|
|
491
|
+
</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
) : loading || apiConfigured === null ? (
|
|
495
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
496
|
+
<p style={{ color: "var(--text-secondary)", fontSize: "10px" }}>
|
|
497
|
+
Loading...
|
|
498
|
+
</p>
|
|
499
|
+
</div>
|
|
500
|
+
) : gpuPods.length === 0 ? (
|
|
501
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
502
|
+
<p
|
|
503
|
+
style={{
|
|
504
|
+
marginBottom: "4px",
|
|
505
|
+
color: "var(--text-secondary)",
|
|
506
|
+
fontSize: "10px",
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
No GPU pods. Browse GPUs to create.
|
|
510
|
+
</p>
|
|
511
|
+
<div style={{ display: "flex", gap: "4px", width: "100%" }}>
|
|
512
|
+
<button
|
|
513
|
+
className="runtime-btn runtime-btn-primary"
|
|
514
|
+
onClick={() => {
|
|
515
|
+
setShowBrowseGPUs(!showBrowseGPUs);
|
|
516
|
+
if (!showBrowseGPUs && availableGPUs.length === 0) {
|
|
517
|
+
loadAvailableGPUs();
|
|
518
|
+
}
|
|
519
|
+
}}
|
|
520
|
+
style={{ flex: 1, fontSize: "11px", padding: "4px 8px" }}
|
|
521
|
+
>
|
|
522
|
+
<Search size={10} style={{ marginRight: "3px" }} />
|
|
523
|
+
Browse GPUs
|
|
524
|
+
</button>
|
|
525
|
+
<button
|
|
526
|
+
className="runtime-btn runtime-btn-secondary"
|
|
527
|
+
onClick={handleConnectToPrimeIntellect}
|
|
528
|
+
style={{ fontSize: "11px", padding: "4px 8px" }}
|
|
529
|
+
>
|
|
530
|
+
<ExternalLink size={10} style={{ marginRight: "3px" }} />
|
|
531
|
+
Manage
|
|
532
|
+
</button>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
) : (
|
|
536
|
+
<>
|
|
537
|
+
<div className="runtime-gpu-list">
|
|
538
|
+
{gpuPods.map((pod) => (
|
|
539
|
+
<div key={pod.id} className="runtime-gpu-item">
|
|
540
|
+
<div className="runtime-gpu-info">
|
|
541
|
+
<div className="runtime-gpu-header">
|
|
542
|
+
<span className="runtime-gpu-name">{pod.name}</span>
|
|
543
|
+
<span
|
|
544
|
+
className={`runtime-status-badge runtime-status-${pod.status}`}
|
|
545
|
+
>
|
|
546
|
+
<Activity size={10} />
|
|
547
|
+
{pod.status}
|
|
548
|
+
</span>
|
|
549
|
+
</div>
|
|
550
|
+
<div className="runtime-gpu-details">
|
|
551
|
+
<span className="runtime-gpu-type">
|
|
552
|
+
{pod.gpuType}
|
|
553
|
+
</span>
|
|
554
|
+
<span className="runtime-gpu-region">
|
|
555
|
+
{pod.region}
|
|
556
|
+
</span>
|
|
557
|
+
<span className="runtime-gpu-cost">
|
|
558
|
+
${pod.costPerHour.toFixed(2)}/hour
|
|
559
|
+
</span>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
<div style={{ display: "flex", gap: "4px" }}>
|
|
563
|
+
{pod.status === "running" ? (
|
|
564
|
+
connectedPodId === pod.id ? (
|
|
565
|
+
<button
|
|
566
|
+
className="runtime-btn runtime-btn-sm"
|
|
567
|
+
onClick={handleDisconnect}
|
|
568
|
+
style={{
|
|
569
|
+
fontSize: "11px",
|
|
570
|
+
padding: "4px 8px",
|
|
571
|
+
backgroundColor: "var(--success)",
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
Disconnect
|
|
575
|
+
</button>
|
|
576
|
+
) : (
|
|
577
|
+
<button
|
|
578
|
+
className="runtime-btn runtime-btn-sm"
|
|
579
|
+
onClick={() => handleConnectToPod(pod.id)}
|
|
580
|
+
disabled={connectingPodId === pod.id}
|
|
581
|
+
style={{ fontSize: "11px", padding: "4px 8px" }}
|
|
582
|
+
>
|
|
583
|
+
{connectingPodId === pod.id
|
|
584
|
+
? "Connecting..."
|
|
585
|
+
: "Connect"}
|
|
586
|
+
</button>
|
|
587
|
+
)
|
|
588
|
+
) : (
|
|
589
|
+
<button
|
|
590
|
+
className="runtime-btn runtime-btn-sm runtime-btn-secondary"
|
|
591
|
+
style={{ fontSize: "11px", padding: "4px 8px" }}
|
|
592
|
+
disabled
|
|
593
|
+
>
|
|
594
|
+
{pod.status === "starting"
|
|
595
|
+
? "Starting..."
|
|
596
|
+
: "Stopped"}
|
|
597
|
+
</button>
|
|
598
|
+
)}
|
|
599
|
+
<button
|
|
600
|
+
className="runtime-btn runtime-btn-sm"
|
|
601
|
+
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
602
|
+
disabled={deletingPodId === pod.id}
|
|
603
|
+
style={{
|
|
604
|
+
fontSize: "11px",
|
|
605
|
+
padding: "4px 8px",
|
|
606
|
+
backgroundColor: "var(--error-color)",
|
|
607
|
+
color: "white",
|
|
608
|
+
}}
|
|
609
|
+
>
|
|
610
|
+
{deletingPodId === pod.id ? "..." : "×"}
|
|
611
|
+
</button>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
))}
|
|
615
|
+
</div>
|
|
616
|
+
<div style={{ display: "flex", gap: "4px" }}>
|
|
617
|
+
<button
|
|
618
|
+
className="runtime-btn runtime-btn-link"
|
|
619
|
+
onClick={() => loadGPUPods()}
|
|
620
|
+
style={{ fontSize: "12px", padding: "6px 8px", flex: 1 }}
|
|
621
|
+
>
|
|
622
|
+
Refresh
|
|
623
|
+
</button>
|
|
624
|
+
<button
|
|
625
|
+
className="runtime-btn runtime-btn-link"
|
|
626
|
+
onClick={() => {
|
|
627
|
+
setShowBrowseGPUs(!showBrowseGPUs);
|
|
628
|
+
if (!showBrowseGPUs && availableGPUs.length === 0) {
|
|
629
|
+
loadAvailableGPUs();
|
|
630
|
+
}
|
|
631
|
+
}}
|
|
632
|
+
style={{ fontSize: "12px", padding: "6px 8px", flex: 1 }}
|
|
633
|
+
>
|
|
634
|
+
<Plus size={12} style={{ marginRight: "4px" }} />
|
|
635
|
+
Browse GPUs
|
|
636
|
+
</button>
|
|
637
|
+
</div>
|
|
638
|
+
</>
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
{/* Browse Available GPUs Section */}
|
|
643
|
+
{apiConfigured && showBrowseGPUs && (
|
|
644
|
+
<div className="runtime-subsection" style={{ marginTop: "6px" }}>
|
|
645
|
+
<div
|
|
646
|
+
className="runtime-subsection-header"
|
|
647
|
+
style={{ marginBottom: "4px" }}
|
|
648
|
+
>
|
|
649
|
+
<h4
|
|
650
|
+
className="runtime-subsection-title"
|
|
651
|
+
style={{ fontSize: "11px" }}
|
|
652
|
+
>
|
|
653
|
+
<Filter size={10} style={{ marginRight: "2px" }} />
|
|
654
|
+
Browse GPUs
|
|
655
|
+
</h4>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
{/* Filter and Search Bar */}
|
|
659
|
+
<div
|
|
660
|
+
style={{
|
|
661
|
+
marginBottom: "6px",
|
|
662
|
+
display: "flex",
|
|
663
|
+
gap: "4px",
|
|
664
|
+
alignItems: "center",
|
|
665
|
+
}}
|
|
666
|
+
>
|
|
667
|
+
<button
|
|
668
|
+
className="runtime-btn runtime-btn-secondary"
|
|
669
|
+
onClick={() => setShowFilterPopup(!showFilterPopup)}
|
|
670
|
+
style={{
|
|
671
|
+
padding: "4px 8px",
|
|
672
|
+
fontSize: "11px",
|
|
673
|
+
position: "relative",
|
|
674
|
+
}}
|
|
675
|
+
>
|
|
676
|
+
<Filter size={10} style={{ marginRight: "3px" }} />
|
|
677
|
+
Filter
|
|
678
|
+
{(filters.gpu_type ||
|
|
679
|
+
filters.gpu_count ||
|
|
680
|
+
filters.security ||
|
|
681
|
+
filters.socket) && (
|
|
682
|
+
<span
|
|
683
|
+
style={{
|
|
684
|
+
position: "absolute",
|
|
685
|
+
top: "-2px",
|
|
686
|
+
right: "-2px",
|
|
687
|
+
width: "8px",
|
|
688
|
+
height: "8px",
|
|
689
|
+
borderRadius: "50%",
|
|
690
|
+
backgroundColor: "var(--accent)",
|
|
691
|
+
}}
|
|
692
|
+
/>
|
|
693
|
+
)}
|
|
694
|
+
</button>
|
|
695
|
+
<button
|
|
696
|
+
className="runtime-btn runtime-btn-primary"
|
|
697
|
+
onClick={loadAvailableGPUs}
|
|
698
|
+
disabled={loadingAvailability}
|
|
699
|
+
style={{
|
|
700
|
+
flex: 1,
|
|
701
|
+
padding: "4px 8px",
|
|
702
|
+
fontSize: "11px",
|
|
703
|
+
}}
|
|
704
|
+
>
|
|
705
|
+
<Search size={10} style={{ marginRight: "3px" }} />
|
|
706
|
+
{loadingAvailability ? "Searching..." : "Search"}
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
{/* Filter Popup */}
|
|
711
|
+
<FilterPopup
|
|
712
|
+
isOpen={showFilterPopup}
|
|
713
|
+
onClose={() => setShowFilterPopup(false)}
|
|
714
|
+
filters={filters}
|
|
715
|
+
onFiltersChange={setFilters}
|
|
716
|
+
onApply={loadAvailableGPUs}
|
|
717
|
+
/>
|
|
718
|
+
|
|
719
|
+
{/* Results */}
|
|
720
|
+
{loadingAvailability ? (
|
|
721
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
722
|
+
<p
|
|
723
|
+
style={{ color: "var(--text-secondary)", fontSize: "10px" }}
|
|
724
|
+
>
|
|
725
|
+
Loading...
|
|
726
|
+
</p>
|
|
727
|
+
</div>
|
|
728
|
+
) : availableGPUs.length === 0 ? (
|
|
729
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
730
|
+
<p
|
|
731
|
+
style={{ color: "var(--text-secondary)", fontSize: "10px" }}
|
|
732
|
+
>
|
|
733
|
+
Click Search to find GPUs
|
|
734
|
+
</p>
|
|
735
|
+
</div>
|
|
736
|
+
) : (
|
|
737
|
+
<div style={{ maxHeight: "300px", overflowY: "auto" }}>
|
|
738
|
+
{availableGPUs.map((gpu, index) => (
|
|
739
|
+
<div
|
|
740
|
+
key={`${gpu.cloudId}-${index}`}
|
|
741
|
+
style={{
|
|
742
|
+
padding: "4px 6px",
|
|
743
|
+
borderRadius: "3px",
|
|
744
|
+
border: "1px solid var(--border-color)",
|
|
745
|
+
marginBottom: "3px",
|
|
746
|
+
backgroundColor: "var(--background-secondary)",
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
<div
|
|
750
|
+
style={{
|
|
751
|
+
display: "flex",
|
|
752
|
+
justifyContent: "space-between",
|
|
753
|
+
alignItems: "flex-start",
|
|
754
|
+
marginBottom: "3px",
|
|
755
|
+
}}
|
|
756
|
+
>
|
|
757
|
+
<div>
|
|
758
|
+
<div
|
|
759
|
+
style={{
|
|
760
|
+
fontWeight: 600,
|
|
761
|
+
fontSize: "11px",
|
|
762
|
+
marginBottom: "1px",
|
|
763
|
+
}}
|
|
764
|
+
>
|
|
765
|
+
{gpu.gpuType} ({gpu.gpuCount}x)
|
|
766
|
+
</div>
|
|
767
|
+
<div
|
|
768
|
+
style={{
|
|
769
|
+
fontSize: "9px",
|
|
770
|
+
color: "var(--text-secondary)",
|
|
771
|
+
}}
|
|
772
|
+
>
|
|
773
|
+
{gpu.provider} - {gpu.socket} - {gpu.gpuMemory}GB
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
<div style={{ textAlign: "right" }}>
|
|
777
|
+
<div
|
|
778
|
+
style={{
|
|
779
|
+
fontWeight: 600,
|
|
780
|
+
fontSize: "11px",
|
|
781
|
+
color: "var(--accent)",
|
|
782
|
+
}}
|
|
783
|
+
>
|
|
784
|
+
${gpu.prices?.onDemand?.toFixed(2) || "N/A"}/hr
|
|
785
|
+
</div>
|
|
786
|
+
{gpu.stockStatus && (
|
|
787
|
+
<div
|
|
788
|
+
style={{
|
|
789
|
+
fontSize: "9px",
|
|
790
|
+
color:
|
|
791
|
+
gpu.stockStatus === "Available"
|
|
792
|
+
? "var(--success)"
|
|
793
|
+
: "var(--text-secondary)",
|
|
794
|
+
marginTop: "1px",
|
|
795
|
+
}}
|
|
796
|
+
>
|
|
797
|
+
{gpu.stockStatus}
|
|
798
|
+
</div>
|
|
799
|
+
)}
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
<div
|
|
803
|
+
style={{
|
|
804
|
+
display: "flex",
|
|
805
|
+
gap: "4px",
|
|
806
|
+
fontSize: "9px",
|
|
807
|
+
color: "var(--text-secondary)",
|
|
808
|
+
alignItems: "center",
|
|
809
|
+
justifyContent: "space-between",
|
|
810
|
+
}}
|
|
811
|
+
>
|
|
812
|
+
<div
|
|
813
|
+
style={{
|
|
814
|
+
display: "flex",
|
|
815
|
+
gap: "4px",
|
|
816
|
+
flexWrap: "wrap",
|
|
817
|
+
flex: 1,
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
{gpu.region && (
|
|
821
|
+
<span style={{ marginRight: "8px" }}>
|
|
822
|
+
{gpu.region}
|
|
823
|
+
</span>
|
|
824
|
+
)}
|
|
825
|
+
{gpu.dataCenter && (
|
|
826
|
+
<span style={{ marginRight: "8px" }}>
|
|
827
|
+
{gpu.dataCenter}
|
|
828
|
+
</span>
|
|
829
|
+
)}
|
|
830
|
+
{gpu.security && (
|
|
831
|
+
<span
|
|
832
|
+
style={{
|
|
833
|
+
backgroundColor:
|
|
834
|
+
gpu.security === "secure_cloud"
|
|
835
|
+
? "var(--success-bg)"
|
|
836
|
+
: "var(--info-bg)",
|
|
837
|
+
color:
|
|
838
|
+
gpu.security === "secure_cloud"
|
|
839
|
+
? "var(--success)"
|
|
840
|
+
: "var(--info)",
|
|
841
|
+
padding: "1px 4px",
|
|
842
|
+
borderRadius: "2px",
|
|
843
|
+
fontSize: "9px",
|
|
844
|
+
}}
|
|
845
|
+
>
|
|
846
|
+
{gpu.security === "secure_cloud"
|
|
847
|
+
? "Secure"
|
|
848
|
+
: "Community"}
|
|
849
|
+
</span>
|
|
850
|
+
)}
|
|
851
|
+
</div>
|
|
852
|
+
<button
|
|
853
|
+
className="runtime-btn runtime-btn-sm runtime-btn-primary"
|
|
854
|
+
onClick={() => createPodFromGpu(gpu)}
|
|
855
|
+
disabled={creatingPodId === gpu.cloudId}
|
|
856
|
+
style={{
|
|
857
|
+
fontSize: "10px",
|
|
858
|
+
padding: "3px 6px",
|
|
859
|
+
whiteSpace: "nowrap",
|
|
860
|
+
}}
|
|
861
|
+
>
|
|
862
|
+
{creatingPodId === gpu.cloudId
|
|
863
|
+
? "Creating..."
|
|
864
|
+
: "Create"}
|
|
865
|
+
</button>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
))}
|
|
869
|
+
</div>
|
|
870
|
+
)}
|
|
871
|
+
</div>
|
|
872
|
+
)}
|
|
873
|
+
</section>
|
|
874
|
+
</div>
|
|
875
|
+
</>
|
|
876
|
+
);
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
export default ComputePopup;
|