more-compute 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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