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.
Files changed (57) hide show
  1. frontend/app/globals.css +734 -27
  2. frontend/app/layout.tsx +13 -3
  3. frontend/components/Notebook.tsx +2 -14
  4. frontend/components/cell/MonacoCell.tsx +99 -5
  5. frontend/components/layout/Sidebar.tsx +39 -4
  6. frontend/components/panels/ClaudePanel.tsx +461 -0
  7. frontend/components/popups/ComputePopup.tsx +738 -447
  8. frontend/components/popups/FilterPopup.tsx +305 -189
  9. frontend/components/popups/MetricsPopup.tsx +20 -1
  10. frontend/components/popups/ProviderConfigModal.tsx +322 -0
  11. frontend/components/popups/ProviderDropdown.tsx +398 -0
  12. frontend/components/popups/SettingsPopup.tsx +1 -1
  13. frontend/contexts/ClaudeContext.tsx +392 -0
  14. frontend/contexts/PodWebSocketContext.tsx +16 -21
  15. frontend/hooks/useInlineDiff.ts +269 -0
  16. frontend/lib/api.ts +323 -12
  17. frontend/lib/settings.ts +5 -0
  18. frontend/lib/websocket-native.ts +4 -8
  19. frontend/lib/websocket.ts +1 -2
  20. frontend/package-lock.json +733 -36
  21. frontend/package.json +2 -0
  22. frontend/public/assets/icons/providers/lambda_labs.svg +22 -0
  23. frontend/public/assets/icons/providers/prime_intellect.svg +18 -0
  24. frontend/public/assets/icons/providers/runpod.svg +9 -0
  25. frontend/public/assets/icons/providers/vastai.svg +1 -0
  26. frontend/settings.md +54 -0
  27. frontend/tsconfig.tsbuildinfo +1 -0
  28. frontend/types/claude.ts +194 -0
  29. kernel_run.py +13 -0
  30. {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
  31. {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
  32. {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/WHEEL +1 -1
  33. morecompute/__init__.py +1 -1
  34. morecompute/__version__.py +1 -1
  35. morecompute/execution/executor.py +24 -67
  36. morecompute/execution/worker.py +6 -72
  37. morecompute/models/api_models.py +62 -0
  38. morecompute/notebook.py +11 -0
  39. morecompute/server.py +641 -133
  40. morecompute/services/claude_service.py +392 -0
  41. morecompute/services/pod_manager.py +168 -67
  42. morecompute/services/pod_monitor.py +67 -39
  43. morecompute/services/prime_intellect.py +0 -4
  44. morecompute/services/providers/__init__.py +92 -0
  45. morecompute/services/providers/base_provider.py +336 -0
  46. morecompute/services/providers/lambda_labs_provider.py +394 -0
  47. morecompute/services/providers/provider_factory.py +194 -0
  48. morecompute/services/providers/runpod_provider.py +504 -0
  49. morecompute/services/providers/vastai_provider.py +407 -0
  50. morecompute/utils/cell_magics.py +0 -3
  51. morecompute/utils/config_util.py +93 -3
  52. morecompute/utils/special_commands.py +5 -32
  53. morecompute/utils/version_check.py +117 -0
  54. frontend/styling_README.md +0 -23
  55. {more_compute-0.4.4.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
  56. {more_compute-0.4.4.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
  57. {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 React, { useState, useEffect, useMemo } from "react";
2
- import {
3
- Zap,
4
- ExternalLink,
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
- fetchGpuPods,
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 ErrorModal from "@/components/modals/ErrorModal";
28
- import SuccessModal from "@/components/modals/SuccessModal";
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 { usePodWebSocket } from "@/contexts/PodWebSocketContext";
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<"healthy" | "unhealthy" | "unknown">("unknown");
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 fetchGpuPods(params || { limit: 100 });
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
- if (pod.status === "ACTIVE" && pod.sshConnection) {
222
+ const isFullyReady = pod.status === "ACTIVE" && pod.sshConnection;
223
+
224
+ if (isFullyReady) {
204
225
  uiStatus = "running";
205
- } else if (pod.status === "ACTIVE" || pod.status === "PROVISIONING" || pod.status === "PENDING") {
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 (err instanceof Error && err.message.includes("401")) {
224
- setApiConfigured(false);
225
- setShowApiKeyInput(true);
226
- setErrorModal({
227
- isOpen: true,
228
- title: "Invalid API Key",
229
- message: "Your API key is invalid or has expired. Please enter a new API key to continue.",
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 fetchGpuAvailability(filters);
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 createGpuPod(podRequest);
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 = "Authentication failed. Check your API key configuration.";
319
- } else if (errorMsg.includes("503") || errorMsg.includes("not available")) {
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: "https://app.primeintellect.ai/dashboard/billing",
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 (connectingPodId === podId || connectedPodId === podId) {
354
- console.log(`[CONNECT] Already connecting/connected to pod ${podId}, skipping`);
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 connectToPod(podId);
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
- const status: PodConnectionStatus = await getPodConnectionStatus();
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: "Connected!",
395
- message: "Successfully connected to GPU pod. You can now run code on the remote GPU.",
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: "Connection took too long. The pod may not be ready yet. Check the pod status and try again.",
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: "Successfully connected to GPU pod. You can now run code on the remote GPU.",
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 (errorMsg.includes("SSH authentication") || errorMsg.includes("SSH public key")) {
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 = err instanceof Error ? err.message : "Failed to connect to pod";
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: "Disconnected from pod. Note: The pod is still running and incurring costs. You can reconnect or terminate it below.",
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
- await deleteGpuPod(podId);
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 handleConnectToPrimeIntellect = () => {
560
- window.open("https://app.primeintellect.ai/dashboard/tokens", "_blank");
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 style={{ fontSize: "11px", color: "var(--text-secondary)", marginBottom: "4px" }}>
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 style={{
650
- height: "1px",
651
- backgroundColor: "rgba(128, 128, 128, 0.2)",
652
- margin: "0 16px"
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 className="runtime-section-title" style={{ fontSize: "12px", fontWeight: 500 }}>
829
+ <h3
830
+ className="runtime-section-title"
831
+ style={{ fontSize: "12px", fontWeight: 500 }}
832
+ >
665
833
  Compute Profile
666
834
  </h3>
667
- <span className="runtime-cost" style={{ fontSize: "12px", fontWeight: 500 }}>
835
+ <span
836
+ className="runtime-cost"
837
+ style={{ fontSize: "12px", fontWeight: 500 }}
838
+ >
668
839
  {(() => {
669
- const runningPods = gpuPods.filter(p => p.status === "running");
670
- const totalCost = runningPods.reduce((sum, pod) => sum + pod.costPerHour, 0);
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 style={{
679
- height: "1px",
680
- backgroundColor: "rgba(128, 128, 128, 0.2)",
681
- margin: "0 16px"
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
- {/* GPU Pods Section */}
685
- <section className="runtime-section" style={{ padding: "12px 16px" }}>
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
- display: "flex",
689
- justifyContent: "space-between",
690
- alignItems: "center",
691
- marginBottom: "12px"
977
+ padding: "8px 0",
978
+ color: "var(--text-secondary)",
979
+ fontSize: "11px",
692
980
  }}
693
981
  >
694
- <h3 className="runtime-section-title" style={{ fontSize: "12px", fontWeight: 500 }}>
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
- {(apiConfigured === false || showApiKeyInput) ? (
734
- <div className="runtime-empty-state" style={{ padding: "6px" }}>
735
- <p
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
- Enter API key to enable GPU pods
743
- </p>
744
- <div style={{ marginBottom: "4px", width: "100%" }}>
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: "4px", width: "100%" }}>
1050
+ <div style={{ display: "flex", gap: "6px" }}>
775
1051
  <button
776
- className="runtime-btn runtime-btn-primary"
777
- onClick={handleSaveApiKey}
778
- disabled={saving}
1052
+ className="runtime-btn runtime-btn-secondary"
1053
+ onClick={() => handleConnectToPod(discoveredPod.id)}
779
1054
  style={{
780
1055
  flex: 1,
781
- fontSize: "11px",
1056
+ fontSize: "10px",
782
1057
  padding: "6px 12px",
783
- backgroundColor: "var(--mc-text-color)",
784
- color: "var(--mc-cell-background)",
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
- {saving ? "Saving..." : "Save"}
1066
+ Reconnect
791
1067
  </button>
792
1068
  <button
793
1069
  className="runtime-btn runtime-btn-secondary"
794
- onClick={handleConnectToPrimeIntellect}
1070
+ onClick={() =>
1071
+ handleDeletePod(discoveredPod.id, discoveredPod.name)
1072
+ }
1073
+ disabled={deletingPodId === discoveredPod.id}
795
1074
  style={{
796
- fontSize: "11px",
1075
+ fontSize: "10px",
797
1076
  padding: "6px 12px",
798
- backgroundColor: "var(--mc-text-color)",
799
- color: "var(--mc-cell-background)",
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
- <ExternalLink size={10} style={{ marginRight: "3px" }} />
806
- Get Key
1084
+ {deletingPodId === discoveredPod.id ? "..." : "Terminate"}
807
1085
  </button>
808
1086
  </div>
809
1087
  </div>
810
- ) : loading || apiConfigured === null ? (
811
- <div style={{ padding: "8px 0", color: "var(--text-secondary)", fontSize: "11px" }}>
812
- Loading...
813
- </div>
814
- ) : gpuPods.filter(p => p.status === "running").length === 0 && !discoveredPod ? (
815
- <div style={{ padding: "8px 0", color: "var(--text-secondary)", fontSize: "11px" }}>
816
- Currently not connected to any.
817
- </div>
818
- ) : !connectedPodId && discoveredPod ? (
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
- fontSize: "10px",
860
- padding: "6px 12px",
861
- backgroundColor: "rgba(220, 38, 38, 0.9)",
862
- color: "white",
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
- {deletingPodId === discoveredPod.id ? "..." : "Terminate"}
869
- </button>
870
- </div>
871
- </div>
872
- </div>
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
- Connected
894
- </span>
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
- </section>
1206
+ </div>
1207
+ );
1208
+ })
1209
+ )}
1210
+ </section>
979
1211
 
980
1212
  {/* Divider */}
981
- <div style={{
982
- height: "1px",
983
- backgroundColor: "rgba(128, 128, 128, 0.2)",
984
- margin: "0 16px"
985
- }} />
986
-
987
- {/* Browse Available GPUs Section */}
988
- {apiConfigured && (
989
- <section className="runtime-section" style={{ padding: "12px 16px" }}>
990
- {/* Search and Filter Bar */}
991
- <div
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
- marginBottom: "20px",
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
- <input
1000
- type="text"
1001
- placeholder="Search GPU (e.g., H100, A100, RTX 4090)"
1002
- value={searchQuery}
1003
- onChange={(e) => setSearchQuery(e.target.value)}
1004
- style={{
1005
- flex: 1,
1006
- padding: "6px 12px",
1007
- fontSize: "11px",
1008
- border: "1px solid var(--mc-border)",
1009
- borderRadius: "8px",
1010
- backgroundColor: "var(--mc-background)",
1011
- color: "var(--mc-text-color)",
1012
- outline: "none",
1013
- }}
1014
- />
1015
- <button
1016
- className="runtime-btn runtime-btn-secondary"
1017
- onClick={() => setShowFilterPopup(!showFilterPopup)}
1018
- style={{
1019
- padding: "6px 12px",
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
- {/* Results */}
1060
- {loadingAvailability ? (
1061
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1062
- Loading...
1063
- </div>
1064
- ) : availableGPUs.length === 0 ? (
1065
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1066
- Use filters to find available GPUs
1067
- </div>
1068
- ) : filteredGPUs.length === 0 ? (
1069
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1070
- No GPUs match "{searchQuery}"
1071
- </div>
1072
- ) : (
1073
- <div style={{ maxHeight: "calc(100vh - 400px)", overflowY: "auto", paddingRight: "12px" }}>
1074
- {filteredGPUs.map((gpu, index) => (
1075
- <React.Fragment key={`${gpu.cloudId}-${index}`}>
1076
- {index > 0 && (
1077
- <div style={{
1078
- height: "1px",
1079
- backgroundColor: "rgba(128, 128, 128, 0.15)",
1080
- margin: "8px 0"
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
- padding: "8px 0",
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} - {gpu.socket} - {gpu.gpuMemory}GB
1395
+ {gpu.provider} {gpu.memoryGb || gpu.gpuMemory}GB{gpu.regionDescription ? ` • ${gpu.regionDescription}` : ""}
1113
1396
  </div>
1114
1397
  </div>
1115
- <div style={{ textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "6px" }}>
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
- </React.Fragment>
1208
- ))}
1209
- </div>
1210
- )}
1211
- </section>
1212
- )}
1498
+ </React.Fragment>
1499
+ ))}
1500
+ </div>
1501
+ )}
1502
+ </section>
1503
+ )}
1213
1504
  </div>
1214
1505
  </>
1215
1506
  );