more-compute 0.4.3__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 +739 -418
  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.3.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
  31. {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
  32. {more_compute-0.4.3.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.3.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
  56. {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
  57. {more_compute-0.4.3.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,13 +68,25 @@ 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);
76
+ const [showApiKeyInput, setShowApiKeyInput] = useState(false);
77
77
 
78
78
  // Filter popup state
79
79
  const [showFilterPopup, setShowFilterPopup] = useState(false);
80
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
+
81
90
  // Error modal state
82
91
  const [errorModal, setErrorModal] = useState<{
83
92
  isOpen: boolean;
@@ -165,11 +174,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
165
174
  setConnectionHealth("healthy");
166
175
  } else if (!status.connected && status.pod) {
167
176
  // Discovered running pod but not connected (backend restart scenario)
168
- console.log("[COMPUTE POPUP] Discovered running pod after restart:", status.pod);
169
177
  // Only show discovered pod warning if it's not already in the pods list
170
178
  // (This can happen if pods haven't loaded yet)
171
179
  const pod = status.pod;
172
- const alreadyInList = gpuPods.some(p => p.id === pod.id);
180
+ const alreadyInList = gpuPods.some((p) => p.id === pod.id);
173
181
  if (!alreadyInList) {
174
182
  setDiscoveredPod({
175
183
  id: pod.id,
@@ -191,20 +199,45 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
191
199
  checkApiConfig();
192
200
  }, []);
193
201
 
194
- 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
+
195
213
  setLoading(true);
214
+ setPodsLoadError(null);
196
215
  try {
197
- const response = await fetchGpuPods(params || { limit: 100 });
216
+ const response = await fetchProviderPods(activeProvider.name, params || { limit: 100 });
217
+
198
218
  const pods = (response.data || []).map((pod: PodResponse) => {
199
219
  // Map API status to UI status
200
220
  // Pod must be ACTIVE *and* have SSH connection info to be "running"
201
221
  let uiStatus: "running" | "stopped" | "starting" = "stopped";
202
- if (pod.status === "ACTIVE" && pod.sshConnection) {
222
+ const isFullyReady = pod.status === "ACTIVE" && pod.sshConnection;
223
+
224
+ if (isFullyReady) {
203
225
  uiStatus = "running";
204
- } 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
+ ) {
205
231
  uiStatus = "starting";
206
232
  }
207
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
+
208
241
  return {
209
242
  id: pod.id,
210
243
  name: pod.name,
@@ -218,22 +251,54 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
218
251
  setPods(pods);
219
252
  } catch (err) {
220
253
  console.error("Failed to load GPU pods:", err);
254
+ const errorMsg = err instanceof Error ? err.message : "Failed to load pods";
255
+
256
+ // If 401 error, API key is invalid - allow user to reset it
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}`);
266
+ }
267
+ setPods([]);
221
268
  } finally {
222
269
  setLoading(false);
223
270
  }
224
271
  };
225
272
 
226
- 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
+
227
284
  setLoadingAvailability(true);
285
+ setGpusLoadError(null);
228
286
  try {
229
- const response = await fetchGpuAvailability(filters);
230
- const gpuList: GpuAvailability[] = [];
231
- Object.values(response).forEach((gpus) => {
232
- gpuList.push(...gpus);
233
- });
287
+ const response = await fetchProviderGpuAvailability(activeProvider.name, filters);
288
+ const gpuList: GpuAvailability[] = response.data || [];
234
289
  setAvailableGPUs(gpuList);
235
290
  } catch (err) {
236
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([]);
237
302
  } finally {
238
303
  setLoadingAvailability(false);
239
304
  }
@@ -244,6 +309,11 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
244
309
  setPodCreationError(null);
245
310
 
246
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
+
247
317
  // Generate a pod name based on GPU type and timestamp
248
318
  const timestamp = new Date()
249
319
  .toISOString()
@@ -271,7 +341,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
271
341
  },
272
342
  };
273
343
 
274
- const newPod = await createGpuPod(podRequest);
344
+ const newPod = await createProviderPod(selectedProvider.name, podRequest);
275
345
 
276
346
  // Register auto-connect callback for when pod becomes ready
277
347
  registerAutoConnect(newPod.id, handleConnectToPod);
@@ -292,6 +362,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
292
362
  });
293
363
  } catch (err) {
294
364
  let errorMsg = "Failed to create pod";
365
+ const providerName = selectedProvider?.display_name || "GPU Provider";
366
+ const providerDashboard = selectedProvider?.dashboard_url || "";
295
367
 
296
368
  if (err instanceof Error) {
297
369
  errorMsg = err.message;
@@ -301,11 +373,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
301
373
  errorMsg.includes("402") ||
302
374
  errorMsg.includes("Insufficient funds")
303
375
  ) {
304
- errorMsg =
305
- "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}` : ""}`;
306
377
  } else if (errorMsg.includes("401") || errorMsg.includes("403")) {
307
- errorMsg = "Authentication failed. Check your API key configuration.";
308
- } 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
+ ) {
309
383
  errorMsg =
310
384
  "This GPU type is currently unavailable. Please try selecting a different GPU from the list below.";
311
385
  } else if (errorMsg.includes("data_center_id")) {
@@ -317,13 +391,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
317
391
  setPodCreationError(errorMsg);
318
392
 
319
393
  // Show error in modal with link to billing if insufficient funds
320
- if (errorMsg.includes("Insufficient funds")) {
394
+ if (errorMsg.includes("Insufficient funds") && providerDashboard) {
321
395
  setErrorModal({
322
396
  isOpen: true,
323
397
  title: "Insufficient Funds",
324
398
  message: errorMsg,
325
399
  actionLabel: "Add Credits",
326
- actionUrl: "https://app.primeintellect.ai/dashboard/billing",
400
+ actionUrl: providerDashboard,
327
401
  });
328
402
  } else {
329
403
  setErrorModal({
@@ -338,9 +412,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
338
412
  };
339
413
 
340
414
  const handleConnectToPod = async (podId: string) => {
341
- // Prevent double-connecting
342
- if (connectingPodId === podId || connectedPodId === podId) {
343
- 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") {
344
421
  return;
345
422
  }
346
423
 
@@ -348,11 +425,15 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
348
425
  setConnectionState("deploying"); // Show "Deploying worker..." banner
349
426
  setConnectionHealth("unknown"); // Reset health status during connection
350
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
+
351
433
  // Initiate connection (now returns immediately with "connecting" status)
352
- const result = await connectToPod(podId);
434
+ const result = await connectToProviderPod(selectedProvider.name, podId);
353
435
 
354
436
  if (result.status === "connecting") {
355
- console.log("[CONNECT] Connection initiated, polling for completion...");
356
437
 
357
438
  // Poll connection status until it's connected or fails
358
439
  const maxAttempts = 30; // 30 seconds max
@@ -364,7 +445,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
364
445
  attempts++;
365
446
 
366
447
  try {
367
- 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
+ }
368
466
 
369
467
  if (status.connected && status.pod?.id === podId && !isComplete) {
370
468
  // Successfully connected!
@@ -380,8 +478,9 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
380
478
 
381
479
  setErrorModal({
382
480
  isOpen: true,
383
- title: "Connected!",
384
- 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.",
385
484
  });
386
485
 
387
486
  // Hide the connected banner after 3 seconds
@@ -398,7 +497,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
398
497
  setErrorModal({
399
498
  isOpen: true,
400
499
  title: "Connection Timeout",
401
- 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.",
402
502
  });
403
503
  }
404
504
  } catch (err) {
@@ -415,7 +515,6 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
415
515
  });
416
516
  }
417
517
  }, 1000); // Poll every second
418
-
419
518
  } else if (result.status === "ok") {
420
519
  // Immediate success (backwards compatible)
421
520
  setConnectedPodId(podId);
@@ -428,7 +527,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
428
527
  setErrorModal({
429
528
  isOpen: true,
430
529
  title: "✓ Connected!",
431
- 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.",
432
532
  });
433
533
 
434
534
  // Hide the connected banner after 3 seconds
@@ -443,7 +543,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
443
543
 
444
544
  let errorMsg = result.message || "Connection failed";
445
545
 
446
- if (errorMsg.includes("SSH authentication") || errorMsg.includes("SSH public key")) {
546
+ if (
547
+ errorMsg.includes("SSH authentication") ||
548
+ errorMsg.includes("SSH public key")
549
+ ) {
447
550
  setErrorModal({
448
551
  isOpen: true,
449
552
  title: "SSH Key Required",
@@ -460,7 +563,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
460
563
  }
461
564
  }
462
565
  } catch (err) {
463
- 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";
464
568
  setConnectionHealth("unhealthy");
465
569
  setConnectingPodId(null);
466
570
  setConnectionState(null); // Hide banner on error
@@ -485,7 +589,8 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
485
589
  setErrorModal({
486
590
  isOpen: true,
487
591
  title: "⚠️ Disconnected",
488
- 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.",
489
594
  });
490
595
  } catch (err) {
491
596
  const errorMsg =
@@ -517,7 +622,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
517
622
  setConnectionState(null);
518
623
  }
519
624
 
520
- 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);
521
631
 
522
632
  // Clear discovered pod if it was the one we just deleted
523
633
  if (discoveredPod && discoveredPod.id === podId) {
@@ -545,8 +655,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
545
655
  });
546
656
  };
547
657
 
548
- const handleConnectToPrimeIntellect = () => {
549
- 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);
550
691
  };
551
692
 
552
693
  const handleSaveApiKey = async () => {
@@ -561,8 +702,10 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
561
702
  try {
562
703
  await setGpuApiKey(apiKey);
563
704
  setApiConfigured(true);
705
+ setShowApiKeyInput(false);
564
706
  setApiKey("");
565
707
  await loadGPUPods();
708
+ await loadAvailableGPUs();
566
709
  } catch (err) {
567
710
  setSaveError(
568
711
  err instanceof Error ? err.message : "Failed to save API key",
@@ -611,12 +754,39 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
611
754
  cancelLabel="Cancel"
612
755
  isDangerous={true}
613
756
  />
757
+ {showProviderConfig && providerToConfig && (
758
+ <ProviderConfigModal
759
+ provider={providerToConfig}
760
+ onClose={() => {
761
+ setShowProviderConfig(false);
762
+ setProviderToConfig(null);
763
+ }}
764
+ onConfigured={handleProviderConfigured}
765
+ />
766
+ )}
614
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
+ />
615
788
  {/* Kernel Status Section */}
616
- <section
617
- className="runtime-section"
618
- style={{ padding: "16px 20px" }}
619
- >
789
+ <section className="runtime-section" style={{ padding: "16px 20px" }}>
620
790
  <div
621
791
  style={{
622
792
  display: "flex",
@@ -625,7 +795,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
625
795
  }}
626
796
  >
627
797
  <div>
628
- <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
+ >
629
805
  The kernel is currently: {kernelStatus ? "Running" : "Stopped"}
630
806
  </div>
631
807
  </div>
@@ -633,11 +809,13 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
633
809
  </section>
634
810
 
635
811
  {/* Divider */}
636
- <div style={{
637
- height: "1px",
638
- backgroundColor: "rgba(128, 128, 128, 0.2)",
639
- margin: "0 16px"
640
- }} />
812
+ <div
813
+ style={{
814
+ height: "1px",
815
+ backgroundColor: "rgba(128, 128, 128, 0.2)",
816
+ margin: "0 16px",
817
+ }}
818
+ />
641
819
 
642
820
  {/* Compute Profile Section */}
643
821
  <section className="runtime-section" style={{ padding: "12px 16px" }}>
@@ -648,13 +826,24 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
648
826
  alignItems: "center",
649
827
  }}
650
828
  >
651
- <h3 className="runtime-section-title" style={{ fontSize: "12px", fontWeight: 500 }}>
829
+ <h3
830
+ className="runtime-section-title"
831
+ style={{ fontSize: "12px", fontWeight: 500 }}
832
+ >
652
833
  Compute Profile
653
834
  </h3>
654
- <span className="runtime-cost" style={{ fontSize: "12px", fontWeight: 500 }}>
835
+ <span
836
+ className="runtime-cost"
837
+ style={{ fontSize: "12px", fontWeight: 500 }}
838
+ >
655
839
  {(() => {
656
- const runningPods = gpuPods.filter(p => p.status === "running");
657
- 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
+ );
658
847
  return `$${totalCost.toFixed(2)} / hour`;
659
848
  })()}
660
849
  </span>
@@ -662,399 +851,523 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
662
851
  </section>
663
852
 
664
853
  {/* Divider */}
665
- <div style={{
666
- height: "1px",
667
- backgroundColor: "rgba(128, 128, 128, 0.2)",
668
- margin: "0 16px"
669
- }} />
670
-
671
- {/* GPU Pods Section */}
672
- <section className="runtime-section" style={{ padding: "12px 16px" }}>
673
- <div
674
- style={{
675
- display: "flex",
676
- justifyContent: "space-between",
677
- alignItems: "center",
678
- marginBottom: "12px"
679
- }}
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 }}
680
875
  >
681
- <h3 className="runtime-section-title" style={{ fontSize: "12px", fontWeight: 500 }}>
682
- Remote GPU Pods
683
- </h3>
684
- {apiConfigured && (
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>
896
+
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>
685
956
  <button
686
957
  className="runtime-btn runtime-btn-secondary"
687
- onClick={handleConnectToPrimeIntellect}
958
+ onClick={handleOpenProviderDashboard}
688
959
  style={{
689
960
  fontSize: "11px",
690
961
  padding: "6px 12px",
691
- backgroundColor: "#000",
692
- color: "white",
962
+ backgroundColor: "var(--mc-text-color)",
963
+ color: "var(--mc-cell-background)",
693
964
  border: "none",
694
965
  borderRadius: "8px",
695
- cursor: "pointer"
966
+ cursor: "pointer",
696
967
  }}
697
968
  >
698
- Manage
969
+ <ExternalLink size={10} style={{ marginRight: "3px" }} />
970
+ Get Key
699
971
  </button>
700
- )}
972
+ </div>
701
973
  </div>
702
-
703
- {apiConfigured === false ? (
704
- <div className="runtime-empty-state" style={{ padding: "6px" }}>
705
- <p
974
+ ) : loading || apiConfigured === null ? (
975
+ <div
976
+ style={{
977
+ padding: "8px 0",
978
+ color: "var(--text-secondary)",
979
+ fontSize: "11px",
980
+ }}
981
+ >
982
+ Loading...
983
+ </div>
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
706
1020
  style={{
1021
+ fontSize: "11px",
1022
+ fontWeight: 600,
1023
+ color: "var(--mc-text-color)",
707
1024
  marginBottom: "4px",
1025
+ }}
1026
+ >
1027
+ ⚠️ Running Pod Detected
1028
+ </div>
1029
+ <div
1030
+ style={{
1031
+ fontSize: "10px",
708
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={{
709
1041
  fontSize: "10px",
1042
+ marginBottom: "8px",
1043
+ color: "var(--mc-text-color)",
710
1044
  }}
711
1045
  >
712
- Enter API key to enable GPU pods
713
- </p>
714
- <div style={{ marginBottom: "4px", width: "100%" }}>
715
- <input
716
- type="password"
717
- placeholder="API key"
718
- value={apiKey}
719
- onChange={(e) => setApiKey(e.target.value)}
720
- onKeyPress={(e) => e.key === "Enter" && handleSaveApiKey()}
721
- style={{
722
- width: "100%",
723
- padding: "6px 12px",
724
- borderRadius: "8px",
725
- border: "1px solid var(--border-color)",
726
- backgroundColor: "var(--background)",
727
- color: "var(--text)",
728
- fontSize: "11px",
729
- marginBottom: "3px",
730
- }}
731
- />
732
- {saveError && (
733
- <p
734
- style={{
735
- color: "var(--error-color)",
736
- fontSize: "10px",
737
- marginBottom: "4px",
738
- }}
739
- >
740
- {saveError}
741
- </p>
742
- )}
1046
+ <span style={{ fontWeight: 500 }}>{discoveredPod.name}</span>{" "}
1047
+ • {discoveredPod.gpuType} • $
1048
+ {discoveredPod.costPerHour.toFixed(2)}/hour
743
1049
  </div>
744
- <div style={{ display: "flex", gap: "4px", width: "100%" }}>
1050
+ <div style={{ display: "flex", gap: "6px" }}>
745
1051
  <button
746
- className="runtime-btn runtime-btn-primary"
747
- onClick={handleSaveApiKey}
748
- disabled={saving}
1052
+ className="runtime-btn runtime-btn-secondary"
1053
+ onClick={() => handleConnectToPod(discoveredPod.id)}
749
1054
  style={{
750
1055
  flex: 1,
751
- fontSize: "11px",
1056
+ fontSize: "10px",
752
1057
  padding: "6px 12px",
753
- backgroundColor: "#000",
754
- color: "white",
1058
+ backgroundColor: "rgba(251, 191, 36, 0.8)",
1059
+ color: "var(--mc-text-color)",
755
1060
  border: "none",
756
1061
  borderRadius: "8px",
757
- cursor: "pointer"
1062
+ cursor: "pointer",
1063
+ fontWeight: 600,
758
1064
  }}
759
1065
  >
760
- {saving ? "Saving..." : "Save"}
1066
+ Reconnect
761
1067
  </button>
762
1068
  <button
763
1069
  className="runtime-btn runtime-btn-secondary"
764
- onClick={handleConnectToPrimeIntellect}
1070
+ onClick={() =>
1071
+ handleDeletePod(discoveredPod.id, discoveredPod.name)
1072
+ }
1073
+ disabled={deletingPodId === discoveredPod.id}
765
1074
  style={{
766
- fontSize: "11px",
1075
+ fontSize: "10px",
767
1076
  padding: "6px 12px",
768
- backgroundColor: "#000",
1077
+ backgroundColor: "rgba(220, 38, 38, 0.9)",
769
1078
  color: "white",
770
1079
  border: "none",
771
1080
  borderRadius: "8px",
772
- cursor: "pointer"
1081
+ cursor: "pointer",
773
1082
  }}
774
1083
  >
775
- <ExternalLink size={10} style={{ marginRight: "3px" }} />
776
- Get Key
1084
+ {deletingPodId === discoveredPod.id ? "..." : "Terminate"}
777
1085
  </button>
778
1086
  </div>
779
1087
  </div>
780
- ) : loading || apiConfigured === null ? (
781
- <div style={{ padding: "8px 0", color: "var(--text-secondary)", fontSize: "11px" }}>
782
- Loading...
783
- </div>
784
- ) : gpuPods.filter(p => p.status === "running").length === 0 && !discoveredPod ? (
785
- <div style={{ padding: "8px 0", color: "var(--text-secondary)", fontSize: "11px" }}>
786
- Currently not connected to any.
787
- </div>
788
- ) : !connectedPodId && discoveredPod ? (
789
- <div style={{ padding: "8px 0" }}>
790
- <div style={{
791
- backgroundColor: "#fff3cd",
792
- border: "1px solid #ffc107",
793
- borderRadius: "8px",
794
- padding: "12px",
795
- marginBottom: "8px"
796
- }}>
797
- <div style={{ fontSize: "11px", fontWeight: 600, color: "#856404", marginBottom: "4px" }}>
798
- ⚠️ Running Pod Detected
799
- </div>
800
- <div style={{ fontSize: "10px", color: "#856404", marginBottom: "8px" }}>
801
- Found a running pod but not connected (backend may have restarted). This pod is still costing money!
802
- </div>
803
- <div style={{ fontSize: "10px", marginBottom: "8px" }}>
804
- <span style={{ fontWeight: 500 }}>{discoveredPod.name}</span> • {discoveredPod.gpuType} • ${discoveredPod.costPerHour.toFixed(2)}/hour
805
- </div>
806
- <div style={{ display: "flex", gap: "6px" }}>
807
- <button
808
- className="runtime-btn runtime-btn-secondary"
809
- onClick={() => handleConnectToPod(discoveredPod.id)}
810
- style={{
811
- flex: 1,
812
- fontSize: "10px",
813
- padding: "6px 12px",
814
- backgroundColor: "#ffc107",
815
- color: "#000",
816
- border: "none",
817
- borderRadius: "8px",
818
- cursor: "pointer",
819
- fontWeight: 600
820
- }}
821
- >
822
- Reconnect
823
- </button>
824
- <button
825
- className="runtime-btn runtime-btn-secondary"
826
- onClick={() => handleDeletePod(discoveredPod.id, discoveredPod.name)}
827
- 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
828
1097
  style={{
829
- fontSize: "10px",
830
- padding: "6px 12px",
831
- backgroundColor: "#dc2626",
832
- color: "white",
833
- border: "none",
834
- borderRadius: "8px",
835
- cursor: "pointer"
1098
+ display: "flex",
1099
+ justifyContent: "space-between",
1100
+ alignItems: "flex-start",
1101
+ marginBottom: "8px",
836
1102
  }}
837
1103
  >
838
- {deletingPodId === discoveredPod.id ? "..." : "Terminate"}
839
- </button>
840
- </div>
841
- </div>
842
- </div>
843
- ) : (
844
- gpuPods
845
- .filter((pod) => pod.status === "running")
846
- .map((pod) => {
847
- const isConnected = pod.id === connectedPodId;
848
- return (
849
- <div key={pod.id} style={{ padding: "8px 0" }}>
850
- <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "8px" }}>
851
- <div>
852
- <div style={{ fontSize: "11px", marginBottom: "4px" }}>
853
- <span style={{ fontWeight: 500 }}>{pod.name}</span>
854
- {isConnected && (
855
- <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={{
856
1110
  marginLeft: "6px",
857
1111
  fontSize: "9px",
858
- backgroundColor: "#10b981",
1112
+ backgroundColor: "rgba(16, 185, 129, 0.9)",
859
1113
  color: "white",
860
1114
  padding: "2px 6px",
861
- borderRadius: "4px"
862
- }}>
863
- Connected
864
- </span>
865
- )}
866
- </div>
867
- <div style={{ fontSize: "10px", color: "var(--text-secondary)" }}>
868
- {pod.gpuType} • ${pod.costPerHour.toFixed(2)}/hour
869
- </div>
870
- </div>
871
- <div style={{ display: "flex", gap: "6px" }}>
872
- {isConnected ? (
873
- <>
874
- <button
875
- className="runtime-btn runtime-btn-secondary"
876
- onClick={handleDisconnect}
877
- style={{
878
- fontSize: "10px",
879
- padding: "6px 12px",
880
- backgroundColor: "#000",
881
- color: "white",
882
- border: "none",
883
- borderRadius: "8px",
884
- cursor: "pointer"
885
- }}
886
- >
887
- Disconnect
888
- </button>
889
- <button
890
- className="runtime-btn runtime-btn-secondary"
891
- onClick={() => handleDeletePod(pod.id, pod.name)}
892
- disabled={deletingPodId === pod.id}
893
- style={{
894
- fontSize: "10px",
895
- padding: "6px 12px",
896
- backgroundColor: "#dc2626",
897
- color: "white",
898
- border: "none",
899
- borderRadius: "8px",
900
- cursor: "pointer"
901
- }}
902
- >
903
- {deletingPodId === pod.id ? "..." : "Terminate"}
904
- </button>
905
- </>
906
- ) : (
907
- <>
908
- <button
909
- className="runtime-btn runtime-btn-secondary"
910
- onClick={() => handleConnectToPod(pod.id)}
911
- disabled={connectingPodId === pod.id}
912
- style={{
913
- fontSize: "10px",
914
- padding: "6px 12px",
915
- backgroundColor: "#10b981",
916
- color: "white",
917
- border: "none",
918
- borderRadius: "8px",
919
- cursor: "pointer"
920
- }}
921
- >
922
- {connectingPodId === pod.id ? "Connecting..." : "Connect"}
923
- </button>
924
- <button
925
- className="runtime-btn runtime-btn-secondary"
926
- onClick={() => handleDeletePod(pod.id, pod.name)}
927
- disabled={deletingPodId === pod.id}
928
- style={{
929
- fontSize: "10px",
930
- padding: "6px 12px",
931
- backgroundColor: "#dc2626",
932
- color: "white",
933
- border: "none",
934
- borderRadius: "8px",
935
- cursor: "pointer"
936
- }}
937
- >
938
- {deletingPodId === pod.id ? "..." : "Terminate"}
939
- </button>
940
- </>
1115
+ borderRadius: "4px",
1116
+ }}
1117
+ >
1118
+ Connected
1119
+ </span>
941
1120
  )}
942
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
+ )}
943
1204
  </div>
944
1205
  </div>
945
- );
946
- })
947
- )}
948
- </section>
1206
+ </div>
1207
+ );
1208
+ })
1209
+ )}
1210
+ </section>
949
1211
 
950
1212
  {/* Divider */}
951
- <div style={{
952
- height: "1px",
953
- backgroundColor: "rgba(128, 128, 128, 0.2)",
954
- margin: "0 16px"
955
- }} />
956
-
957
- {/* Browse Available GPUs Section */}
958
- {apiConfigured && (
959
- <section className="runtime-section" style={{ padding: "12px 16px" }}>
960
- {/* Search and Filter Bar */}
961
- <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"
962
1253
  style={{
963
- 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",
964
1262
  display: "flex",
965
- gap: "8px",
966
1263
  alignItems: "center",
1264
+ justifyContent: "center",
967
1265
  }}
968
1266
  >
969
- <input
970
- type="text"
971
- placeholder="Search GPU (e.g., H100, A100, RTX 4090)"
972
- value={searchQuery}
973
- onChange={(e) => setSearchQuery(e.target.value)}
974
- style={{
975
- flex: 1,
976
- padding: "6px 12px",
977
- fontSize: "11px",
978
- border: "1px solid #d1d5db",
979
- borderRadius: "8px",
980
- backgroundColor: "var(--background)",
981
- color: "var(--text)",
982
- outline: "none",
983
- }}
984
- />
985
- <button
986
- className="runtime-btn runtime-btn-secondary"
987
- onClick={() => setShowFilterPopup(!showFilterPopup)}
988
- style={{
989
- padding: "6px 12px",
990
- fontSize: "11px",
991
- position: "relative",
992
- backgroundColor: "#000",
993
- color: "white",
994
- border: "none",
995
- borderRadius: "8px",
996
- cursor: "pointer"
997
- }}
998
- >
999
- <Filter size={10} style={{ marginRight: "3px" }} />
1000
- Filter
1001
- {(filters.gpu_type ||
1002
- filters.gpu_count ||
1003
- filters.security ||
1004
- filters.socket) && (
1005
- <span
1006
- style={{
1007
- position: "absolute",
1008
- top: "-2px",
1009
- right: "-2px",
1010
- width: "8px",
1011
- height: "8px",
1012
- borderRadius: "50%",
1013
- backgroundColor: "var(--accent)",
1014
- }}
1015
- />
1016
- )}
1017
- </button>
1018
- </div>
1019
-
1020
- {/* Filter Popup */}
1021
- <FilterPopup
1022
- isOpen={showFilterPopup}
1023
- onClose={() => setShowFilterPopup(false)}
1024
- filters={filters}
1025
- onFiltersChange={setFilters}
1026
- onApply={loadAvailableGPUs}
1027
- />
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>
1028
1288
 
1029
- {/* Results */}
1030
- {loadingAvailability ? (
1031
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1032
- Loading...
1033
- </div>
1034
- ) : availableGPUs.length === 0 ? (
1035
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1036
- Use filters to find available GPUs
1037
- </div>
1038
- ) : filteredGPUs.length === 0 ? (
1039
- <div style={{ padding: "16px 0", textAlign: "center", color: "var(--text-secondary)", fontSize: "11px" }}>
1040
- No GPUs match "{searchQuery}"
1041
- </div>
1042
- ) : (
1043
- <div style={{ maxHeight: "calc(100vh - 400px)", overflowY: "auto", paddingRight: "12px" }}>
1044
- {filteredGPUs.map((gpu, index) => (
1045
- <React.Fragment key={`${gpu.cloudId}-${index}`}>
1046
- {index > 0 && (
1047
- <div style={{
1048
- height: "1px",
1049
- backgroundColor: "rgba(128, 128, 128, 0.15)",
1050
- margin: "8px 0"
1051
- }} />
1052
- )}
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 && (
1053
1358
  <div
1054
1359
  style={{
1055
- padding: "8px 0",
1360
+ height: "1px",
1361
+ backgroundColor: "rgba(128, 128, 128, 0.15)",
1362
+ margin: "8px 0",
1056
1363
  }}
1057
- >
1364
+ />
1365
+ )}
1366
+ <div
1367
+ style={{
1368
+ padding: "8px 0",
1369
+ }}
1370
+ >
1058
1371
  <div
1059
1372
  style={{
1060
1373
  display: "flex",
@@ -1071,7 +1384,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1071
1384
  marginBottom: "1px",
1072
1385
  }}
1073
1386
  >
1074
- {gpu.gpuType} ({gpu.gpuCount}x)
1387
+ {gpu.gpuName || gpu.gpuType} ({gpu.gpuCount}x)
1075
1388
  </div>
1076
1389
  <div
1077
1390
  style={{
@@ -1079,10 +1392,18 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1079
1392
  color: "var(--text-secondary)",
1080
1393
  }}
1081
1394
  >
1082
- {gpu.provider} - {gpu.socket} - {gpu.gpuMemory}GB
1395
+ {gpu.provider} {gpu.memoryGb || gpu.gpuMemory}GB{gpu.regionDescription ? ` • ${gpu.regionDescription}` : ""}
1083
1396
  </div>
1084
1397
  </div>
1085
- <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
+ >
1086
1407
  <div>
1087
1408
  <div
1088
1409
  style={{
@@ -1091,7 +1412,7 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1091
1412
  color: "var(--accent)",
1092
1413
  }}
1093
1414
  >
1094
- ${gpu.prices?.onDemand?.toFixed(2) || "N/A"}/hr
1415
+ ${(gpu.priceHr ?? gpu.prices?.onDemand)?.toFixed(2) || "N/A"}/hr
1095
1416
  </div>
1096
1417
  {gpu.stockStatus && (
1097
1418
  <div
@@ -1119,11 +1440,11 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1119
1440
  fontSize: "10px",
1120
1441
  padding: "6px 16px",
1121
1442
  whiteSpace: "nowrap",
1122
- backgroundColor: "#000",
1123
- color: "white",
1443
+ backgroundColor: "var(--mc-text-color)",
1444
+ color: "var(--mc-cell-background)",
1124
1445
  border: "none",
1125
1446
  borderRadius: "8px",
1126
- cursor: "pointer"
1447
+ cursor: "pointer",
1127
1448
  }}
1128
1449
  >
1129
1450
  {creatingPodId === gpu.cloudId
@@ -1174,12 +1495,12 @@ const ComputePopup: React.FC<ComputePopupProps> = ({ onClose }) => {
1174
1495
  )}
1175
1496
  </div>
1176
1497
  </div>
1177
- </React.Fragment>
1178
- ))}
1179
- </div>
1180
- )}
1181
- </section>
1182
- )}
1498
+ </React.Fragment>
1499
+ ))}
1500
+ </div>
1501
+ )}
1502
+ </section>
1503
+ )}
1183
1504
  </div>
1184
1505
  </>
1185
1506
  );