From d7aa2801bbef74de4e0814ded64d1e76a93e442e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 17 Feb 2026 12:32:23 -0300 Subject: [PATCH] feat(dashboard): add import models progress modal with real-time feedback --- .../dashboard/providers/[id]/page.tsx | 358 ++++++++++++++++-- 1 file changed, 326 insertions(+), 32 deletions(-) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 2a99e886..ee9116d2 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -51,6 +51,16 @@ export default function ProviderDetailPage() { const [proxyTarget, setProxyTarget] = useState(null); const [proxyConfig, setProxyConfig] = useState(null); const [importingModels, setImportingModels] = useState(false); + const [showImportModal, setShowImportModal] = useState(false); + const [importProgress, setImportProgress] = useState({ + current: 0, + total: 0, + phase: "idle" as "idle" | "fetching" | "importing" | "done" | "error", + status: "", + logs: [] as string[], + error: "", + importedCount: 0, + }); const providerInfo = providerNode ? { @@ -66,7 +76,9 @@ export default function ProviderDetailPage() { baseUrl: providerNode.baseUrl, type: providerNode.type, } - : (FREE_PROVIDERS as any)[providerId] || (OAUTH_PROVIDERS as any)[providerId] || (APIKEY_PROVIDERS as any)[providerId]; + : (FREE_PROVIDERS as any)[providerId] || + (OAUTH_PROVIDERS as any)[providerId] || + (APIKEY_PROVIDERS as any)[providerId]; const isOAuth = !!(FREE_PROVIDERS as any)[providerId] || !!(OAUTH_PROVIDERS as any)[providerId]; const models = getModelsByProviderId(providerId); const providerAlias = getProviderAlias(providerId); @@ -237,9 +249,14 @@ export default function ProviderDetailPage() { if (res.ok) { await fetchConnections(); setShowAddApiKeyModal(false); + return null; } + const data = await res.json().catch(() => ({})); + const errorMsg = data.error?.message || data.error || "Failed to save connection"; + return errorMsg; } catch (error) { console.log("Error saving connection:", error); + return "Failed to save connection. Please try again."; } }; @@ -352,24 +369,63 @@ export default function ProviderDetailPage() { if (!activeConnection) return; setImportingModels(true); + setShowImportModal(true); + setImportProgress({ + current: 0, + total: 0, + phase: "fetching", + status: "Fetching available models...", + logs: [], + error: "", + importedCount: 0, + }); + try { const res = await fetch(`/api/providers/${activeConnection.id}/models`); const data = await res.json(); if (!res.ok) { - alert(data.error || "Failed to import models"); + setImportProgress((prev) => ({ + ...prev, + phase: "error", + status: "Failed to fetch models", + error: data.error || "Failed to import models", + })); return; } const fetchedModels = data.models || []; if (fetchedModels.length === 0) { - alert("No models returned from /models."); + setImportProgress((prev) => ({ + ...prev, + phase: "done", + status: "No models found", + logs: ["No models returned from /models endpoint."], + })); return; } + + setImportProgress((prev) => ({ + ...prev, + phase: "importing", + total: fetchedModels.length, + status: `Importing 0 of ${fetchedModels.length} models...`, + logs: [`Found ${fetchedModels.length} models. Starting import...`], + })); + let importedCount = 0; - for (const model of fetchedModels) { + for (let i = 0; i < fetchedModels.length; i++) { + const model = fetchedModels[i]; const modelId = model.id || model.name || model.model; if (!modelId) continue; const parts = modelId.split("/"); const baseAlias = parts[parts.length - 1]; + + setImportProgress((prev) => ({ + ...prev, + current: i + 1, + status: `Importing ${i + 1} of ${fetchedModels.length} models...`, + logs: [...prev.logs, `Importing ${modelId}...`], + })); + // Save as imported (default) model in the DB await fetch("/api/provider-models", { method: "POST", @@ -387,17 +443,132 @@ export default function ProviderDetailPage() { } importedCount += 1; } - if (importedCount === 0) { - alert("No new models were added (all already exist)."); - } + await fetchAliases(); + + setImportProgress((prev) => ({ + ...prev, + phase: "done", + current: fetchedModels.length, + status: + importedCount > 0 + ? `Successfully imported ${importedCount} model${importedCount === 1 ? "" : "s"}!` + : "No new models were added (all already exist).", + logs: [ + ...prev.logs, + importedCount > 0 + ? `✓ Done! ${importedCount} model${importedCount === 1 ? "" : "s"} imported.` + : "No new models were added.", + ], + importedCount, + })); + + // Auto-reload after success + if (importedCount > 0) { + setTimeout(() => { + window.location.reload(); + }, 2000); + } } catch (error) { console.log("Error importing models:", error); + setImportProgress((prev) => ({ + ...prev, + phase: "error", + status: "Import failed", + error: error instanceof Error ? error.message : "An unexpected error occurred", + })); } finally { setImportingModels(false); } }; + // Shared import handler for CompatibleModelsSection + const handleCompatibleImportWithProgress = async ( + fetchModels: () => Promise<{ models: any[] }>, + processModel: (model: any) => Promise + ) => { + setShowImportModal(true); + setImportProgress({ + current: 0, + total: 0, + phase: "fetching", + status: "Fetching available models...", + logs: [], + error: "", + importedCount: 0, + }); + + try { + const data = await fetchModels(); + const models = data.models || []; + if (models.length === 0) { + setImportProgress((prev) => ({ + ...prev, + phase: "done", + status: "No models found", + logs: ["No models returned from /models endpoint."], + })); + return; + } + + setImportProgress((prev) => ({ + ...prev, + phase: "importing", + total: models.length, + status: `Importing 0 of ${models.length} models...`, + logs: [`Found ${models.length} models. Starting import...`], + })); + + let importedCount = 0; + for (let i = 0; i < models.length; i++) { + const model = models[i]; + const modelId = model.id || model.name || model.model; + if (!modelId) continue; + + setImportProgress((prev) => ({ + ...prev, + current: i + 1, + status: `Importing ${i + 1} of ${models.length} models...`, + logs: [...prev.logs, `Importing ${modelId}...`], + })); + + const added = await processModel(model); + if (added) importedCount += 1; + } + + setImportProgress((prev) => ({ + ...prev, + phase: "done", + current: models.length, + status: + importedCount > 0 + ? `Successfully imported ${importedCount} model${importedCount === 1 ? "" : "s"}!` + : "No new models were added.", + logs: [ + ...prev.logs, + importedCount > 0 + ? `✓ Done! ${importedCount} model${importedCount === 1 ? "" : "s"} imported.` + : "No new models were added.", + ], + importedCount, + })); + + if (importedCount > 0) { + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } catch (error) { + console.log("Error importing models:", error); + setImportProgress((prev) => ({ + ...prev, + phase: "error", + status: "Import failed", + error: error instanceof Error ? error.message : "An unexpected error occurred", + })); + } + }; + const canImportModels = connections.some((conn) => conn.isActive !== false); const renderModelsSection = () => { @@ -413,6 +584,7 @@ export default function ProviderDetailPage() { onDeleteAlias={handleDeleteAlias} connections={connections} isAnthropic={isAnthropicCompatible} + onImportWithProgress={handleCompatibleImportWithProgress} /> ); } @@ -813,6 +985,115 @@ export default function ProviderDetailPage() { levelLabel={proxyTarget.label} /> )} + {/* Import Progress Modal */} + { + if (importProgress.phase === "done" || importProgress.phase === "error") { + setShowImportModal(false); + } + }} + title="Importing Models" + size="md" + closeOnOverlay={false} + showCloseButton={importProgress.phase === "done" || importProgress.phase === "error"} + > +
+ {/* Status text */} +
+ {importProgress.phase === "fetching" && ( + + progress_activity + + )} + {importProgress.phase === "importing" && ( + + progress_activity + + )} + {importProgress.phase === "done" && ( + check_circle + )} + {importProgress.phase === "error" && ( + error + )} + {importProgress.status} +
+ + {/* Progress bar */} + {(importProgress.phase === "importing" || importProgress.phase === "done") && + importProgress.total > 0 && ( +
+
+ + {importProgress.current} / {importProgress.total} + + + {Math.round((importProgress.current / importProgress.total) * 100)}% + +
+
+
+
+
+ )} + + {/* Fetching indeterminate bar */} + {importProgress.phase === "fetching" && ( +
+
+
+ )} + + {/* Error message */} + {importProgress.phase === "error" && importProgress.error && ( +
+

{importProgress.error}

+
+ )} + + {/* Log list */} + {importProgress.logs.length > 0 && ( +
+
+ {importProgress.logs.map((log, i) => ( +

+ {log} +

+ ))} +
+
+ )} + + {/* Auto-reload notice */} + {importProgress.phase === "done" && importProgress.importedCount > 0 && ( +

+ Page will refresh automatically... +

+ )} +
+
); } @@ -1175,6 +1456,7 @@ function CompatibleModelsSection({ onDeleteAlias, connections, isAnthropic, + onImportWithProgress, }) { const [newModel, setNewModel] = useState(""); const [adding, setAdding] = useState(false); @@ -1232,29 +1514,24 @@ function CompatibleModelsSection({ setImporting(true); try { - const res = await fetch(`/api/providers/${activeConnection.id}/models`); - const data = await res.json(); - if (!res.ok) { - alert(data.error || "Failed to import models"); - return; - } - const models = data.models || []; - if (models.length === 0) { - alert("No models returned from /models."); - return; - } - let importedCount = 0; - for (const model of models) { - const modelId = model.id || model.name || model.model; - if (!modelId) continue; - const resolvedAlias = resolveAlias(modelId); - if (!resolvedAlias) continue; - await onSetAlias(modelId, resolvedAlias, providerStorageAlias); - importedCount += 1; - } - if (importedCount === 0) { - alert("No new models were added."); - } + await onImportWithProgress( + // fetchModels callback + async () => { + const res = await fetch(`/api/providers/${activeConnection.id}/models`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to import models"); + return data; + }, + // processModel callback + async (model: any) => { + const modelId = model.id || model.name || model.model; + if (!modelId) return false; + const resolvedAlias = resolveAlias(modelId); + if (!resolvedAlias) return false; + await onSetAlias(modelId, resolvedAlias, providerStorageAlias); + return true; + } + ); } catch (error) { console.log("Error importing models:", error); } finally { @@ -1340,6 +1617,7 @@ CompatibleModelsSection.propTypes = { }) ).isRequired, isAnthropic: PropTypes.bool, + onImportWithProgress: PropTypes.func.isRequired, }; function CooldownTimer({ until }) { @@ -1757,9 +2035,11 @@ function AddApiKeyModal({ const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); const handleValidate = async () => { setValidating(true); + setSaveError(null); try { const res = await fetch("/api/providers/validate", { method: "POST", @@ -1779,6 +2059,7 @@ function AddApiKeyModal({ if (!provider || !formData.apiKey) return; setSaving(true); + setSaveError(null); try { let isValid = false; try { @@ -1798,12 +2079,20 @@ function AddApiKeyModal({ setValidating(false); } - await onSave({ + if (!isValid) { + setSaveError("API key validation failed. Please check your key and try again."); + return; + } + + const error = await onSave({ name: formData.name, apiKey: formData.apiKey, priority: formData.priority, - testStatus: isValid ? "active" : "unknown", + testStatus: "active", }); + if (error) { + setSaveError(typeof error === "string" ? error : "Failed to save connection"); + } } finally { setSaving(false); } @@ -1843,6 +2132,11 @@ function AddApiKeyModal({ {validationResult === "success" ? "Valid" : "Invalid"} )} + {saveError && ( +
+ {saveError} +
+ )} {isCompatible && (

{isAnthropic