more-compute 0.4.4__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frontend/app/globals.css +734 -27
- frontend/app/layout.tsx +13 -3
- frontend/components/Notebook.tsx +2 -14
- frontend/components/cell/MonacoCell.tsx +99 -5
- frontend/components/layout/Sidebar.tsx +39 -4
- frontend/components/panels/ClaudePanel.tsx +461 -0
- frontend/components/popups/ComputePopup.tsx +738 -447
- frontend/components/popups/FilterPopup.tsx +305 -189
- frontend/components/popups/MetricsPopup.tsx +20 -1
- frontend/components/popups/ProviderConfigModal.tsx +322 -0
- frontend/components/popups/ProviderDropdown.tsx +398 -0
- frontend/components/popups/SettingsPopup.tsx +1 -1
- frontend/contexts/ClaudeContext.tsx +392 -0
- frontend/contexts/PodWebSocketContext.tsx +16 -21
- frontend/hooks/useInlineDiff.ts +269 -0
- frontend/lib/api.ts +323 -12
- frontend/lib/settings.ts +5 -0
- frontend/lib/websocket-native.ts +4 -8
- frontend/lib/websocket.ts +1 -2
- frontend/package-lock.json +733 -36
- frontend/package.json +2 -0
- frontend/public/assets/icons/providers/lambda_labs.svg +22 -0
- frontend/public/assets/icons/providers/prime_intellect.svg +18 -0
- frontend/public/assets/icons/providers/runpod.svg +9 -0
- frontend/public/assets/icons/providers/vastai.svg +1 -0
- frontend/settings.md +54 -0
- frontend/tsconfig.tsbuildinfo +1 -0
- frontend/types/claude.ts +194 -0
- kernel_run.py +13 -0
- {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
- {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
- {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/WHEEL +1 -1
- morecompute/__init__.py +1 -1
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +24 -67
- morecompute/execution/worker.py +6 -72
- morecompute/models/api_models.py +62 -0
- morecompute/notebook.py +11 -0
- morecompute/server.py +641 -133
- morecompute/services/claude_service.py +392 -0
- morecompute/services/pod_manager.py +168 -67
- morecompute/services/pod_monitor.py +67 -39
- morecompute/services/prime_intellect.py +0 -4
- morecompute/services/providers/__init__.py +92 -0
- morecompute/services/providers/base_provider.py +336 -0
- morecompute/services/providers/lambda_labs_provider.py +394 -0
- morecompute/services/providers/provider_factory.py +194 -0
- morecompute/services/providers/runpod_provider.py +504 -0
- morecompute/services/providers/vastai_provider.py +407 -0
- morecompute/utils/cell_magics.py +0 -3
- morecompute/utils/config_util.py +93 -3
- morecompute/utils/special_commands.py +5 -32
- morecompute/utils/version_check.py +117 -0
- frontend/styling_README.md +0 -23
- {more_compute-0.4.4.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
- {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Plus,
|
|
6
|
-
Activity,
|
|
7
|
-
Search,
|
|
8
|
-
Filter,
|
|
9
|
-
} from "lucide-react";
|
|
1
|
+
import ConfirmModal from "@/components/modals/ConfirmModal";
|
|
2
|
+
import ErrorModal from "@/components/modals/ErrorModal";
|
|
3
|
+
import SuccessModal from "@/components/modals/SuccessModal";
|
|
4
|
+
import { usePodWebSocket } from "@/contexts/PodWebSocketContext";
|
|
10
5
|
import {
|
|
11
|
-
|
|
12
|
-
fetchGpuConfig,
|
|
13
|
-
setGpuApiKey,
|
|
14
|
-
fetchGpuAvailability,
|
|
15
|
-
createGpuPod,
|
|
16
|
-
deleteGpuPod,
|
|
17
|
-
connectToPod,
|
|
6
|
+
CreatePodRequest,
|
|
18
7
|
disconnectFromPod,
|
|
8
|
+
fetchGpuConfig,
|
|
19
9
|
getPodConnectionStatus,
|
|
20
|
-
PodResponse,
|
|
21
|
-
PodsListParams,
|
|
22
10
|
GpuAvailability,
|
|
23
11
|
GpuAvailabilityParams,
|
|
24
|
-
CreatePodRequest,
|
|
25
12
|
PodConnectionStatus,
|
|
13
|
+
PodResponse,
|
|
14
|
+
PodsListParams,
|
|
15
|
+
setGpuApiKey,
|
|
16
|
+
// Multi-provider API
|
|
17
|
+
ProviderInfo,
|
|
18
|
+
fetchProviderGpuAvailability,
|
|
19
|
+
fetchProviderPods,
|
|
20
|
+
createProviderPod,
|
|
21
|
+
deleteProviderPod,
|
|
22
|
+
connectToProviderPod,
|
|
26
23
|
} from "@/lib/api";
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import ConfirmModal from "@/components/modals/ConfirmModal";
|
|
24
|
+
import { ExternalLink, Filter } from "lucide-react";
|
|
25
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
30
26
|
import FilterPopup from "./FilterPopup";
|
|
31
|
-
import
|
|
27
|
+
import ProviderDropdown from "./ProviderDropdown";
|
|
28
|
+
import ProviderConfigModal from "./ProviderConfigModal";
|
|
32
29
|
|
|
33
30
|
interface GPUPod {
|
|
34
31
|
id: string;
|
|
@@ -71,7 +68,9 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
71
68
|
const [creatingPodId, setCreatingPodId] = useState<string | null>(null);
|
|
72
69
|
const [podCreationError, setPodCreationError] = useState<string | null>(null);
|
|
73
70
|
const [deletingPodId, setDeletingPodId] = useState<string | null>(null);
|
|
74
|
-
const [connectionHealth, setConnectionHealth] = useState<
|
|
71
|
+
const [connectionHealth, setConnectionHealth] = useState<
|
|
72
|
+
"healthy" | "unhealthy" | "unknown"
|
|
73
|
+
>("unknown");
|
|
75
74
|
const [searchQuery, setSearchQuery] = useState("");
|
|
76
75
|
const [discoveredPod, setDiscoveredPod] = useState<GPUPod | null>(null);
|
|
77
76
|
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
|
|
@@ -79,6 +78,15 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
79
78
|
// Filter popup state
|
|
80
79
|
const [showFilterPopup, setShowFilterPopup] = useState(false);
|
|
81
80
|
|
|
81
|
+
// Provider state
|
|
82
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderInfo | null>(null);
|
|
83
|
+
const [showProviderConfig, setShowProviderConfig] = useState(false);
|
|
84
|
+
const [providerToConfig, setProviderToConfig] = useState<ProviderInfo | null>(null);
|
|
85
|
+
|
|
86
|
+
// Loading error states
|
|
87
|
+
const [podsLoadError, setPodsLoadError] = useState<string | null>(null);
|
|
88
|
+
const [gpusLoadError, setGpusLoadError] = useState<string | null>(null);
|
|
89
|
+
|
|
82
90
|
// Error modal state
|
|
83
91
|
const [errorModal, setErrorModal] = useState<{
|
|
84
92
|
isOpen: boolean;
|
|
@@ -166,11 +174,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
166
174
|
setConnectionHealth("healthy");
|
|
167
175
|
} else if (!status.connected && status.pod) {
|
|
168
176
|
// Discovered running pod but not connected (backend restart scenario)
|
|
169
|
-
console.log("[COMPUTE POPUP] Discovered running pod after restart:", status.pod);
|
|
170
177
|
// Only show discovered pod warning if it's not already in the pods list
|
|
171
178
|
// (This can happen if pods haven't loaded yet)
|
|
172
179
|
const pod = status.pod;
|
|
173
|
-
const alreadyInList = gpuPods.some(p => p.id === pod.id);
|
|
180
|
+
const alreadyInList = gpuPods.some((p) => p.id === pod.id);
|
|
174
181
|
if (!alreadyInList) {
|
|
175
182
|
setDiscoveredPod({
|
|
176
183
|
id: pod.id,
|
|
@@ -192,20 +199,45 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
192
199
|
checkApiConfig();
|
|
193
200
|
}, []);
|
|
194
201
|
|
|
195
|
-
const loadGPUPods = async (params?: PodsListParams) => {
|
|
202
|
+
const loadGPUPods = async (params?: PodsListParams, provider?: ProviderInfo | null) => {
|
|
203
|
+
// Use passed provider or fall back to state
|
|
204
|
+
const activeProvider = provider !== undefined ? provider : selectedProvider;
|
|
205
|
+
|
|
206
|
+
// Require a configured provider - don't load pods without one
|
|
207
|
+
if (!activeProvider || !activeProvider.configured) {
|
|
208
|
+
setPods([]);
|
|
209
|
+
setPodsLoadError(null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
setLoading(true);
|
|
214
|
+
setPodsLoadError(null);
|
|
197
215
|
try {
|
|
198
|
-
const response = await
|
|
216
|
+
const response = await fetchProviderPods(activeProvider.name, params || { limit: 100 });
|
|
217
|
+
|
|
199
218
|
const pods = (response.data || []).map((pod: PodResponse) => {
|
|
200
219
|
// Map API status to UI status
|
|
201
220
|
// Pod must be ACTIVE *and* have SSH connection info to be "running"
|
|
202
221
|
let uiStatus: "running" | "stopped" | "starting" = "stopped";
|
|
203
|
-
|
|
222
|
+
const isFullyReady = pod.status === "ACTIVE" && pod.sshConnection;
|
|
223
|
+
|
|
224
|
+
if (isFullyReady) {
|
|
204
225
|
uiStatus = "running";
|
|
205
|
-
} else if (
|
|
226
|
+
} else if (
|
|
227
|
+
pod.status === "ACTIVE" ||
|
|
228
|
+
pod.status === "PROVISIONING" ||
|
|
229
|
+
pod.status === "PENDING"
|
|
230
|
+
) {
|
|
206
231
|
uiStatus = "starting";
|
|
207
232
|
}
|
|
208
233
|
|
|
234
|
+
// Check if this is the pod we're waiting for (provisioning state)
|
|
235
|
+
// and it's now ready - trigger auto-connect
|
|
236
|
+
if (isFullyReady && connectingPodId === pod.id && connectionState === "provisioning") {
|
|
237
|
+
// Schedule auto-connect (give a small delay to avoid race conditions)
|
|
238
|
+
setTimeout(() => handleConnectToPod(pod.id), 1000);
|
|
239
|
+
}
|
|
240
|
+
|
|
209
241
|
return {
|
|
210
242
|
id: pod.id,
|
|
211
243
|
name: pod.name,
|
|
@@ -219,32 +251,54 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
219
251
|
setPods(pods);
|
|
220
252
|
} catch (err) {
|
|
221
253
|
console.error("Failed to load GPU pods:", err);
|
|
254
|
+
const errorMsg = err instanceof Error ? err.message : "Failed to load pods";
|
|
255
|
+
|
|
222
256
|
// If 401 error, API key is invalid - allow user to reset it
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
257
|
+
if (errorMsg.includes("401")) {
|
|
258
|
+
setPodsLoadError(`Invalid API key. Please reconfigure ${activeProvider.display_name}.`);
|
|
259
|
+
// Mark provider as needing reconfiguration
|
|
260
|
+
localStorage.removeItem("morecompute_active_provider");
|
|
261
|
+
setSelectedProvider({ ...activeProvider, configured: false });
|
|
262
|
+
} else if (errorMsg.includes("301") || errorMsg.includes("redirect")) {
|
|
263
|
+
setPodsLoadError(`${activeProvider.display_name} API error. Please try again.`);
|
|
264
|
+
} else {
|
|
265
|
+
setPodsLoadError(`Failed to load pods: ${errorMsg}`);
|
|
231
266
|
}
|
|
267
|
+
setPods([]);
|
|
232
268
|
} finally {
|
|
233
269
|
setLoading(false);
|
|
234
270
|
}
|
|
235
271
|
};
|
|
236
272
|
|
|
237
|
-
const loadAvailableGPUs = async () => {
|
|
273
|
+
const loadAvailableGPUs = async (provider?: ProviderInfo | null) => {
|
|
274
|
+
// Use passed provider or fall back to state
|
|
275
|
+
const activeProvider = provider !== undefined ? provider : selectedProvider;
|
|
276
|
+
|
|
277
|
+
// Require a configured provider - don't show GPUs without one
|
|
278
|
+
if (!activeProvider || !activeProvider.configured) {
|
|
279
|
+
setAvailableGPUs([]);
|
|
280
|
+
setGpusLoadError(null);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
238
284
|
setLoadingAvailability(true);
|
|
285
|
+
setGpusLoadError(null);
|
|
239
286
|
try {
|
|
240
|
-
const response = await
|
|
241
|
-
const gpuList: GpuAvailability[] = [];
|
|
242
|
-
Object.values(response).forEach((gpus) => {
|
|
243
|
-
gpuList.push(...gpus);
|
|
244
|
-
});
|
|
287
|
+
const response = await fetchProviderGpuAvailability(activeProvider.name, filters);
|
|
288
|
+
const gpuList: GpuAvailability[] = response.data || [];
|
|
245
289
|
setAvailableGPUs(gpuList);
|
|
246
290
|
} catch (err) {
|
|
247
291
|
console.error("Failed to load GPU availability:", err);
|
|
292
|
+
const errorMsg = err instanceof Error ? err.message : "Failed to load GPUs";
|
|
293
|
+
|
|
294
|
+
if (errorMsg.includes("401")) {
|
|
295
|
+
setGpusLoadError(`Invalid API key. Please reconfigure ${activeProvider.display_name}.`);
|
|
296
|
+
} else if (errorMsg.includes("301") || errorMsg.includes("redirect")) {
|
|
297
|
+
setGpusLoadError(`${activeProvider.display_name} API error. Please try again.`);
|
|
298
|
+
} else {
|
|
299
|
+
setGpusLoadError(`Failed to load GPUs: ${errorMsg}`);
|
|
300
|
+
}
|
|
301
|
+
setAvailableGPUs([]);
|
|
248
302
|
} finally {
|
|
249
303
|
setLoadingAvailability(false);
|
|
250
304
|
}
|
|
@@ -255,6 +309,11 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
255
309
|
setPodCreationError(null);
|
|
256
310
|
|
|
257
311
|
try {
|
|
312
|
+
// Require a configured provider
|
|
313
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
314
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
315
|
+
}
|
|
316
|
+
|
|
258
317
|
// Generate a pod name based on GPU type and timestamp
|
|
259
318
|
const timestamp = new Date()
|
|
260
319
|
.toISOString()
|
|
@@ -282,7 +341,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
282
341
|
},
|
|
283
342
|
};
|
|
284
343
|
|
|
285
|
-
const newPod = await
|
|
344
|
+
const newPod = await createProviderPod(selectedProvider.name, podRequest);
|
|
286
345
|
|
|
287
346
|
// Register auto-connect callback for when pod becomes ready
|
|
288
347
|
registerAutoConnect(newPod.id, handleConnectToPod);
|
|
@@ -303,6 +362,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
303
362
|
});
|
|
304
363
|
} catch (err) {
|
|
305
364
|
let errorMsg = "Failed to create pod";
|
|
365
|
+
const providerName = selectedProvider?.display_name || "GPU Provider";
|
|
366
|
+
const providerDashboard = selectedProvider?.dashboard_url || "";
|
|
306
367
|
|
|
307
368
|
if (err instanceof Error) {
|
|
308
369
|
errorMsg = err.message;
|
|
@@ -312,11 +373,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
312
373
|
errorMsg.includes("402") ||
|
|
313
374
|
errorMsg.includes("Insufficient funds")
|
|
314
375
|
) {
|
|
315
|
-
errorMsg =
|
|
316
|
-
"Insufficient funds. Please add credits to your Prime Intellect wallet:\nhttps://app.primeintellect.ai/dashboard/billing";
|
|
376
|
+
errorMsg = `Insufficient funds. Please add credits to your ${providerName} account.${providerDashboard ? `\n${providerDashboard}` : ""}`;
|
|
317
377
|
} else if (errorMsg.includes("401") || errorMsg.includes("403")) {
|
|
318
|
-
errorMsg =
|
|
319
|
-
} else if (
|
|
378
|
+
errorMsg = `Authentication failed. Please check your ${providerName} API key configuration.`;
|
|
379
|
+
} else if (
|
|
380
|
+
errorMsg.includes("503") ||
|
|
381
|
+
errorMsg.includes("not available")
|
|
382
|
+
) {
|
|
320
383
|
errorMsg =
|
|
321
384
|
"This GPU type is currently unavailable. Please try selecting a different GPU from the list below.";
|
|
322
385
|
} else if (errorMsg.includes("data_center_id")) {
|
|
@@ -328,13 +391,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
328
391
|
setPodCreationError(errorMsg);
|
|
329
392
|
|
|
330
393
|
// Show error in modal with link to billing if insufficient funds
|
|
331
|
-
if (errorMsg.includes("Insufficient funds")) {
|
|
394
|
+
if (errorMsg.includes("Insufficient funds") && providerDashboard) {
|
|
332
395
|
setErrorModal({
|
|
333
396
|
isOpen: true,
|
|
334
397
|
title: "Insufficient Funds",
|
|
335
398
|
message: errorMsg,
|
|
336
399
|
actionLabel: "Add Credits",
|
|
337
|
-
actionUrl:
|
|
400
|
+
actionUrl: providerDashboard,
|
|
338
401
|
});
|
|
339
402
|
} else {
|
|
340
403
|
setErrorModal({
|
|
@@ -349,9 +412,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
349
412
|
};
|
|
350
413
|
|
|
351
414
|
const handleConnectToPod = async (podId: string) => {
|
|
352
|
-
// Prevent double-connecting
|
|
353
|
-
if (
|
|
354
|
-
|
|
415
|
+
// Prevent double-connecting - but allow transition from "provisioning" to "deploying"
|
|
416
|
+
if (connectedPodId === podId) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Only skip if we're already in "deploying" state (actually connecting)
|
|
420
|
+
if (connectingPodId === podId && connectionState === "deploying") {
|
|
355
421
|
return;
|
|
356
422
|
}
|
|
357
423
|
|
|
@@ -359,11 +425,15 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
359
425
|
setConnectionState("deploying"); // Show "Deploying worker..." banner
|
|
360
426
|
setConnectionHealth("unknown"); // Reset health status during connection
|
|
361
427
|
try {
|
|
428
|
+
// Require a configured provider
|
|
429
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
430
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
431
|
+
}
|
|
432
|
+
|
|
362
433
|
// Initiate connection (now returns immediately with "connecting" status)
|
|
363
|
-
const result = await
|
|
434
|
+
const result = await connectToProviderPod(selectedProvider.name, podId);
|
|
364
435
|
|
|
365
436
|
if (result.status === "connecting") {
|
|
366
|
-
console.log("[CONNECT] Connection initiated, polling for completion...");
|
|
367
437
|
|
|
368
438
|
// Poll connection status until it's connected or fails
|
|
369
439
|
const maxAttempts = 30; // 30 seconds max
|
|
@@ -375,7 +445,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
375
445
|
attempts++;
|
|
376
446
|
|
|
377
447
|
try {
|
|
378
|
-
|
|
448
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
449
|
+
const status: PodConnectionStatus & { error?: string } = await getPodConnectionStatus() as any;
|
|
450
|
+
|
|
451
|
+
// Check for connection error from backend
|
|
452
|
+
if (status.error && !isComplete) {
|
|
453
|
+
isComplete = true;
|
|
454
|
+
clearInterval(pollInterval);
|
|
455
|
+
setConnectionHealth("unhealthy");
|
|
456
|
+
setConnectingPodId(null);
|
|
457
|
+
setConnectionState(null); // Hide banner on error
|
|
458
|
+
|
|
459
|
+
setErrorModal({
|
|
460
|
+
isOpen: true,
|
|
461
|
+
title: "Connection Failed",
|
|
462
|
+
message: status.error,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
379
466
|
|
|
380
467
|
if (status.connected && status.pod?.id === podId && !isComplete) {
|
|
381
468
|
// Successfully connected!
|
|
@@ -391,8 +478,9 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
391
478
|
|
|
392
479
|
setErrorModal({
|
|
393
480
|
isOpen: true,
|
|
394
|
-
title: "
|
|
395
|
-
message:
|
|
481
|
+
title: "Connected!",
|
|
482
|
+
message:
|
|
483
|
+
"Successfully connected to GPU pod. You can now run code on the remote GPU.",
|
|
396
484
|
});
|
|
397
485
|
|
|
398
486
|
// Hide the connected banner after 3 seconds
|
|
@@ -409,7 +497,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
409
497
|
setErrorModal({
|
|
410
498
|
isOpen: true,
|
|
411
499
|
title: "Connection Timeout",
|
|
412
|
-
message:
|
|
500
|
+
message:
|
|
501
|
+
"Connection took too long. The pod may not be ready yet. Check the pod status and try again.",
|
|
413
502
|
});
|
|
414
503
|
}
|
|
415
504
|
} catch (err) {
|
|
@@ -426,7 +515,6 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
426
515
|
});
|
|
427
516
|
}
|
|
428
517
|
}, 1000); // Poll every second
|
|
429
|
-
|
|
430
518
|
} else if (result.status === "ok") {
|
|
431
519
|
// Immediate success (backwards compatible)
|
|
432
520
|
setConnectedPodId(podId);
|
|
@@ -439,7 +527,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
439
527
|
setErrorModal({
|
|
440
528
|
isOpen: true,
|
|
441
529
|
title: "✓ Connected!",
|
|
442
|
-
message:
|
|
530
|
+
message:
|
|
531
|
+
"Successfully connected to GPU pod. You can now run code on the remote GPU.",
|
|
443
532
|
});
|
|
444
533
|
|
|
445
534
|
// Hide the connected banner after 3 seconds
|
|
@@ -454,7 +543,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
454
543
|
|
|
455
544
|
let errorMsg = result.message || "Connection failed";
|
|
456
545
|
|
|
457
|
-
if (
|
|
546
|
+
if (
|
|
547
|
+
errorMsg.includes("SSH authentication") ||
|
|
548
|
+
errorMsg.includes("SSH public key")
|
|
549
|
+
) {
|
|
458
550
|
setErrorModal({
|
|
459
551
|
isOpen: true,
|
|
460
552
|
title: "SSH Key Required",
|
|
@@ -471,7 +563,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
471
563
|
}
|
|
472
564
|
}
|
|
473
565
|
} catch (err) {
|
|
474
|
-
const errorMsg =
|
|
566
|
+
const errorMsg =
|
|
567
|
+
err instanceof Error ? err.message : "Failed to connect to pod";
|
|
475
568
|
setConnectionHealth("unhealthy");
|
|
476
569
|
setConnectingPodId(null);
|
|
477
570
|
setConnectionState(null); // Hide banner on error
|
|
@@ -496,7 +589,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
496
589
|
setErrorModal({
|
|
497
590
|
isOpen: true,
|
|
498
591
|
title: "⚠️ Disconnected",
|
|
499
|
-
message:
|
|
592
|
+
message:
|
|
593
|
+
"Disconnected from pod. Note: The pod is still running and incurring costs. You can reconnect or terminate it below.",
|
|
500
594
|
});
|
|
501
595
|
} catch (err) {
|
|
502
596
|
const errorMsg =
|
|
@@ -528,7 +622,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
528
622
|
setConnectionState(null);
|
|
529
623
|
}
|
|
530
624
|
|
|
531
|
-
|
|
625
|
+
// Require a configured provider
|
|
626
|
+
if (!selectedProvider || !selectedProvider.configured) {
|
|
627
|
+
throw new Error("Please select and configure a GPU provider first.");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
await deleteProviderPod(selectedProvider.name, podId);
|
|
532
631
|
|
|
533
632
|
// Clear discovered pod if it was the one we just deleted
|
|
534
633
|
if (discoveredPod && discoveredPod.id === podId) {
|
|
@@ -556,8 +655,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
556
655
|
});
|
|
557
656
|
};
|
|
558
657
|
|
|
559
|
-
const
|
|
560
|
-
|
|
658
|
+
const handleOpenProviderDashboard = () => {
|
|
659
|
+
if (selectedProvider?.dashboard_url) {
|
|
660
|
+
window.open(selectedProvider.dashboard_url, "_blank");
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Handle provider selection
|
|
665
|
+
const handleProviderSelect = (provider: ProviderInfo) => {
|
|
666
|
+
if (!provider.configured) {
|
|
667
|
+
// Open config modal for unconfigured provider
|
|
668
|
+
setProviderToConfig(provider);
|
|
669
|
+
setShowProviderConfig(true);
|
|
670
|
+
} else {
|
|
671
|
+
// Use the configured provider
|
|
672
|
+
setSelectedProvider(provider);
|
|
673
|
+
// Clear filters when switching providers (provider-specific filters won't apply)
|
|
674
|
+
setFilters({});
|
|
675
|
+
// Reload data from this provider
|
|
676
|
+
loadGPUPods(undefined, provider);
|
|
677
|
+
loadAvailableGPUs(provider);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// Handle provider configuration complete
|
|
682
|
+
const handleProviderConfigured = (provider: ProviderInfo) => {
|
|
683
|
+
setSelectedProvider(provider);
|
|
684
|
+
setShowProviderConfig(false);
|
|
685
|
+
setProviderToConfig(null);
|
|
686
|
+
// Clear filters when switching providers
|
|
687
|
+
setFilters({});
|
|
688
|
+
// Load data from newly configured provider
|
|
689
|
+
loadGPUPods(undefined, provider);
|
|
690
|
+
loadAvailableGPUs(provider);
|
|
561
691
|
};
|
|
562
692
|
|
|
563
693
|
const handleSaveApiKey = async () => {
|
|
@@ -624,12 +754,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
624
754
|
cancelLabel="Cancel"
|
|
625
755
|
isDangerous={true}
|
|
626
756
|
/>
|
|
757
|
+
{showProviderConfig && providerToConfig && (
|
|
758
|
+
<ProviderConfigModal
|
|
759
|
+
provider={providerToConfig}
|
|
760
|
+
onClose={() => {
|
|
761
|
+
setShowProviderConfig(false);
|
|
762
|
+
setProviderToConfig(null);
|
|
763
|
+
}}
|
|
764
|
+
onConfigured={handleProviderConfigured}
|
|
765
|
+
/>
|
|
766
|
+
)}
|
|
627
767
|
<div className="runtime-popup">
|
|
768
|
+
{/* Provider Selector */}
|
|
769
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
770
|
+
<ProviderDropdown
|
|
771
|
+
selectedProvider={selectedProvider}
|
|
772
|
+
onProviderChange={handleProviderSelect}
|
|
773
|
+
onConfigureProvider={(provider) => {
|
|
774
|
+
setProviderToConfig(provider);
|
|
775
|
+
setShowProviderConfig(true);
|
|
776
|
+
}}
|
|
777
|
+
/>
|
|
778
|
+
</section>
|
|
779
|
+
|
|
780
|
+
{/* Divider */}
|
|
781
|
+
<div
|
|
782
|
+
style={{
|
|
783
|
+
height: "1px",
|
|
784
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
785
|
+
margin: "0 16px",
|
|
786
|
+
}}
|
|
787
|
+
/>
|
|
628
788
|
{/* Kernel Status Section */}
|
|
629
|
-
<section
|
|
630
|
-
className="runtime-section"
|
|
631
|
-
style={{ padding: "16px 20px" }}
|
|
632
|
-
>
|
|
789
|
+
<section className="runtime-section" style={{ padding: "16px 20px" }}>
|
|
633
790
|
<div
|
|
634
791
|
style={{
|
|
635
792
|
display: "flex",
|
|
@@ -638,7 +795,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
638
795
|
}}
|
|
639
796
|
>
|
|
640
797
|
<div>
|
|
641
|
-
<div
|
|
798
|
+
<div
|
|
799
|
+
style={{
|
|
800
|
+
fontSize: "11px",
|
|
801
|
+
color: "var(--text-secondary)",
|
|
802
|
+
marginBottom: "4px",
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
642
805
|
The kernel is currently: {kernelStatus ? "Running" : "Stopped"}
|
|
643
806
|
</div>
|
|
644
807
|
</div>
|
|
@@ -646,11 +809,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
646
809
|
</section>
|
|
647
810
|
|
|
648
811
|
{/* Divider */}
|
|
649
|
-
<div
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
812
|
+
<div
|
|
813
|
+
style={{
|
|
814
|
+
height: "1px",
|
|
815
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
816
|
+
margin: "0 16px",
|
|
817
|
+
}}
|
|
818
|
+
/>
|
|
654
819
|
|
|
655
820
|
{/* Compute Profile Section */}
|
|
656
821
|
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
@@ -661,13 +826,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
661
826
|
alignItems: "center",
|
|
662
827
|
}}
|
|
663
828
|
>
|
|
664
|
-
<h3
|
|
829
|
+
<h3
|
|
830
|
+
className="runtime-section-title"
|
|
831
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
832
|
+
>
|
|
665
833
|
Compute Profile
|
|
666
834
|
</h3>
|
|
667
|
-
<span
|
|
835
|
+
<span
|
|
836
|
+
className="runtime-cost"
|
|
837
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
838
|
+
>
|
|
668
839
|
{(() => {
|
|
669
|
-
const runningPods = gpuPods.filter(
|
|
670
|
-
|
|
840
|
+
const runningPods = gpuPods.filter(
|
|
841
|
+
(p) => p.status === "running",
|
|
842
|
+
);
|
|
843
|
+
const totalCost = runningPods.reduce(
|
|
844
|
+
(sum, pod) => sum + pod.costPerHour,
|
|
845
|
+
0,
|
|
846
|
+
);
|
|
671
847
|
return `$${totalCost.toFixed(2)} / hour`;
|
|
672
848
|
})()}
|
|
673
849
|
</span>
|
|
@@ -675,416 +851,523 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
675
851
|
</section>
|
|
676
852
|
|
|
677
853
|
{/* Divider */}
|
|
678
|
-
<div
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
854
|
+
<div
|
|
855
|
+
style={{
|
|
856
|
+
height: "1px",
|
|
857
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
858
|
+
margin: "0 16px",
|
|
859
|
+
}}
|
|
860
|
+
/>
|
|
861
|
+
|
|
862
|
+
{/* GPU Pods Section */}
|
|
863
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
864
|
+
<div
|
|
865
|
+
style={{
|
|
866
|
+
display: "flex",
|
|
867
|
+
justifyContent: "space-between",
|
|
868
|
+
alignItems: "center",
|
|
869
|
+
marginBottom: "12px",
|
|
870
|
+
}}
|
|
871
|
+
>
|
|
872
|
+
<h3
|
|
873
|
+
className="runtime-section-title"
|
|
874
|
+
style={{ fontSize: "12px", fontWeight: 500 }}
|
|
875
|
+
>
|
|
876
|
+
Remote GPU Pods
|
|
877
|
+
</h3>
|
|
878
|
+
{selectedProvider && (
|
|
879
|
+
<button
|
|
880
|
+
className="runtime-btn runtime-btn-secondary"
|
|
881
|
+
onClick={handleOpenProviderDashboard}
|
|
882
|
+
style={{
|
|
883
|
+
fontSize: "11px",
|
|
884
|
+
padding: "6px 12px",
|
|
885
|
+
backgroundColor: "var(--mc-text-color)",
|
|
886
|
+
color: "var(--mc-cell-background)",
|
|
887
|
+
border: "none",
|
|
888
|
+
borderRadius: "8px",
|
|
889
|
+
cursor: "pointer",
|
|
890
|
+
}}
|
|
891
|
+
>
|
|
892
|
+
Manage
|
|
893
|
+
</button>
|
|
894
|
+
)}
|
|
895
|
+
</div>
|
|
683
896
|
|
|
684
|
-
{
|
|
685
|
-
|
|
897
|
+
{apiConfigured === false || showApiKeyInput ? (
|
|
898
|
+
<div className="runtime-empty-state" style={{ padding: "6px" }}>
|
|
899
|
+
<p
|
|
900
|
+
style={{
|
|
901
|
+
marginBottom: "4px",
|
|
902
|
+
color: "var(--text-secondary)",
|
|
903
|
+
fontSize: "10px",
|
|
904
|
+
}}
|
|
905
|
+
>
|
|
906
|
+
Enter API key to enable GPU pods
|
|
907
|
+
</p>
|
|
908
|
+
<div style={{ marginBottom: "4px", width: "100%" }}>
|
|
909
|
+
<input
|
|
910
|
+
type="password"
|
|
911
|
+
placeholder="API key"
|
|
912
|
+
value={apiKey}
|
|
913
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
914
|
+
onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
|
|
915
|
+
style={{
|
|
916
|
+
width: "100%",
|
|
917
|
+
padding: "6px 12px",
|
|
918
|
+
borderRadius: "8px",
|
|
919
|
+
border: "1px solid var(--border-color)",
|
|
920
|
+
backgroundColor: "var(--background)",
|
|
921
|
+
color: "var(--text)",
|
|
922
|
+
fontSize: "11px",
|
|
923
|
+
marginBottom: "3px",
|
|
924
|
+
}}
|
|
925
|
+
/>
|
|
926
|
+
{saveError && (
|
|
927
|
+
<p
|
|
928
|
+
style={{
|
|
929
|
+
color: "var(--error-color)",
|
|
930
|
+
fontSize: "10px",
|
|
931
|
+
marginBottom: "4px",
|
|
932
|
+
}}
|
|
933
|
+
>
|
|
934
|
+
{saveError}
|
|
935
|
+
</p>
|
|
936
|
+
)}
|
|
937
|
+
</div>
|
|
938
|
+
<div style={{ display: "flex", gap: "4px", width: "100%" }}>
|
|
939
|
+
<button
|
|
940
|
+
className="runtime-btn runtime-btn-primary"
|
|
941
|
+
onClick={handleSaveApiKey}
|
|
942
|
+
disabled={saving}
|
|
943
|
+
style={{
|
|
944
|
+
flex: 1,
|
|
945
|
+
fontSize: "11px",
|
|
946
|
+
padding: "6px 12px",
|
|
947
|
+
backgroundColor: "var(--mc-text-color)",
|
|
948
|
+
color: "var(--mc-cell-background)",
|
|
949
|
+
border: "none",
|
|
950
|
+
borderRadius: "8px",
|
|
951
|
+
cursor: "pointer",
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{saving ? "Saving..." : "Save"}
|
|
955
|
+
</button>
|
|
956
|
+
<button
|
|
957
|
+
className="runtime-btn runtime-btn-secondary"
|
|
958
|
+
onClick={handleOpenProviderDashboard}
|
|
959
|
+
style={{
|
|
960
|
+
fontSize: "11px",
|
|
961
|
+
padding: "6px 12px",
|
|
962
|
+
backgroundColor: "var(--mc-text-color)",
|
|
963
|
+
color: "var(--mc-cell-background)",
|
|
964
|
+
border: "none",
|
|
965
|
+
borderRadius: "8px",
|
|
966
|
+
cursor: "pointer",
|
|
967
|
+
}}
|
|
968
|
+
>
|
|
969
|
+
<ExternalLink size={10} style={{ marginRight: "3px" }} />
|
|
970
|
+
Get Key
|
|
971
|
+
</button>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
) : loading || apiConfigured === null ? (
|
|
686
975
|
<div
|
|
687
976
|
style={{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
marginBottom: "12px"
|
|
977
|
+
padding: "8px 0",
|
|
978
|
+
color: "var(--text-secondary)",
|
|
979
|
+
fontSize: "11px",
|
|
692
980
|
}}
|
|
693
981
|
>
|
|
694
|
-
|
|
695
|
-
Remote GPU Pods
|
|
696
|
-
</h3>
|
|
697
|
-
{apiConfigured && !showApiKeyInput && (
|
|
698
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
699
|
-
<button
|
|
700
|
-
className="runtime-btn runtime-btn-secondary"
|
|
701
|
-
onClick={() => setShowApiKeyInput(true)}
|
|
702
|
-
style={{
|
|
703
|
-
fontSize: "11px",
|
|
704
|
-
padding: "6px 12px",
|
|
705
|
-
backgroundColor: "var(--text-secondary)",
|
|
706
|
-
color: "white",
|
|
707
|
-
border: "none",
|
|
708
|
-
borderRadius: "8px",
|
|
709
|
-
cursor: "pointer"
|
|
710
|
-
}}
|
|
711
|
-
>
|
|
712
|
-
Reset API Key
|
|
713
|
-
</button>
|
|
714
|
-
<button
|
|
715
|
-
className="runtime-btn runtime-btn-secondary"
|
|
716
|
-
onClick={handleConnectToPrimeIntellect}
|
|
717
|
-
style={{
|
|
718
|
-
fontSize: "11px",
|
|
719
|
-
padding: "6px 12px",
|
|
720
|
-
backgroundColor: "var(--mc-text-color)",
|
|
721
|
-
color: "var(--mc-cell-background)",
|
|
722
|
-
border: "none",
|
|
723
|
-
borderRadius: "8px",
|
|
724
|
-
cursor: "pointer"
|
|
725
|
-
}}
|
|
726
|
-
>
|
|
727
|
-
Manage
|
|
728
|
-
</button>
|
|
729
|
-
</div>
|
|
730
|
-
)}
|
|
982
|
+
Loading...
|
|
731
983
|
</div>
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
984
|
+
) : podsLoadError ? (
|
|
985
|
+
<div
|
|
986
|
+
style={{
|
|
987
|
+
padding: "8px 12px",
|
|
988
|
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
989
|
+
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
990
|
+
borderRadius: "6px",
|
|
991
|
+
color: "#ef4444",
|
|
992
|
+
fontSize: "11px",
|
|
993
|
+
}}
|
|
994
|
+
>
|
|
995
|
+
{podsLoadError}
|
|
996
|
+
</div>
|
|
997
|
+
) : gpuPods.filter((p) => p.status === "running").length === 0 &&
|
|
998
|
+
!discoveredPod ? (
|
|
999
|
+
<div
|
|
1000
|
+
style={{
|
|
1001
|
+
padding: "8px 0",
|
|
1002
|
+
color: "var(--text-secondary)",
|
|
1003
|
+
fontSize: "11px",
|
|
1004
|
+
}}
|
|
1005
|
+
>
|
|
1006
|
+
Currently not connected to any.
|
|
1007
|
+
</div>
|
|
1008
|
+
) : !connectedPodId && discoveredPod ? (
|
|
1009
|
+
<div style={{ padding: "8px 0" }}>
|
|
1010
|
+
<div
|
|
1011
|
+
style={{
|
|
1012
|
+
backgroundColor: "rgba(251, 191, 36, 0.1)",
|
|
1013
|
+
border: "1px solid rgba(251, 191, 36, 0.3)",
|
|
1014
|
+
borderRadius: "8px",
|
|
1015
|
+
padding: "12px",
|
|
1016
|
+
marginBottom: "8px",
|
|
1017
|
+
}}
|
|
1018
|
+
>
|
|
1019
|
+
<div
|
|
736
1020
|
style={{
|
|
1021
|
+
fontSize: "11px",
|
|
1022
|
+
fontWeight: 600,
|
|
1023
|
+
color: "var(--mc-text-color)",
|
|
737
1024
|
marginBottom: "4px",
|
|
1025
|
+
}}
|
|
1026
|
+
>
|
|
1027
|
+
⚠️ Running Pod Detected
|
|
1028
|
+
</div>
|
|
1029
|
+
<div
|
|
1030
|
+
style={{
|
|
1031
|
+
fontSize: "10px",
|
|
738
1032
|
color: "var(--text-secondary)",
|
|
1033
|
+
marginBottom: "8px",
|
|
1034
|
+
}}
|
|
1035
|
+
>
|
|
1036
|
+
Found a running pod but not connected (backend may have
|
|
1037
|
+
restarted). This pod is still costing money!
|
|
1038
|
+
</div>
|
|
1039
|
+
<div
|
|
1040
|
+
style={{
|
|
739
1041
|
fontSize: "10px",
|
|
1042
|
+
marginBottom: "8px",
|
|
1043
|
+
color: "var(--mc-text-color)",
|
|
740
1044
|
}}
|
|
741
1045
|
>
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
<input
|
|
746
|
-
type="password"
|
|
747
|
-
placeholder="API key"
|
|
748
|
-
value={apiKey}
|
|
749
|
-
onChange={(e) => setApiKey(e.target.value)}
|
|
750
|
-
onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
|
|
751
|
-
style={{
|
|
752
|
-
width: "100%",
|
|
753
|
-
padding: "6px 12px",
|
|
754
|
-
borderRadius: "8px",
|
|
755
|
-
border: "1px solid var(--border-color)",
|
|
756
|
-
backgroundColor: "var(--background)",
|
|
757
|
-
color: "var(--text)",
|
|
758
|
-
fontSize: "11px",
|
|
759
|
-
marginBottom: "3px",
|
|
760
|
-
}}
|
|
761
|
-
/>
|
|
762
|
-
{saveError && (
|
|
763
|
-
<p
|
|
764
|
-
style={{
|
|
765
|
-
color: "var(--error-color)",
|
|
766
|
-
fontSize: "10px",
|
|
767
|
-
marginBottom: "4px",
|
|
768
|
-
}}
|
|
769
|
-
>
|
|
770
|
-
{saveError}
|
|
771
|
-
</p>
|
|
772
|
-
)}
|
|
1046
|
+
<span style={{ fontWeight: 500 }}>{discoveredPod.name}</span>{" "}
|
|
1047
|
+
• {discoveredPod.gpuType} • $
|
|
1048
|
+
{discoveredPod.costPerHour.toFixed(2)}/hour
|
|
773
1049
|
</div>
|
|
774
|
-
<div style={{ display: "flex", gap: "
|
|
1050
|
+
<div style={{ display: "flex", gap: "6px" }}>
|
|
775
1051
|
<button
|
|
776
|
-
className="runtime-btn runtime-btn-
|
|
777
|
-
onClick={
|
|
778
|
-
disabled={saving}
|
|
1052
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1053
|
+
onClick={() => handleConnectToPod(discoveredPod.id)}
|
|
779
1054
|
style={{
|
|
780
1055
|
flex: 1,
|
|
781
|
-
fontSize: "
|
|
1056
|
+
fontSize: "10px",
|
|
782
1057
|
padding: "6px 12px",
|
|
783
|
-
backgroundColor: "
|
|
784
|
-
color: "var(--mc-
|
|
1058
|
+
backgroundColor: "rgba(251, 191, 36, 0.8)",
|
|
1059
|
+
color: "var(--mc-text-color)",
|
|
785
1060
|
border: "none",
|
|
786
1061
|
borderRadius: "8px",
|
|
787
|
-
cursor: "pointer"
|
|
1062
|
+
cursor: "pointer",
|
|
1063
|
+
fontWeight: 600,
|
|
788
1064
|
}}
|
|
789
1065
|
>
|
|
790
|
-
|
|
1066
|
+
Reconnect
|
|
791
1067
|
</button>
|
|
792
1068
|
<button
|
|
793
1069
|
className="runtime-btn runtime-btn-secondary"
|
|
794
|
-
onClick={
|
|
1070
|
+
onClick={() =>
|
|
1071
|
+
handleDeletePod(discoveredPod.id, discoveredPod.name)
|
|
1072
|
+
}
|
|
1073
|
+
disabled={deletingPodId === discoveredPod.id}
|
|
795
1074
|
style={{
|
|
796
|
-
fontSize: "
|
|
1075
|
+
fontSize: "10px",
|
|
797
1076
|
padding: "6px 12px",
|
|
798
|
-
backgroundColor: "
|
|
799
|
-
color: "
|
|
1077
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
1078
|
+
color: "white",
|
|
800
1079
|
border: "none",
|
|
801
1080
|
borderRadius: "8px",
|
|
802
|
-
cursor: "pointer"
|
|
1081
|
+
cursor: "pointer",
|
|
803
1082
|
}}
|
|
804
1083
|
>
|
|
805
|
-
|
|
806
|
-
Get Key
|
|
1084
|
+
{deletingPodId === discoveredPod.id ? "..." : "Terminate"}
|
|
807
1085
|
</button>
|
|
808
1086
|
</div>
|
|
809
1087
|
</div>
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
<div style={{ padding: "8px 0" }}>
|
|
820
|
-
<div style={{
|
|
821
|
-
backgroundColor: "rgba(251, 191, 36, 0.1)",
|
|
822
|
-
border: "1px solid rgba(251, 191, 36, 0.3)",
|
|
823
|
-
borderRadius: "8px",
|
|
824
|
-
padding: "12px",
|
|
825
|
-
marginBottom: "8px"
|
|
826
|
-
}}>
|
|
827
|
-
<div style={{ fontSize: "11px", fontWeight: 600, color: "var(--mc-text-color)", marginBottom: "4px" }}>
|
|
828
|
-
⚠️ Running Pod Detected
|
|
829
|
-
</div>
|
|
830
|
-
<div style={{ fontSize: "10px", color: "var(--text-secondary)", marginBottom: "8px" }}>
|
|
831
|
-
Found a running pod but not connected (backend may have restarted). This pod is still costing money!
|
|
832
|
-
</div>
|
|
833
|
-
<div style={{ fontSize: "10px", marginBottom: "8px", color: "var(--mc-text-color)" }}>
|
|
834
|
-
<span style={{ fontWeight: 500 }}>{discoveredPod.name}</span> • {discoveredPod.gpuType} • ${discoveredPod.costPerHour.toFixed(2)}/hour
|
|
835
|
-
</div>
|
|
836
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
837
|
-
<button
|
|
838
|
-
className="runtime-btn runtime-btn-secondary"
|
|
839
|
-
onClick={() => handleConnectToPod(discoveredPod.id)}
|
|
840
|
-
style={{
|
|
841
|
-
flex: 1,
|
|
842
|
-
fontSize: "10px",
|
|
843
|
-
padding: "6px 12px",
|
|
844
|
-
backgroundColor: "rgba(251, 191, 36, 0.8)",
|
|
845
|
-
color: "var(--mc-text-color)",
|
|
846
|
-
border: "none",
|
|
847
|
-
borderRadius: "8px",
|
|
848
|
-
cursor: "pointer",
|
|
849
|
-
fontWeight: 600
|
|
850
|
-
}}
|
|
851
|
-
>
|
|
852
|
-
Reconnect
|
|
853
|
-
</button>
|
|
854
|
-
<button
|
|
855
|
-
className="runtime-btn runtime-btn-secondary"
|
|
856
|
-
onClick={() => handleDeletePod(discoveredPod.id, discoveredPod.name)}
|
|
857
|
-
disabled={deletingPodId === discoveredPod.id}
|
|
1088
|
+
</div>
|
|
1089
|
+
) : (
|
|
1090
|
+
gpuPods
|
|
1091
|
+
.filter((pod) => pod.status === "running")
|
|
1092
|
+
.map((pod) => {
|
|
1093
|
+
const isConnected = pod.id === connectedPodId;
|
|
1094
|
+
return (
|
|
1095
|
+
<div key={pod.id} style={{ padding: "8px 0" }}>
|
|
1096
|
+
<div
|
|
858
1097
|
style={{
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
border: "none",
|
|
864
|
-
borderRadius: "8px",
|
|
865
|
-
cursor: "pointer"
|
|
1098
|
+
display: "flex",
|
|
1099
|
+
justifyContent: "space-between",
|
|
1100
|
+
alignItems: "flex-start",
|
|
1101
|
+
marginBottom: "8px",
|
|
866
1102
|
}}
|
|
867
1103
|
>
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
gpuPods
|
|
875
|
-
.filter((pod) => pod.status === "running")
|
|
876
|
-
.map((pod) => {
|
|
877
|
-
const isConnected = pod.id === connectedPodId;
|
|
878
|
-
return (
|
|
879
|
-
<div key={pod.id} style={{ padding: "8px 0" }}>
|
|
880
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "8px" }}>
|
|
881
|
-
<div>
|
|
882
|
-
<div style={{ fontSize: "11px", marginBottom: "4px" }}>
|
|
883
|
-
<span style={{ fontWeight: 500 }}>{pod.name}</span>
|
|
884
|
-
{isConnected && (
|
|
885
|
-
<span style={{
|
|
1104
|
+
<div>
|
|
1105
|
+
<div style={{ fontSize: "11px", marginBottom: "4px" }}>
|
|
1106
|
+
<span style={{ fontWeight: 500 }}>{pod.name}</span>
|
|
1107
|
+
{isConnected && (
|
|
1108
|
+
<span
|
|
1109
|
+
style={{
|
|
886
1110
|
marginLeft: "6px",
|
|
887
1111
|
fontSize: "9px",
|
|
888
1112
|
backgroundColor: "rgba(16, 185, 129, 0.9)",
|
|
889
1113
|
color: "white",
|
|
890
1114
|
padding: "2px 6px",
|
|
891
|
-
borderRadius: "4px"
|
|
892
|
-
}}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
</div>
|
|
897
|
-
<div style={{ fontSize: "10px", color: "var(--text-secondary)" }}>
|
|
898
|
-
{pod.gpuType} • ${pod.costPerHour.toFixed(2)}/hour
|
|
899
|
-
</div>
|
|
900
|
-
</div>
|
|
901
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
902
|
-
{isConnected ? (
|
|
903
|
-
<>
|
|
904
|
-
<button
|
|
905
|
-
className="runtime-btn runtime-btn-secondary"
|
|
906
|
-
onClick={handleDisconnect}
|
|
907
|
-
style={{
|
|
908
|
-
fontSize: "10px",
|
|
909
|
-
padding: "6px 12px",
|
|
910
|
-
backgroundColor: "var(--mc-text-color)",
|
|
911
|
-
color: "var(--mc-cell-background)",
|
|
912
|
-
border: "none",
|
|
913
|
-
borderRadius: "8px",
|
|
914
|
-
cursor: "pointer"
|
|
915
|
-
}}
|
|
916
|
-
>
|
|
917
|
-
Disconnect
|
|
918
|
-
</button>
|
|
919
|
-
<button
|
|
920
|
-
className="runtime-btn runtime-btn-secondary"
|
|
921
|
-
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
922
|
-
disabled={deletingPodId === pod.id}
|
|
923
|
-
style={{
|
|
924
|
-
fontSize: "10px",
|
|
925
|
-
padding: "6px 12px",
|
|
926
|
-
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
927
|
-
color: "white",
|
|
928
|
-
border: "none",
|
|
929
|
-
borderRadius: "8px",
|
|
930
|
-
cursor: "pointer"
|
|
931
|
-
}}
|
|
932
|
-
>
|
|
933
|
-
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
934
|
-
</button>
|
|
935
|
-
</>
|
|
936
|
-
) : (
|
|
937
|
-
<>
|
|
938
|
-
<button
|
|
939
|
-
className="runtime-btn runtime-btn-secondary"
|
|
940
|
-
onClick={() => handleConnectToPod(pod.id)}
|
|
941
|
-
disabled={connectingPodId === pod.id}
|
|
942
|
-
style={{
|
|
943
|
-
fontSize: "10px",
|
|
944
|
-
padding: "6px 12px",
|
|
945
|
-
backgroundColor: "rgba(16, 185, 129, 0.9)",
|
|
946
|
-
color: "white",
|
|
947
|
-
border: "none",
|
|
948
|
-
borderRadius: "8px",
|
|
949
|
-
cursor: "pointer"
|
|
950
|
-
}}
|
|
951
|
-
>
|
|
952
|
-
{connectingPodId === pod.id ? "Connecting..." : "Connect"}
|
|
953
|
-
</button>
|
|
954
|
-
<button
|
|
955
|
-
className="runtime-btn runtime-btn-secondary"
|
|
956
|
-
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
957
|
-
disabled={deletingPodId === pod.id}
|
|
958
|
-
style={{
|
|
959
|
-
fontSize: "10px",
|
|
960
|
-
padding: "6px 12px",
|
|
961
|
-
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
962
|
-
color: "white",
|
|
963
|
-
border: "none",
|
|
964
|
-
borderRadius: "8px",
|
|
965
|
-
cursor: "pointer"
|
|
966
|
-
}}
|
|
967
|
-
>
|
|
968
|
-
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
969
|
-
</button>
|
|
970
|
-
</>
|
|
1115
|
+
borderRadius: "4px",
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
Connected
|
|
1119
|
+
</span>
|
|
971
1120
|
)}
|
|
972
1121
|
</div>
|
|
1122
|
+
<div
|
|
1123
|
+
style={{
|
|
1124
|
+
fontSize: "10px",
|
|
1125
|
+
color: "var(--text-secondary)",
|
|
1126
|
+
}}
|
|
1127
|
+
>
|
|
1128
|
+
{pod.gpuType} • ${pod.costPerHour.toFixed(2)}/hour
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
<div style={{ display: "flex", gap: "6px" }}>
|
|
1132
|
+
{isConnected ? (
|
|
1133
|
+
<>
|
|
1134
|
+
<button
|
|
1135
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1136
|
+
onClick={handleDisconnect}
|
|
1137
|
+
style={{
|
|
1138
|
+
fontSize: "10px",
|
|
1139
|
+
padding: "6px 12px",
|
|
1140
|
+
backgroundColor: "var(--mc-text-color)",
|
|
1141
|
+
color: "var(--mc-cell-background)",
|
|
1142
|
+
border: "none",
|
|
1143
|
+
borderRadius: "8px",
|
|
1144
|
+
cursor: "pointer",
|
|
1145
|
+
}}
|
|
1146
|
+
>
|
|
1147
|
+
Disconnect
|
|
1148
|
+
</button>
|
|
1149
|
+
<button
|
|
1150
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1151
|
+
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
1152
|
+
disabled={deletingPodId === pod.id}
|
|
1153
|
+
style={{
|
|
1154
|
+
fontSize: "10px",
|
|
1155
|
+
padding: "6px 12px",
|
|
1156
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
1157
|
+
color: "white",
|
|
1158
|
+
border: "none",
|
|
1159
|
+
borderRadius: "8px",
|
|
1160
|
+
cursor: "pointer",
|
|
1161
|
+
}}
|
|
1162
|
+
>
|
|
1163
|
+
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
1164
|
+
</button>
|
|
1165
|
+
</>
|
|
1166
|
+
) : (
|
|
1167
|
+
<>
|
|
1168
|
+
<button
|
|
1169
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1170
|
+
onClick={() => handleConnectToPod(pod.id)}
|
|
1171
|
+
disabled={connectingPodId === pod.id}
|
|
1172
|
+
style={{
|
|
1173
|
+
fontSize: "10px",
|
|
1174
|
+
padding: "6px 12px",
|
|
1175
|
+
backgroundColor: "rgba(16, 185, 129, 0.9)",
|
|
1176
|
+
color: "white",
|
|
1177
|
+
border: "none",
|
|
1178
|
+
borderRadius: "8px",
|
|
1179
|
+
cursor: "pointer",
|
|
1180
|
+
}}
|
|
1181
|
+
>
|
|
1182
|
+
{connectingPodId === pod.id
|
|
1183
|
+
? "Connecting..."
|
|
1184
|
+
: "Connect"}
|
|
1185
|
+
</button>
|
|
1186
|
+
<button
|
|
1187
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1188
|
+
onClick={() => handleDeletePod(pod.id, pod.name)}
|
|
1189
|
+
disabled={deletingPodId === pod.id}
|
|
1190
|
+
style={{
|
|
1191
|
+
fontSize: "10px",
|
|
1192
|
+
padding: "6px 12px",
|
|
1193
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)",
|
|
1194
|
+
color: "white",
|
|
1195
|
+
border: "none",
|
|
1196
|
+
borderRadius: "8px",
|
|
1197
|
+
cursor: "pointer",
|
|
1198
|
+
}}
|
|
1199
|
+
>
|
|
1200
|
+
{deletingPodId === pod.id ? "..." : "Terminate"}
|
|
1201
|
+
</button>
|
|
1202
|
+
</>
|
|
1203
|
+
)}
|
|
973
1204
|
</div>
|
|
974
1205
|
</div>
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1206
|
+
</div>
|
|
1207
|
+
);
|
|
1208
|
+
})
|
|
1209
|
+
)}
|
|
1210
|
+
</section>
|
|
979
1211
|
|
|
980
1212
|
{/* Divider */}
|
|
981
|
-
<div
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1213
|
+
<div
|
|
1214
|
+
style={{
|
|
1215
|
+
height: "1px",
|
|
1216
|
+
backgroundColor: "rgba(128, 128, 128, 0.2)",
|
|
1217
|
+
margin: "0 16px",
|
|
1218
|
+
}}
|
|
1219
|
+
/>
|
|
1220
|
+
|
|
1221
|
+
{/* Browse Available GPUs Section */}
|
|
1222
|
+
{apiConfigured && (
|
|
1223
|
+
<section className="runtime-section" style={{ padding: "12px 16px" }}>
|
|
1224
|
+
{/* Search and Filter Bar */}
|
|
1225
|
+
<div
|
|
1226
|
+
style={{
|
|
1227
|
+
marginBottom: "20px",
|
|
1228
|
+
display: "flex",
|
|
1229
|
+
gap: "8px",
|
|
1230
|
+
alignItems: "center",
|
|
1231
|
+
}}
|
|
1232
|
+
>
|
|
1233
|
+
<input
|
|
1234
|
+
type="text"
|
|
1235
|
+
placeholder="Search GPU (e.g., H100, A100, RTX 4090)"
|
|
1236
|
+
value={searchQuery}
|
|
1237
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1238
|
+
style={{
|
|
1239
|
+
flex: 1,
|
|
1240
|
+
padding: "6px 12px",
|
|
1241
|
+
fontSize: "11px",
|
|
1242
|
+
border: "1px solid var(--mc-border)",
|
|
1243
|
+
borderRadius: "8px",
|
|
1244
|
+
backgroundColor: "var(--mc-background)",
|
|
1245
|
+
color: "var(--mc-text-color)",
|
|
1246
|
+
outline: "none",
|
|
1247
|
+
}}
|
|
1248
|
+
/>
|
|
1249
|
+
<button
|
|
1250
|
+
className="runtime-btn runtime-btn-secondary"
|
|
1251
|
+
onClick={() => setShowFilterPopup(!showFilterPopup)}
|
|
1252
|
+
title="Filter GPUs"
|
|
992
1253
|
style={{
|
|
993
|
-
|
|
1254
|
+
padding: "6px 8px",
|
|
1255
|
+
fontSize: "11px",
|
|
1256
|
+
position: "relative",
|
|
1257
|
+
backgroundColor: "var(--mc-text-color)",
|
|
1258
|
+
color: "var(--mc-cell-background)",
|
|
1259
|
+
border: "none",
|
|
1260
|
+
borderRadius: "8px",
|
|
1261
|
+
cursor: "pointer",
|
|
994
1262
|
display: "flex",
|
|
995
|
-
gap: "8px",
|
|
996
1263
|
alignItems: "center",
|
|
1264
|
+
justifyContent: "center",
|
|
997
1265
|
}}
|
|
998
1266
|
>
|
|
999
|
-
<
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
fontSize: "11px",
|
|
1021
|
-
position: "relative",
|
|
1022
|
-
backgroundColor: "var(--mc-text-color)",
|
|
1023
|
-
color: "var(--mc-cell-background)",
|
|
1024
|
-
border: "none",
|
|
1025
|
-
borderRadius: "8px",
|
|
1026
|
-
cursor: "pointer"
|
|
1027
|
-
}}
|
|
1028
|
-
>
|
|
1029
|
-
<Filter size={10} style={{ marginRight: "3px" }} />
|
|
1030
|
-
Filter
|
|
1031
|
-
{(filters.gpu_type ||
|
|
1032
|
-
filters.gpu_count ||
|
|
1033
|
-
filters.security ||
|
|
1034
|
-
filters.socket) && (
|
|
1035
|
-
<span
|
|
1036
|
-
style={{
|
|
1037
|
-
position: "absolute",
|
|
1038
|
-
top: "-2px",
|
|
1039
|
-
right: "-2px",
|
|
1040
|
-
width: "8px",
|
|
1041
|
-
height: "8px",
|
|
1042
|
-
borderRadius: "50%",
|
|
1043
|
-
backgroundColor: "var(--accent)",
|
|
1044
|
-
}}
|
|
1045
|
-
/>
|
|
1046
|
-
)}
|
|
1047
|
-
</button>
|
|
1048
|
-
</div>
|
|
1049
|
-
|
|
1050
|
-
{/* Filter Popup */}
|
|
1051
|
-
<FilterPopup
|
|
1052
|
-
isOpen={showFilterPopup}
|
|
1053
|
-
onClose={() => setShowFilterPopup(false)}
|
|
1054
|
-
filters={filters}
|
|
1055
|
-
onFiltersChange={setFilters}
|
|
1056
|
-
onApply={loadAvailableGPUs}
|
|
1057
|
-
/>
|
|
1267
|
+
<Filter size={14} />
|
|
1268
|
+
{(filters.gpu_type ||
|
|
1269
|
+
filters.gpu_count ||
|
|
1270
|
+
filters.secure_cloud !== undefined ||
|
|
1271
|
+
filters.community_cloud !== undefined ||
|
|
1272
|
+
filters.verified !== undefined ||
|
|
1273
|
+
filters.min_reliability !== undefined) && (
|
|
1274
|
+
<span
|
|
1275
|
+
style={{
|
|
1276
|
+
position: "absolute",
|
|
1277
|
+
top: "-2px",
|
|
1278
|
+
right: "-2px",
|
|
1279
|
+
width: "8px",
|
|
1280
|
+
height: "8px",
|
|
1281
|
+
borderRadius: "50%",
|
|
1282
|
+
backgroundColor: "var(--accent)",
|
|
1283
|
+
}}
|
|
1284
|
+
/>
|
|
1285
|
+
)}
|
|
1286
|
+
</button>
|
|
1287
|
+
</div>
|
|
1058
1288
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1289
|
+
{/* Filter Popup */}
|
|
1290
|
+
<FilterPopup
|
|
1291
|
+
isOpen={showFilterPopup}
|
|
1292
|
+
onClose={() => setShowFilterPopup(false)}
|
|
1293
|
+
filters={filters}
|
|
1294
|
+
onFiltersChange={setFilters}
|
|
1295
|
+
onApply={loadAvailableGPUs}
|
|
1296
|
+
providerName={selectedProvider?.name}
|
|
1297
|
+
/>
|
|
1298
|
+
|
|
1299
|
+
{/* Results */}
|
|
1300
|
+
{loadingAvailability ? (
|
|
1301
|
+
<div
|
|
1302
|
+
style={{
|
|
1303
|
+
padding: "16px 0",
|
|
1304
|
+
textAlign: "center",
|
|
1305
|
+
color: "var(--text-secondary)",
|
|
1306
|
+
fontSize: "11px",
|
|
1307
|
+
}}
|
|
1308
|
+
>
|
|
1309
|
+
Loading...
|
|
1310
|
+
</div>
|
|
1311
|
+
) : gpusLoadError ? (
|
|
1312
|
+
<div
|
|
1313
|
+
style={{
|
|
1314
|
+
padding: "12px",
|
|
1315
|
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
1316
|
+
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
1317
|
+
borderRadius: "6px",
|
|
1318
|
+
color: "#ef4444",
|
|
1319
|
+
fontSize: "11px",
|
|
1320
|
+
textAlign: "center",
|
|
1321
|
+
}}
|
|
1322
|
+
>
|
|
1323
|
+
{gpusLoadError}
|
|
1324
|
+
</div>
|
|
1325
|
+
) : availableGPUs.length === 0 ? (
|
|
1326
|
+
<div
|
|
1327
|
+
style={{
|
|
1328
|
+
padding: "16px 0",
|
|
1329
|
+
textAlign: "center",
|
|
1330
|
+
color: "var(--text-secondary)",
|
|
1331
|
+
fontSize: "11px",
|
|
1332
|
+
}}
|
|
1333
|
+
>
|
|
1334
|
+
Use filters to find available GPUs
|
|
1335
|
+
</div>
|
|
1336
|
+
) : filteredGPUs.length === 0 ? (
|
|
1337
|
+
<div
|
|
1338
|
+
style={{
|
|
1339
|
+
padding: "16px 0",
|
|
1340
|
+
textAlign: "center",
|
|
1341
|
+
color: "var(--text-secondary)",
|
|
1342
|
+
fontSize: "11px",
|
|
1343
|
+
}}
|
|
1344
|
+
>
|
|
1345
|
+
No GPUs match "{searchQuery}"
|
|
1346
|
+
</div>
|
|
1347
|
+
) : (
|
|
1348
|
+
<div
|
|
1349
|
+
style={{
|
|
1350
|
+
maxHeight: "calc(100vh - 400px)",
|
|
1351
|
+
overflowY: "auto",
|
|
1352
|
+
paddingRight: "12px",
|
|
1353
|
+
}}
|
|
1354
|
+
>
|
|
1355
|
+
{filteredGPUs.map((gpu, index) => (
|
|
1356
|
+
<React.Fragment key={`${gpu.cloudId}-${index}`}>
|
|
1357
|
+
{index > 0 && (
|
|
1083
1358
|
<div
|
|
1084
1359
|
style={{
|
|
1085
|
-
|
|
1360
|
+
height: "1px",
|
|
1361
|
+
backgroundColor: "rgba(128, 128, 128, 0.15)",
|
|
1362
|
+
margin: "8px 0",
|
|
1086
1363
|
}}
|
|
1087
|
-
|
|
1364
|
+
/>
|
|
1365
|
+
)}
|
|
1366
|
+
<div
|
|
1367
|
+
style={{
|
|
1368
|
+
padding: "8px 0",
|
|
1369
|
+
}}
|
|
1370
|
+
>
|
|
1088
1371
|
<div
|
|
1089
1372
|
style={{
|
|
1090
1373
|
display: "flex",
|
|
@@ -1101,7 +1384,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1101
1384
|
marginBottom: "1px",
|
|
1102
1385
|
}}
|
|
1103
1386
|
>
|
|
1104
|
-
{gpu.gpuType} ({gpu.gpuCount}x)
|
|
1387
|
+
{gpu.gpuName || gpu.gpuType} ({gpu.gpuCount}x)
|
|
1105
1388
|
</div>
|
|
1106
1389
|
<div
|
|
1107
1390
|
style={{
|
|
@@ -1109,10 +1392,18 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1109
1392
|
color: "var(--text-secondary)",
|
|
1110
1393
|
}}
|
|
1111
1394
|
>
|
|
1112
|
-
{gpu.provider}
|
|
1395
|
+
{gpu.provider} • {gpu.memoryGb || gpu.gpuMemory}GB{gpu.regionDescription ? ` • ${gpu.regionDescription}` : ""}
|
|
1113
1396
|
</div>
|
|
1114
1397
|
</div>
|
|
1115
|
-
<div
|
|
1398
|
+
<div
|
|
1399
|
+
style={{
|
|
1400
|
+
textAlign: "right",
|
|
1401
|
+
display: "flex",
|
|
1402
|
+
flexDirection: "column",
|
|
1403
|
+
alignItems: "flex-end",
|
|
1404
|
+
gap: "6px",
|
|
1405
|
+
}}
|
|
1406
|
+
>
|
|
1116
1407
|
<div>
|
|
1117
1408
|
<div
|
|
1118
1409
|
style={{
|
|
@@ -1121,7 +1412,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1121
1412
|
color: "var(--accent)",
|
|
1122
1413
|
}}
|
|
1123
1414
|
>
|
|
1124
|
-
${gpu.prices?.onDemand?.toFixed(2) || "N/A"}/hr
|
|
1415
|
+
${(gpu.priceHr ?? gpu.prices?.onDemand)?.toFixed(2) || "N/A"}/hr
|
|
1125
1416
|
</div>
|
|
1126
1417
|
{gpu.stockStatus && (
|
|
1127
1418
|
<div
|
|
@@ -1153,7 +1444,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1153
1444
|
color: "var(--mc-cell-background)",
|
|
1154
1445
|
border: "none",
|
|
1155
1446
|
borderRadius: "8px",
|
|
1156
|
-
cursor: "pointer"
|
|
1447
|
+
cursor: "pointer",
|
|
1157
1448
|
}}
|
|
1158
1449
|
>
|
|
1159
1450
|
{creatingPodId === gpu.cloudId
|
|
@@ -1204,12 +1495,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
|
|
|
1204
1495
|
)}
|
|
1205
1496
|
</div>
|
|
1206
1497
|
</div>
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1498
|
+
</React.Fragment>
|
|
1499
|
+
))}
|
|
1500
|
+
</div>
|
|
1501
|
+
)}
|
|
1502
|
+
</section>
|
|
1503
|
+
)}
|
|
1213
1504
|
</div>
|
|
1214
1505
|
</>
|
|
1215
1506
|
);
|