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.
- frontend/app/globals.css +734 -27
- frontend/app/layout.tsx +13 -3
- frontend/components/Notebook.tsx +2 -14
- frontend/components/cell/MonacoCell.tsx +99 -5
- frontend/components/layout/Sidebar.tsx +39 -4
- frontend/components/panels/ClaudePanel.tsx +461 -0
- frontend/components/popups/ComputePopup.tsx +739 -418
- frontend/components/popups/FilterPopup.tsx +305 -189
- frontend/components/popups/MetricsPopup.tsx +20 -1
- frontend/components/popups/ProviderConfigModal.tsx +322 -0
- frontend/components/popups/ProviderDropdown.tsx +398 -0
- frontend/components/popups/SettingsPopup.tsx +1 -1
- frontend/contexts/ClaudeContext.tsx +392 -0
- frontend/contexts/PodWebSocketContext.tsx +16 -21
- frontend/hooks/useInlineDiff.ts +269 -0
- frontend/lib/api.ts +323 -12
- frontend/lib/settings.ts +5 -0
- frontend/lib/websocket-native.ts +4 -8
- frontend/lib/websocket.ts +1 -2
- frontend/package-lock.json +733 -36
- frontend/package.json +2 -0
- frontend/public/assets/icons/providers/lambda_labs.svg +22 -0
- frontend/public/assets/icons/providers/prime_intellect.svg +18 -0
- frontend/public/assets/icons/providers/runpod.svg +9 -0
- frontend/public/assets/icons/providers/vastai.svg +1 -0
- frontend/settings.md +54 -0
- frontend/tsconfig.tsbuildinfo +1 -0
- frontend/types/claude.ts +194 -0
- kernel_run.py +13 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/WHEEL +1 -1
- morecompute/__init__.py +1 -1
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +24 -67
- morecompute/execution/worker.py +6 -72
- morecompute/models/api_models.py +62 -0
- morecompute/notebook.py +11 -0
- morecompute/server.py +641 -133
- morecompute/services/claude_service.py +392 -0
- morecompute/services/pod_manager.py +168 -67
- morecompute/services/pod_monitor.py +67 -39
- morecompute/services/prime_intellect.py +0 -4
- morecompute/services/providers/__init__.py +92 -0
- morecompute/services/providers/base_provider.py +336 -0
- morecompute/services/providers/lambda_labs_provider.py +394 -0
- morecompute/services/providers/provider_factory.py +194 -0
- morecompute/services/providers/runpod_provider.py +504 -0
- morecompute/services/providers/vastai_provider.py +407 -0
- morecompute/utils/cell_magics.py +0 -3
- morecompute/utils/config_util.py +93 -3
- morecompute/utils/special_commands.py +5 -32
- morecompute/utils/version_check.py +117 -0
- frontend/styling_README.md +0 -23
- {more_compute-0.4.3.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { ProviderInfo, configureGpuProvider } from "../../lib/api";
|
|
5
|
+
|
|
6
|
+
interface ProviderConfigModalProps {
|
|
7
|
+
provider: ProviderInfo;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onConfigured: (provider: ProviderInfo) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ProviderConfigModal({
|
|
13
|
+
provider,
|
|
14
|
+
onClose,
|
|
15
|
+
onConfigured,
|
|
16
|
+
}: ProviderConfigModalProps) {
|
|
17
|
+
const [apiKey, setApiKey] = useState("");
|
|
18
|
+
const [makeActive, setMakeActive] = useState(true);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
// Reset form when provider changes
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setApiKey("");
|
|
25
|
+
setError(null);
|
|
26
|
+
setMakeActive(true);
|
|
27
|
+
}, [provider.name]);
|
|
28
|
+
|
|
29
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
|
|
32
|
+
if (!apiKey.trim()) {
|
|
33
|
+
setError("API key is required");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
|
|
41
|
+
await configureGpuProvider(provider.name, {
|
|
42
|
+
api_key: apiKey.trim(),
|
|
43
|
+
make_active: makeActive,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Update provider status and notify parent
|
|
47
|
+
const updatedProvider: ProviderInfo = {
|
|
48
|
+
...provider,
|
|
49
|
+
configured: true,
|
|
50
|
+
is_active: makeActive,
|
|
51
|
+
};
|
|
52
|
+
onConfigured(updatedProvider);
|
|
53
|
+
onClose();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError(
|
|
56
|
+
err instanceof Error ? err.message : "Failed to configure provider"
|
|
57
|
+
);
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
65
|
+
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
|
66
|
+
<div className="modal-header">
|
|
67
|
+
<h3>Configure {provider.display_name}</h3>
|
|
68
|
+
<button className="modal-close" onClick={onClose}>
|
|
69
|
+
×
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<form onSubmit={handleSubmit}>
|
|
74
|
+
<div className="modal-body">
|
|
75
|
+
<p className="modal-description">
|
|
76
|
+
Enter your {provider.display_name} API key to enable GPU access.
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
<div className="form-group">
|
|
80
|
+
<label htmlFor="apiKey">API Key</label>
|
|
81
|
+
<input
|
|
82
|
+
id="apiKey"
|
|
83
|
+
type="password"
|
|
84
|
+
value={apiKey}
|
|
85
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
86
|
+
placeholder={`Enter your ${provider.display_name} API key`}
|
|
87
|
+
disabled={loading}
|
|
88
|
+
autoFocus
|
|
89
|
+
autoComplete="off"
|
|
90
|
+
data-lpignore="true"
|
|
91
|
+
data-1p-ignore="true"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="form-group checkbox">
|
|
96
|
+
<label>
|
|
97
|
+
<input
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={makeActive}
|
|
100
|
+
onChange={(e) => setMakeActive(e.target.checked)}
|
|
101
|
+
disabled={loading}
|
|
102
|
+
/>
|
|
103
|
+
Set as active provider
|
|
104
|
+
</label>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{error && <div className="error-message">{error}</div>}
|
|
108
|
+
|
|
109
|
+
<div className="help-text">
|
|
110
|
+
<a
|
|
111
|
+
href={provider.dashboard_url}
|
|
112
|
+
target="_blank"
|
|
113
|
+
rel="noopener noreferrer"
|
|
114
|
+
>
|
|
115
|
+
Get your API key from {provider.display_name}
|
|
116
|
+
</a>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="modal-footer">
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className="btn-secondary"
|
|
124
|
+
onClick={onClose}
|
|
125
|
+
disabled={loading}
|
|
126
|
+
>
|
|
127
|
+
Cancel
|
|
128
|
+
</button>
|
|
129
|
+
<button type="submit" className="btn-primary" disabled={loading}>
|
|
130
|
+
{loading ? "Saving..." : "Save & Connect"}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</form>
|
|
134
|
+
|
|
135
|
+
<style jsx>{`
|
|
136
|
+
.modal-overlay {
|
|
137
|
+
position: fixed;
|
|
138
|
+
top: 0;
|
|
139
|
+
left: 0;
|
|
140
|
+
right: 0;
|
|
141
|
+
bottom: 0;
|
|
142
|
+
background: rgba(0, 0, 0, 0.7);
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
z-index: 1000;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.modal-content {
|
|
150
|
+
background: var(--mc-cell-background);
|
|
151
|
+
border: 1px solid var(--mc-border);
|
|
152
|
+
border-radius: 12px;
|
|
153
|
+
width: 100%;
|
|
154
|
+
max-width: 420px;
|
|
155
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.modal-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: space-between;
|
|
162
|
+
padding: 16px 20px;
|
|
163
|
+
border-bottom: 1px solid var(--mc-border);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.modal-header h3 {
|
|
167
|
+
margin: 0;
|
|
168
|
+
font-size: 16px;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
color: var(--mc-text-color);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.modal-close {
|
|
174
|
+
background: none;
|
|
175
|
+
border: none;
|
|
176
|
+
color: var(--mc-text-color);
|
|
177
|
+
opacity: 0.6;
|
|
178
|
+
font-size: 24px;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
padding: 0;
|
|
181
|
+
line-height: 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.modal-close:hover {
|
|
185
|
+
opacity: 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.modal-body {
|
|
189
|
+
padding: 20px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.modal-description {
|
|
193
|
+
margin: 0 0 16px;
|
|
194
|
+
color: var(--mc-text-color);
|
|
195
|
+
opacity: 0.7;
|
|
196
|
+
font-size: 13px;
|
|
197
|
+
line-height: 1.5;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.warning-text {
|
|
201
|
+
color: #f59e0b;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.form-group {
|
|
205
|
+
margin-bottom: 16px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.form-group label {
|
|
209
|
+
display: block;
|
|
210
|
+
margin-bottom: 6px;
|
|
211
|
+
color: var(--mc-text-color);
|
|
212
|
+
font-size: 13px;
|
|
213
|
+
font-weight: 500;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.form-group input[type="password"],
|
|
217
|
+
.form-group input[type="text"] {
|
|
218
|
+
width: 100%;
|
|
219
|
+
padding: 10px 12px;
|
|
220
|
+
background: var(--mc-background);
|
|
221
|
+
border: 1px solid var(--mc-border);
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
color: var(--mc-text-color);
|
|
224
|
+
font-size: 14px;
|
|
225
|
+
transition: border-color 0.2s;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.form-group input:focus {
|
|
229
|
+
outline: none;
|
|
230
|
+
border-color: var(--mc-primary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.form-group input:disabled {
|
|
234
|
+
opacity: 0.5;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.form-group.checkbox label {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 8px;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.form-group.checkbox input[type="checkbox"] {
|
|
245
|
+
width: 16px;
|
|
246
|
+
height: 16px;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.error-message {
|
|
251
|
+
padding: 10px 12px;
|
|
252
|
+
background: rgba(239, 68, 68, 0.1);
|
|
253
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
254
|
+
border-radius: 6px;
|
|
255
|
+
color: #ef4444;
|
|
256
|
+
font-size: 13px;
|
|
257
|
+
margin-bottom: 16px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.help-text {
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.help-text a {
|
|
265
|
+
color: var(--mc-primary);
|
|
266
|
+
text-decoration: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.help-text a:hover {
|
|
270
|
+
text-decoration: underline;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.modal-footer {
|
|
274
|
+
display: flex;
|
|
275
|
+
justify-content: flex-end;
|
|
276
|
+
gap: 8px;
|
|
277
|
+
padding: 16px 20px;
|
|
278
|
+
border-top: 1px solid var(--mc-border);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.btn-secondary,
|
|
282
|
+
.btn-primary {
|
|
283
|
+
padding: 8px 16px;
|
|
284
|
+
border-radius: 6px;
|
|
285
|
+
font-size: 13px;
|
|
286
|
+
font-weight: 500;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
transition: all 0.2s;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.btn-secondary {
|
|
292
|
+
background: transparent;
|
|
293
|
+
border: 1px solid var(--mc-border);
|
|
294
|
+
color: var(--mc-text-color);
|
|
295
|
+
opacity: 0.8;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.btn-secondary:hover:not(:disabled) {
|
|
299
|
+
background: var(--mc-secondary);
|
|
300
|
+
opacity: 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.btn-primary {
|
|
304
|
+
background: var(--mc-primary);
|
|
305
|
+
border: none;
|
|
306
|
+
color: white;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.btn-primary:hover:not(:disabled) {
|
|
310
|
+
background: var(--mc-primary-hover);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.btn-secondary:disabled,
|
|
314
|
+
.btn-primary:disabled {
|
|
315
|
+
opacity: 0.5;
|
|
316
|
+
cursor: not-allowed;
|
|
317
|
+
}
|
|
318
|
+
`}</style>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import {
|
|
6
|
+
ProviderInfo,
|
|
7
|
+
fetchGpuProviders,
|
|
8
|
+
setActiveGpuProvider,
|
|
9
|
+
} from "../../lib/api";
|
|
10
|
+
|
|
11
|
+
// Provider logos stored in public/assets/icons/providers/
|
|
12
|
+
// Only SSH-based providers are supported
|
|
13
|
+
const PROVIDER_LOGOS: Record<string, string> = {
|
|
14
|
+
runpod: "/assets/icons/providers/runpod.svg",
|
|
15
|
+
lambda_labs: "/assets/icons/providers/lambda_labs.svg",
|
|
16
|
+
vastai: "/assets/icons/providers/vastai.svg",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_LOGO = "/assets/icons/providers/runpod.svg";
|
|
20
|
+
|
|
21
|
+
interface ProviderDropdownProps {
|
|
22
|
+
onProviderChange: (provider: ProviderInfo) => void;
|
|
23
|
+
onConfigureProvider: (provider: ProviderInfo) => void;
|
|
24
|
+
selectedProvider: ProviderInfo | null;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function ProviderDropdown({
|
|
29
|
+
onProviderChange,
|
|
30
|
+
onConfigureProvider,
|
|
31
|
+
selectedProvider,
|
|
32
|
+
disabled = false,
|
|
33
|
+
}: ProviderDropdownProps) {
|
|
34
|
+
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
35
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
|
|
40
|
+
// Load providers on mount
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
loadProviders();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Close dropdown when clicking outside
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
function handleClickOutside(event: MouseEvent) {
|
|
48
|
+
if (
|
|
49
|
+
dropdownRef.current &&
|
|
50
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
51
|
+
) {
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
57
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
async function loadProviders() {
|
|
61
|
+
try {
|
|
62
|
+
setLoading(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
const response = await fetchGpuProviders();
|
|
65
|
+
setProviders(response.providers);
|
|
66
|
+
|
|
67
|
+
// If no provider is selected, try to restore from backend or localStorage
|
|
68
|
+
if (!selectedProvider) {
|
|
69
|
+
// First try backend active provider
|
|
70
|
+
let providerToSelect = response.active_provider;
|
|
71
|
+
|
|
72
|
+
// Fallback to localStorage if no backend active provider
|
|
73
|
+
if (!providerToSelect) {
|
|
74
|
+
const savedProvider = localStorage.getItem(
|
|
75
|
+
"morecompute_active_provider",
|
|
76
|
+
);
|
|
77
|
+
if (savedProvider) {
|
|
78
|
+
providerToSelect = savedProvider;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (providerToSelect) {
|
|
83
|
+
const active = response.providers.find(
|
|
84
|
+
(p) => p.name === providerToSelect,
|
|
85
|
+
);
|
|
86
|
+
// Only auto-select if the provider is configured
|
|
87
|
+
if (active && active.configured) {
|
|
88
|
+
onProviderChange(active);
|
|
89
|
+
} else if (active && !active.configured) {
|
|
90
|
+
// Provider saved but not configured - clear localStorage and don't auto-select
|
|
91
|
+
localStorage.removeItem("morecompute_active_provider");
|
|
92
|
+
// Still set the provider so the UI shows it needs configuration
|
|
93
|
+
onProviderChange(active);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
setError(err instanceof Error ? err.message : "Failed to load providers");
|
|
99
|
+
} finally {
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function handleSelectProvider(provider: ProviderInfo) {
|
|
105
|
+
if (!provider.configured) {
|
|
106
|
+
// Open configuration modal for unconfigured providers
|
|
107
|
+
onConfigureProvider(provider);
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Set as active provider
|
|
114
|
+
await setActiveGpuProvider(provider.name);
|
|
115
|
+
|
|
116
|
+
// Save to localStorage for persistence
|
|
117
|
+
localStorage.setItem("morecompute_active_provider", provider.name);
|
|
118
|
+
|
|
119
|
+
onProviderChange(provider);
|
|
120
|
+
setIsOpen(false);
|
|
121
|
+
|
|
122
|
+
// Refresh provider list to update active status
|
|
123
|
+
await loadProviders();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
setError(
|
|
126
|
+
err instanceof Error ? err.message : "Failed to switch provider",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getProviderLogo(providerName: string): string {
|
|
132
|
+
return PROVIDER_LOGOS[providerName] || DEFAULT_LOGO;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="provider-dropdown" ref={dropdownRef}>
|
|
137
|
+
<button
|
|
138
|
+
className="provider-dropdown-button"
|
|
139
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
140
|
+
disabled={disabled || loading}
|
|
141
|
+
>
|
|
142
|
+
{loading ? (
|
|
143
|
+
<span className="provider-loading">Loading...</span>
|
|
144
|
+
) : selectedProvider ? (
|
|
145
|
+
<Image
|
|
146
|
+
src={getProviderLogo(selectedProvider.name)}
|
|
147
|
+
alt={selectedProvider.display_name}
|
|
148
|
+
width={120}
|
|
149
|
+
height={28}
|
|
150
|
+
className="provider-logo"
|
|
151
|
+
style={{ objectFit: "contain" }}
|
|
152
|
+
/>
|
|
153
|
+
) : (
|
|
154
|
+
<span className="provider-placeholder">Select Provider</span>
|
|
155
|
+
)}
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
{isOpen && !loading && (
|
|
159
|
+
<div className="provider-dropdown-menu">
|
|
160
|
+
{error && <div className="provider-error">{error}</div>}
|
|
161
|
+
|
|
162
|
+
{providers.map((provider) => (
|
|
163
|
+
<div key={provider.name} className="provider-option-row">
|
|
164
|
+
<button
|
|
165
|
+
className={`provider-option ${
|
|
166
|
+
provider.is_active ? "active" : ""
|
|
167
|
+
} ${!provider.configured ? "unconfigured" : ""}`}
|
|
168
|
+
onClick={() => handleSelectProvider(provider)}
|
|
169
|
+
>
|
|
170
|
+
<span className="provider-logo-container">
|
|
171
|
+
<Image
|
|
172
|
+
src={getProviderLogo(provider.name)}
|
|
173
|
+
alt={provider.display_name}
|
|
174
|
+
width={90}
|
|
175
|
+
height={24}
|
|
176
|
+
className="provider-logo"
|
|
177
|
+
style={{ objectFit: "contain" }}
|
|
178
|
+
/>
|
|
179
|
+
</span>
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
className="provider-config-btn"
|
|
183
|
+
onClick={(e) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
onConfigureProvider(provider);
|
|
186
|
+
setIsOpen(false);
|
|
187
|
+
}}
|
|
188
|
+
title={
|
|
189
|
+
provider.configured
|
|
190
|
+
? "Reconfigure API Key"
|
|
191
|
+
: "Configure API Key"
|
|
192
|
+
}
|
|
193
|
+
>
|
|
194
|
+
<svg
|
|
195
|
+
width="14"
|
|
196
|
+
height="14"
|
|
197
|
+
viewBox="0 0 24 24"
|
|
198
|
+
fill="none"
|
|
199
|
+
stroke="currentColor"
|
|
200
|
+
strokeWidth="2"
|
|
201
|
+
>
|
|
202
|
+
<circle cx="12" cy="12" r="3" />
|
|
203
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
|
|
209
|
+
<div className="provider-dropdown-footer">
|
|
210
|
+
<a
|
|
211
|
+
href={selectedProvider?.dashboard_url || "#"}
|
|
212
|
+
target="_blank"
|
|
213
|
+
rel="noopener noreferrer"
|
|
214
|
+
className="provider-link"
|
|
215
|
+
>
|
|
216
|
+
Get API Keys
|
|
217
|
+
</a>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<style jsx>{`
|
|
223
|
+
.provider-dropdown {
|
|
224
|
+
position: relative;
|
|
225
|
+
width: 100%;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.provider-dropdown-button {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
width: 100%;
|
|
233
|
+
padding: 12px 16px;
|
|
234
|
+
border: 1px solid #2a2a4e;
|
|
235
|
+
border-radius: 8px;
|
|
236
|
+
background: #1a1a2e;
|
|
237
|
+
color: var(--mc-text-color);
|
|
238
|
+
font-size: 14px;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: all 0.2s;
|
|
241
|
+
min-height: 52px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.provider-dropdown-button:hover:not(:disabled) {
|
|
245
|
+
border-color: var(--mc-primary);
|
|
246
|
+
background: #252545;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.provider-dropdown-button:disabled {
|
|
250
|
+
opacity: 0.5;
|
|
251
|
+
cursor: not-allowed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.provider-logo-container {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: center;
|
|
258
|
+
height: 32px;
|
|
259
|
+
min-width: 110px;
|
|
260
|
+
background: #1a1a2e;
|
|
261
|
+
padding: 6px 12px;
|
|
262
|
+
border-radius: 6px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.provider-placeholder {
|
|
266
|
+
opacity: 0.6;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.provider-loading {
|
|
270
|
+
opacity: 0.6;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.provider-dropdown-menu {
|
|
274
|
+
position: absolute;
|
|
275
|
+
top: calc(100% + 4px);
|
|
276
|
+
left: 0;
|
|
277
|
+
right: 0;
|
|
278
|
+
background: #1a1a2e;
|
|
279
|
+
border: 1px solid #2a2a4e;
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
282
|
+
z-index: 100;
|
|
283
|
+
max-height: 300px;
|
|
284
|
+
overflow-y: auto;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.provider-error {
|
|
288
|
+
padding: 8px 12px;
|
|
289
|
+
color: #ef4444;
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.provider-option {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: center;
|
|
297
|
+
gap: 12px;
|
|
298
|
+
flex: 1;
|
|
299
|
+
padding: 16px 12px;
|
|
300
|
+
border: none;
|
|
301
|
+
background: transparent;
|
|
302
|
+
color: #ffffff;
|
|
303
|
+
font-size: 14px;
|
|
304
|
+
cursor: pointer;
|
|
305
|
+
transition: background 0.15s;
|
|
306
|
+
text-align: center;
|
|
307
|
+
min-height: 56px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.provider-option:hover {
|
|
311
|
+
background: #2a2a4e;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.provider-option.active {
|
|
315
|
+
background: rgba(99, 102, 241, 0.1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.provider-option-row.active-row {
|
|
319
|
+
border-left: 3px solid var(--mc-primary);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.provider-option.unconfigured {
|
|
323
|
+
opacity: 0.8;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.provider-option-row {
|
|
327
|
+
display: flex;
|
|
328
|
+
align-items: stretch;
|
|
329
|
+
border-bottom: 1px solid #2a2a4e;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.provider-option-row:last-of-type {
|
|
333
|
+
border-bottom: none;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.provider-config-btn {
|
|
337
|
+
align-self: stretch;
|
|
338
|
+
min-width: 54px;
|
|
339
|
+
padding: 0 14px;
|
|
340
|
+
background: transparent;
|
|
341
|
+
border: none;
|
|
342
|
+
color: #6b7280;
|
|
343
|
+
cursor: pointer;
|
|
344
|
+
transition: all 0.15s;
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
line-height: 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.provider-config-btn:hover {
|
|
352
|
+
color: #ffffff;
|
|
353
|
+
background: rgba(255, 255, 255, 0.1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.provider-status-dot {
|
|
357
|
+
width: 8px;
|
|
358
|
+
height: 8px;
|
|
359
|
+
border-radius: 50%;
|
|
360
|
+
display: inline-block;
|
|
361
|
+
flex-shrink: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.provider-status-active {
|
|
365
|
+
background: #10b981;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.provider-status-setup {
|
|
369
|
+
background: #fbbf24;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.provider-dropdown-footer {
|
|
373
|
+
padding: 8px 12px;
|
|
374
|
+
border-top: 1px solid #2a2a4e;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.provider-link {
|
|
378
|
+
color: #60a5fa;
|
|
379
|
+
font-size: 12px;
|
|
380
|
+
text-decoration: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.provider-link:hover {
|
|
384
|
+
text-decoration: underline;
|
|
385
|
+
color: #93c5fd;
|
|
386
|
+
}
|
|
387
|
+
`}</style>
|
|
388
|
+
|
|
389
|
+
<style jsx global>{`
|
|
390
|
+
.provider-logo {
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
max-width: 100%;
|
|
393
|
+
height: auto;
|
|
394
|
+
}
|
|
395
|
+
`}</style>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|