diff --git a/file-size-baseline.json b/file-size-baseline.json index 0364b31ef0..d2ba7c8977 100644 --- a/file-size-baseline.json +++ b/file-size-baseline.json @@ -23,11 +23,11 @@ "open-sse/mcp-server/schemas/tools.ts": 1437, "open-sse/mcp-server/server.ts": 1457, "open-sse/mcp-server/tools/advancedTools.ts": 1118, - "open-sse/services/accountFallback.ts": 1645, + "open-sse/services/accountFallback.ts": 1708, "open-sse/services/batchProcessor.ts": 828, "open-sse/services/browserBackedChat.ts": 850, "open-sse/services/claudeCodeCompatible.ts": 1202, - "open-sse/services/combo.ts": 4955, + "open-sse/services/combo.ts": 5054, "open-sse/services/rateLimitManager.ts": 1017, "open-sse/services/tokenRefresh.ts": 1997, "open-sse/services/usage.ts": 3341, @@ -48,7 +48,7 @@ "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570, "src/app/(dashboard)/dashboard/health/page.tsx": 1091, "src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847, - "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 1377, + "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 782, "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 941, "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 843, "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1171, @@ -64,7 +64,7 @@ "src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 880, "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012, "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1072, - "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 981, + "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 983, "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1580, "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924, "src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016, @@ -101,12 +101,13 @@ "src/shared/constants/sidebarVisibility.ts": 990, "src/shared/services/cliRuntime.ts": 1084, "src/shared/validation/schemas.ts": 2515, - "src/sse/handlers/chat.ts": 1381, - "src/sse/services/auth.ts": 2198 + "src/sse/handlers/chat.ts": 1389, + "src/sse/services/auth.ts": 2207 }, "_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.", "_rebaseline_2026_06_11_phase1f": "Phase 1f (#3501): ProviderDetailPageClient.tsx 4948→4062 (-886 LOC); 3 novos hooks extraídos. useProviderConnections.ts=954 acima do cap=800 — justificado: extração direta do god-component (zero lógica nova), própria redução do cliente supera o custo. useProviderSettings.ts=263 e useProviderModels.ts=154 já abaixo do cap.", "_rebaseline_2026_06_12_review_issues": "Re-baseline consciente do /review-issues v3.8.23: 27 arquivos com crescimento herdado (v3.8.22 nunca reconciliado) + fixes deste round (combo.ts #3685, openai-to-gemini.ts #3688, tokenRefresh.ts #3692, validation/proxies de outras merges). providerLimits.ts (941) adicionado como frozen (split coeso de usage). Shrink endereçado separadamente pelo #3501.", "_rebaseline_2026_06_12_phase1g1j": "Phase 1g-1j (#3501): ProviderDetailPageClient.tsx 4063→3409 (extraídos ProviderPlaygroundPanel, useCommandCodeAuth, useExternalLinkFlow+ExternalLinkModal, useAuthFileHandlers — zero lógica nova). models/route.ts 2344→2426: drift do #3712 (vertex dynamic model discovery) reconciliado aqui.", - "_rebaseline_2026_06_12_phase1n1s": "Phase 1n-1s (#3501): ProviderDetailPageClient.tsx 2554→1376 (extraídos ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal + hooks/useApiKeySave + 4 helper closures→providerPageHelpers.ts). providerPageHelpers.ts 822→897 justificado: recebe 4 closures do god-component (getApiLabel/getApiDefaultPath/getApiPath/getHeaderIconProviderId), zero lógica nova, cliente encolhe mais do que helpers crescem." + "_rebaseline_2026_06_12_phase1n1s": "Phase 1n-1s (#3501): ProviderDetailPageClient.tsx 2554→1376 (extraídos ConnectionsListPanel, ConnectionsHeaderToolbar, ZedImportCard, BatchTestResultsModal, AdaptaTutorialModal + hooks/useApiKeySave + 4 helper closures→providerPageHelpers.ts). providerPageHelpers.ts 822→897 justificado: recebe 4 closures do god-component (getApiLabel/getApiDefaultPath/getApiPath/getHeaderIconProviderId), zero lógica nova, cliente encolhe mais do que helpers crescem.", + "_rebaseline_2026_06_12_phase1t": "Phase 1t (#3501): ProviderDetailPageClient.tsx 1377→782 — META ≤800 ATINGIDA (extraídos ProviderPageHeader, CompatibleNodeCard, ProviderModalsPanel, EmptyConnectionsPlaceholder, UpstreamProxyCard, SearchProviderCard + hooks useConnectionGate/useProviderNodeActions). Drift concorrente reconciliado: ResilienceTab/sse-chat/sse-auth/accountFallback/combo (merges #3629 model-lockout etc.)." } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index 422ae14dd6..61ecc980df 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -1,171 +1,55 @@ "use client"; -import { useState, useEffect, useCallback, useRef, useMemo } from "react"; -// Phase 1f extractions — Issue #3501 -import { useProviderConnections } from "./hooks/useProviderConnections"; -import { useProviderSettings } from "./hooks/useProviderSettings"; -import { useProviderModels } from "./hooks/useProviderModels"; -// Phase 1h: commandCode auth flow extracted to hooks/useCommandCodeAuth.ts -import { useCommandCodeAuth } from "./hooks/useCommandCodeAuth"; -// Phase 1i: external link flow extracted to hooks/useExternalLinkFlow.ts -import { useExternalLinkFlow } from "./hooks/useExternalLinkFlow"; -import ExternalLinkModal from "./components/ExternalLinkModal"; -// Phase 1j: auth file handlers extracted to hooks/useAuthFileHandlers.ts -import { useAuthFileHandlers } from "./hooks/useAuthFileHandlers"; -// Phase 1g: ProviderPlaygroundPanel + helpers extracted to components/ProviderPlaygroundPanel.tsx -import ProviderPlaygroundPanel from "./components/ProviderPlaygroundPanel"; -import { useNotificationStore } from "@/store/notificationStore"; -import { useParams, useRouter } from "next/navigation"; +// Issue #3501 strangler-fig decomposition — Phase 1t (final push) +import { useState, useEffect, useCallback, useMemo } from "react"; +import { useParams } from "next/navigation"; import Link from "next/link"; import { useTranslations } from "next-intl"; +import { Card, Button, CardSkeleton, NoAuthProviderCard, NoAuthAccountCard } from "@/shared/components"; import { - Card, - Button, - Badge, - Modal, - ConfirmModal, - CardSkeleton, - OAuthModal, - KiroOAuthWrapper, - CursorAuthModal, - TraeAuthModal, - Toggle, - Select, - ProxyConfigModal, - NoAuthProviderCard, - NoAuthAccountCard, -} from "@/shared/components"; -import { - LOCAL_PROVIDERS, NOAUTH_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isClaudeCodeCompatibleProvider, - isSelfHostedChatProvider, supportsApiKeyOnFreeProvider, - // providerAllowsOptionalApiKey + supportsBulkApiKey used by extracted AddApiKeyModal } from "@/shared/constants/providers"; -// antigravityClientProfile + parseBulkApiKeys used by extracted modals (AddApiKeyModal, EditConnectionModal) import { getModelsByProviderId } from "@/shared/constants/models"; -import { - compatibleProviderSupportsModelImport, - getCompatibleFallbackModels, -} from "@/lib/providers/managedAvailableModels"; -import { - matchesModelCatalogQuery, - normalizeModelCatalogSource, -} from "@/shared/utils/modelCatalogSearch"; +import { compatibleProviderSupportsModelImport, getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels"; +import { normalizeModelCatalogSource } from "@/shared/utils/modelCatalogSearch"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; -import { pickDisplayValue } from "@/shared/utils/maskEmail"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; -import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle"; -import ProviderIcon from "@/shared/components/ProviderIcon"; -import { type CodexServiceTier } from "@/lib/providers/requestDefaults"; -import { type CodexGlobalServiceMode } from "@/lib/providers/codexFastTier"; -// parseExtraApiKeys used by extracted EditConnectionModal -import { compareTr } from "@/shared/utils/turkishText"; -import RiskNoticeModal from "../components/RiskNoticeModal"; -import CodexCliGuideModal from "../components/CodexCliGuideModal"; -import { isRiskAcknowledged, useRiskAcknowledged } from "../hooks/useRiskAcknowledged"; +import { useNotificationStore } from "@/store/notificationStore"; import { resolveDashboardProviderInfo } from "../providerPageUtils"; -// webSessionCredentials used by extracted modals (AddApiKeyModal, EditConnectionModal) -import { - ImportCodexAuthModal, - ApplyCodexAuthModal, -} from "./components/modals/ImportCodexAuthModal"; -import { - ImportClaudeAuthModal, - ApplyClaudeAuthModal, -} from "./components/modals/ImportClaudeAuthModal"; -import { - ImportGeminiAuthModal, - ApplyGeminiAuthModal, -} from "./components/modals/ImportGeminiAuthModal"; - -import EditCompatibleNodeModal from "./components/modals/EditCompatibleNodeModal"; -import AddApiKeyModal from "./components/modals/AddApiKeyModal"; -import EditConnectionModal from "./components/modals/EditConnectionModal"; -import WebSessionCredentialGuide from "./components/WebSessionCredentialGuide"; -// Phase 1d extractions — Issue #3501 -import ConnectionRow, { - type ConnectionRowConnection, -} from "./components/ConnectionRow"; -import ModelCompatPopover from "./components/ModelCompatPopover"; -import SiliconFlowEndpointModal from "./components/SiliconFlowEndpointModal"; -// Phase 1k extractions — Issue #3501 +import { type ConnectionRowConnection } from "./components/ConnectionRow"; +import { useProviderConnections } from "./hooks/useProviderConnections"; +import { useProviderSettings } from "./hooks/useProviderSettings"; +import { useProviderModels } from "./hooks/useProviderModels"; +import { useCommandCodeAuth } from "./hooks/useCommandCodeAuth"; +import { useExternalLinkFlow } from "./hooks/useExternalLinkFlow"; +import { useAuthFileHandlers } from "./hooks/useAuthFileHandlers"; import { useModelImportHandlers } from "./hooks/useModelImportHandlers"; -// Phase 1s extractions — Issue #3501 import { useApiKeySave } from "./hooks/useApiKeySave"; -import ImportProgressModal from "./components/ImportProgressModal"; -// Phase 1l extractions — Issue #3501 import { useModelVisibilityHandlers } from "./hooks/useModelVisibilityHandlers"; -// Phase 1m extractions — Issue #3501 -import ProviderModelsSection from "./components/ProviderModelsSection"; -import { - // CONFIGURABLE_BASE_URL_PROVIDERS, DEFAULT_PROVIDER_BASE_URLS, getLocalProviderMetadata, - // isBaseUrlConfigurableProvider, getProviderBaseUrlDefault, getProviderBaseUrlHint, - // getProviderBaseUrlPlaceholder, isGlmProvider, parseRoutingTagsInput, parseExcludedModelsInput, - // formatRoutingTagsInput, formatExcludedModelsInput, getWebSessionCredentialLabel, - // getWebSessionCredentialHint, getWebSessionCredentialCheckLabel, getAddCredentialModalTitle, - // CODEX_REASONING_STRENGTH_OPTIONS, CODEX_ACCOUNT_SERVICE_TIER_VALUES, getCodexRequestDefaults, - // getClaudeCodeCompatibleRequestDefaults, extractCommandCodeCredentialInput, - // normalizeAndValidateHttpBaseUrl, formatTimeAgo - // — all moved to extracted modals (AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide) - providerText, - providerCountText, - readBooleanToggle, - // formatProviderModelsErrorResponse → hooks/useModelVisibilityHandlers.ts (Phase 1l) - type ProviderMessageTranslator, - type LocalProviderMetadata, - // CommandCodeAuthFlowState moved to hooks/useCommandCodeAuth.ts (Phase 1h) - // CompatByProtocolMap, CompatModelRow, CompatModelMap → hooks/useModelVisibilityHandlers.ts (Phase 1l) - // Phase 1s: pure helpers extracted from god-component closures - getApiLabel, - getApiDefaultPath, - getApiPath, - getHeaderIconProviderId, -} from "./providerPageHelpers"; -// CODEX_GLOBAL_SERVICE_MODE_VALUES, getCodexServiceTierLabel, normalizeCodexLimitPolicy -// moved to hooks/useProviderSettings.ts + hooks/useProviderConnections.ts (Phase 1f) -// Phase 1e extractions — Issue #3501 import { useModelCompatState } from "./hooks/useModelCompatState"; -import ModelRow, { ModelVisibilityToolbar } from "./components/ModelRow"; -import PassthroughModelsSection from "./components/PassthroughModelsSection"; +import { useConnectionGate } from "./hooks/useConnectionGate"; +import { useProviderNodeActions } from "./hooks/useProviderNodeActions"; +import ProviderPlaygroundPanel from "./components/ProviderPlaygroundPanel"; +import ProviderModelsSection from "./components/ProviderModelsSection"; import CustomModelsSection from "./components/CustomModelsSection"; -import CompatibleModelsSection from "./components/CompatibleModelsSection"; import ConnectionsListPanel from "./components/ConnectionsListPanel"; -// Phase 1o extractions — Issue #3501 import ConnectionsHeaderToolbar from "./components/ConnectionsHeaderToolbar"; -// Phase 1p extractions — Issue #3501 import ZedImportCard from "./components/ZedImportCard"; -// Phase 1q extractions — Issue #3501 -import BatchTestResultsModal from "./components/BatchTestResultsModal"; -// Phase 1r extractions — Issue #3501 -import { AdaptaTutorialModal } from "./components/AdaptaTutorialModal"; -// recordToHeaderRows moved to components/ModelCompatPopover.tsx (Phase 1d) -// buildCompatMap, isModelHidden*, effectiveNormalize/Preserve*, anyNormalize/NoPreserveCompatBadge -// moved to providerPageHelpers.ts + hook useModelCompatState (Phase 1e) -// formatProviderModelsErrorResponse moved to providerPageHelpers.ts (Phase 1e) - -// ModelCompatSavePatch → hooks/useModelVisibilityHandlers.ts (Phase 1l) -// MAX_BULK_IDS moved to hooks/useProviderConnections.ts (Phase 1f) -// ModelRowProps, PassthroughModelRowProps → components/ModelRow.tsx, PassthroughModelRow.tsx (Phase 1e) -// PassthroughModelsSectionProps → components/PassthroughModelsSection.tsx (Phase 1e) -// CustomModelsSectionProps → components/CustomModelsSection.tsx (Phase 1e) -// CompatibleModelsSectionProps → components/CompatibleModelsSection.tsx (Phase 1e) -// CooldownTimerProps moved to components/ConnectionRow.tsx (Phase 1d) - -// getModelSourceBadgeClass + ModelSourceBadge → components/ModelRow.tsx (Phase 1e) -// ConnectionRowConnection, ConnectionRowProps moved to components/ConnectionRow.tsx (Phase 1d) - -// ModelCompatPopover extracted to components/ModelCompatPopover.tsx (Phase 1d) - -// ── ProviderPlaygroundPanel extracted to components/ProviderPlaygroundPanel.tsx (Phase 1g) ── +import ProviderPageHeader from "./components/ProviderPageHeader"; +import CompatibleNodeCard from "./components/CompatibleNodeCard"; +import ProviderModalsPanel from "./components/ProviderModalsPanel"; +import EmptyConnectionsPlaceholder from "./components/EmptyConnectionsPlaceholder"; +import UpstreamProxyCard from "./components/UpstreamProxyCard"; +import SearchProviderCard from "./components/SearchProviderCard"; +// providerText used by UpstreamProxyCard (Phase 1t.7) export default function ProviderDetailPageClient() { const params = useParams(); - const router = useRouter(); const providerId = params.id as string; // ── UI-only modal state (not owned by hooks) ───────────────────────────── @@ -174,7 +58,6 @@ export default function ProviderDetailPageClient() { const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); const [showSiliconFlowEndpointModal, setShowSiliconFlowEndpointModal] = useState(false); const [siliconFlowInitialBaseUrl, setSiliconFlowInitialBaseUrl] = useState(); - const [showRiskNoticeModal, setShowRiskNoticeModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showEditNodeModal, setShowEditNodeModal] = useState(false); const [showTutorialModal, setShowTutorialModal] = useState(false); @@ -184,9 +67,6 @@ export default function ProviderDetailPageClient() { const [codexCliGuideOpen, setCodexCliGuideOpen] = useState(false); const [importClaudeModalOpen, setImportClaudeModalOpen] = useState(false); const [importGeminiModalOpen, setImportGeminiModalOpen] = useState(false); - const pendingRiskActionRef = useRef<(() => void) | null>(null); - const { acknowledged: riskAcknowledged, acknowledge: acknowledgeRisk } = - useRiskAcknowledged(providerId); const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isCcCompatible = isClaudeCodeCompatibleProvider(providerId); const isCommandCode = providerId === "command-code"; @@ -221,7 +101,6 @@ export default function ProviderDetailPageClient() { setSelectedIds, setBatchDeleteConfirmOpen, setBatchTestResults, - setConnections, setProviderNode, fetchConnections, fetchProxyConfig, @@ -286,7 +165,6 @@ export default function ProviderDetailPageClient() { externalLinkModalOpen, setExternalLinkModalOpen, externalLinkUrl, - externalLinkToken, externalLinkLoading, externalLinkError, externalLinkCopied, @@ -310,6 +188,11 @@ export default function ProviderDetailPageClient() { const providerSupportsOAuth = providerInfo?.toggleAuthType === "oauth" || providerInfo?.toggleAuthType === "free"; const subscriptionRisk = providerInfo?.subscriptionRisk === true; + + // ── Phase 1t.3: connection gate + risk-notice modal state ─────────────── + const { showRiskNoticeModal, gateConnectionFlow, handleConfirmRiskNotice, handleCancelRiskNotice } = + useConnectionGate({ providerId, subscriptionRisk }); + const providerSupportsPat = supportsApiKeyOnFreeProvider(providerId); const isOAuth = providerSupportsOAuth && !providerSupportsPat; const isFreeNoAuth = NOAUTH_PROVIDERS[providerId]?.noAuth === true; @@ -367,7 +250,6 @@ export default function ProviderDetailPageClient() { togglingAutoSync, canImportModels, isAutoSyncEnabled, - autoSyncConnection, setShowImportModal, setImportProgress, handleImportModels, @@ -389,17 +271,6 @@ export default function ProviderDetailPageClient() { providerStorageAlias, }); - // fetchAliases, handleSetAlias, handleDeleteAlias → hooks/useProviderModels.ts (Phase 1f) - // fetchProviderModelMeta, fetchProxyConfig, fetchConnections → hooks/useProviderConnections.ts + useProviderModels.ts (Phase 1f) - // loadCodexSettings, loadClaudeRoutingSettings → hooks/useProviderSettings.ts (Phase 1f) - // loadConnProxies, handleRetestConnection, handleBatchTestAll, handleBatchRetest → hooks/useProviderConnections.ts (Phase 1f) - // handleDelete, handleBatchDeleteConfirm, handleBatchSetActive → hooks/useProviderConnections.ts (Phase 1f) - // handleUpdateConnectionStatus, handleToggleProxyEnabled, handleTogglePerKeyProxyEnabled → hooks/useProviderConnections.ts (Phase 1f) - // handleDistributeProxies, handleToggleRateLimit, handleToggleClaudeExtraUsage → hooks/useProviderConnections.ts (Phase 1f) - // handleToggleCliproxyapiMode, handleToggleCodexLimit, handleSwapPriority → hooks/useProviderConnections.ts (Phase 1f) - // handleToggleClaudeRoutingPreference, handleChangeCodexGlobalServiceMode → hooks/useProviderSettings.ts (Phase 1f) - // handleRefreshToken → hooks/useProviderConnections.ts (Phase 1f) - // ── model-related effects (loading gate) ──────────────────────────────── useEffect(() => { if (loading || isSearchProvider) return; @@ -407,33 +278,6 @@ export default function ProviderDetailPageClient() { fetchAliases(); }, [loading, isSearchProvider, fetchProviderModelMeta, fetchAliases]); - const handleUpdateNode = async (formData: any) => { - try { - const res = await fetch(`/api/provider-nodes/${providerId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - const data = await res.json(); - if (res.ok) { - setProviderNode(data.node); - await fetchConnections(); - setShowEditNodeModal(false); - } - } catch (error) { - console.log("Error updating provider node:", error); - } - }; - - // loadCodexSettings, loadClaudeRoutingSettings → hooks/useProviderSettings.ts (Phase 1f) - // loadConnProxies → hooks/useProviderConnections.ts (Phase 1f) - // onTestModel, handleTestAll, saveModelCompatFlags, handleToggleModelHidden, - // handleBulkToggleModelHidden, handleClearAllModels, providerAliasEntries - // → hooks/useModelVisibilityHandlers.ts (Phase 1l) - - // handleToggleSelectOne/All, handleBatchDeleteOpenModal/Confirm, handleDelete, - // handleBatchSetActive → hooks/useProviderConnections.ts (Phase 1f) - const handleOAuthSuccess = useCallback(() => { fetchConnections(); setShowOAuthModal(false); @@ -455,37 +299,10 @@ export default function ProviderDetailPageClient() { openApiKeyAddFlow(); }, [isOAuth, openApiKeyAddFlow]); - const gateConnectionFlow = useCallback( - (callback: () => void) => { - if (subscriptionRisk && !riskAcknowledged && !isRiskAcknowledged(providerId)) { - pendingRiskActionRef.current = callback; - setShowRiskNoticeModal(true); - return; - } - callback(); - }, - [providerId, riskAcknowledged, subscriptionRisk] - ); - - const handleConfirmRiskNotice = useCallback(() => { - acknowledgeRisk(); - setShowRiskNoticeModal(false); - const pendingAction = pendingRiskActionRef.current; - pendingRiskActionRef.current = null; - pendingAction?.(); - }, [acknowledgeRisk]); - - const handleCancelRiskNotice = useCallback(() => { - pendingRiskActionRef.current = null; - setShowRiskNoticeModal(false); - }, []); - // ── Phase 1h: commandCode auth flow ───────────────────────────────────── const { commandCodeAuthState, - clearCommandCodeAuthTimer, handleCloseAddApiKeyModal, - handleCommandCodeAuthApply, handleStartCommandCodeAuth, handleOpenCommandCodeConnect, } = useCommandCodeAuth({ @@ -509,54 +326,18 @@ export default function ProviderDetailPageClient() { t, }); - const handleUpdateConnection = async (formData) => { - try { - const res = await fetch(`/api/providers/${selectedConnection.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); - if (res.ok) { - await fetchConnections(); - setShowEditModal(false); - return null; - } - const data = await res.json().catch(() => ({})); - return data.error?.message || data.error || t("failedSaveConnection"); - } catch (error) { - console.log("Error updating connection:", error); - return t("failedSaveConnectionRetry"); - } - }; + // ── Phase 1t.4: node/connection update handlers ────────────────────────── + const { handleUpdateNode, handleUpdateConnection } = useProviderNodeActions({ + providerId, + fetchConnections, + selectedConnection, + setProviderNode, + setShowEditNodeModal, + setShowEditModal, + t, + }); - // handleUpdateConnectionStatus, handleToggleProxyEnabled, handleTogglePerKeyProxyEnabled, - // handleDistributeProxies, handleToggleRateLimit, handleToggleClaudeExtraUsage, - // handleToggleCliproxyapiMode, handleToggleCodexLimit, handleToggleClaudeRoutingPreference, - // handleChangeCodexGlobalServiceMode, handleRetestConnection, runBatchTest, - // handleBatchTestAll, handleBatchRetest, parseApiErrorMessage, getAttachmentFilename, - // handleRefreshToken → hooks/useProviderConnections.ts + useProviderSettings.ts (Phase 1f) - - // handleToggleProxyEnabled → useProviderConnections (Phase 1f) - - // handleTogglePerKeyProxyEnabled → useProviderConnections (Phase 1f) - - // handleDistributeProxies → useProviderConnections (Phase 1f) - - // handleToggleRateLimit → useProviderConnections (Phase 1f) - - // handleToggleClaudeExtraUsage → useProviderConnections (Phase 1f) - - // [cpaProviderEnabled] state + useEffect + handleToggleCliproxyapiMode → useProviderConnections (Phase 1f) - - // handleToggleCodexLimit → useProviderConnections (Phase 1f) - - // handleToggleClaudeRoutingPreference + handleChangeCodexGlobalServiceMode → useProviderSettings (Phase 1f) - - // handleRetestConnection, runBatchTest, handleBatchTestAll, handleBatchRetest, - // [refreshingId], parseApiErrorMessage, getAttachmentFilename, handleRefreshToken - // → useProviderConnections (Phase 1f) - - // Phase 1j: auth file handlers extracted to hooks/useAuthFileHandlers.ts + // Phase 1j: auth file handlers const { applyingCodexAuthId, applyCodexModalConnectionId, @@ -578,16 +359,12 @@ export default function ProviderDetailPageClient() { handleExportGeminiAuthFile, } = useAuthFileHandlers({ parseApiErrorMessage, getAttachmentFilename, notify, t }); - // handleSwapPriority → useProviderConnections (Phase 1f) - // handleImportModels, handleCompatibleImportWithProgress, handleToggleAutoSync, - // canImportModels, isAutoSyncEnabled, autoSyncConnection → hooks/useModelImportHandlers.ts (Phase 1k) - - // Phase 1e: compat-state derivations moved to useModelCompatState hook. + // Phase 1e: compat-state derivations const compat = useModelCompatState( modelMeta.customModels, modelMeta.modelCompatOverrides ); - const { customMap, overrideMap } = compat; + const { customMap } = compat; const effectiveModelNormalize = compat.effectiveModelNormalize; const effectiveModelPreserveDeveloper = compat.effectiveModelPreserveDeveloper; const effectiveModelHidden = compat.isModelHidden; @@ -659,129 +436,32 @@ export default function ProviderDetailPageClient() { return (
- {/* Header */} -
- - arrow_back - {t("backToProviders")} - -
-
- -
-
- {providerInfo.website ? ( - - {providerInfo.name} - open_in_new - - ) : ( -

{providerInfo.name}

- )} -
-

- {t("connectionCountLabel", { count: connections.length })} -

- - {providerId === "adapta-web" && ( - - )} -
-
-
-
+ {/* Header — Phase 1t.1: extracted to components/ProviderPageHeader.tsx */} + setShowTutorialModal(true)} + t={t} + /> {providerId === "zed" && } + {/* CompatibleNodeCard — Phase 1t.2: extracted to components/CompatibleNodeCard.tsx */} {isCompatible && providerNode && ( - -
-
-

- {isCcCompatible - ? t("ccCompatibleDetailsTitle") - : isAnthropicCompatible - ? t("anthropicCompatibleDetails") - : t("openaiCompatibleDetails")} -

-

- {getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/{getApiPath(isCcCompatible, isAnthropicCompatible, providerNode?.apiType, providerNode?.chatPath)} -

-
-
- - - -
-
- {isCcCompatible && ( -
-
- - warning - -

{t("ccCompatibleValidationHint")}

-
-
- )} -
+ setShowEditNodeModal(true)} + t={t} + /> )} {/* Connections */} @@ -849,88 +529,23 @@ export default function ProviderDetailPageClient() { /> {connections.length === 0 ? ( -
-
- - {isOAuth ? "lock" : "key"} - -
-

{t("noConnectionsYet")}

-

{t("addFirstConnectionHint")}

- {!isCompatible && ( -
- {isCommandCode ? ( - <> - - - - ) : ( - <> - - {providerId === "qoder" && ( - - )} - {providerId === "codex" && ( - - )} - {providerId === "claude" && ( - - )} - {providerId === "gemini-cli" && ( - - )} - - )} -
- )} -
+ setShowOAuthModal(true)} + onOpenImportCodex={() => setImportCodexModalOpen(true)} + onOpenImportClaude={() => setImportClaudeModalOpen(true)} + onOpenImportGemini={() => setImportGeminiModalOpen(true)} + t={t} + /> ) : ( )} - {isUpstreamProxyProvider && ( - -
-
-

- {providerText( - t, - "upstreamProxyManagedTitle", - "Managed via Upstream Proxy Settings" - )} -

-

- {providerText( - t, - "upstreamProxyManagedDescription", - "CLIProxyAPI is configured as an upstream proxy layer, not as a direct provider connection. Manage the binary/runtime in CLI Tools and enable proxy routing on each provider via the provider proxy controls." - )} -

-
-
- - terminal - {t("openCliTools")} - - - settings - {t("openSettings")} - -
-
-
- )} + {isUpstreamProxyProvider && } {/* Models — hidden for search providers (they don't have models) */} {!isSearchProvider && !isUpstreamProxyProvider && ( @@ -1111,266 +689,93 @@ export default function ProviderDetailPageClient() { )} {/* Search provider info */} - {isSearchProvider && ( - -

{t("searchProvider")}

-

{t("searchProviderDesc")}

- {providerId === "perplexity-search" && ( -
- link -

{t("perplexitySearchSharedKeyInfo")}

-
- )} - {providerId === "google-pse-search" && ( -
- tune -

{t("googlePseInfo")}

-
- )} - {providerId === "searxng-search" && ( -
- dns -

{t("searxngInfo")}

-
- )} -
- )} + {isSearchProvider && } {/* Playground panel — rendered for providers that declare serviceKinds */} - {/* Modals */} - {showRiskNoticeModal && subscriptionRisk && ( - - )} - {!isUpstreamProxyProvider && - (providerId === "kiro" || providerId === "amazon-q" ? ( - { - setShowOAuthModal(false); - }} - /> - ) : providerId === "cursor" ? ( - { - setShowOAuthModal(false); - }} - /> - ) : providerId === "trae" ? ( - { - setShowOAuthModal(false); - }} - /> - ) : ( - { - setShowOAuthModal(false); - }} - /> - ))} - {providerId === "siliconflow" && ( - { - setSiliconFlowInitialBaseUrl(baseUrl); - setShowSiliconFlowEndpointModal(false); - setShowAddApiKeyModal(true); - }} - onClose={() => { - setShowSiliconFlowEndpointModal(false); - setSiliconFlowInitialBaseUrl(undefined); - }} - /> - )} - {!isUpstreamProxyProvider && ( - - )} - setBatchDeleteConfirmOpen(false)} - onConfirm={handleBatchDeleteConfirm} - title={t("batchDeleteConfirmTitle", "Delete connections")} - message={t("batchDeleteConfirm", { count: selectedIds.size })} - confirmText={t("batchDeleteConfirmButton", "Delete")} - cancelText={t("cancel", "Cancel")} - loading={batchDeleting} - /> - {providerId === "codex" && applyCodexModalConnectionId && ( - setApplyCodexModalConnectionId(null)} - /> - )} - {!isUpstreamProxyProvider && ( - setShowEditModal(false)} - /> - )} - {!isUpstreamProxyProvider && isCompatible && ( - setShowEditNodeModal(false)} - isAnthropic={isAnthropicProtocolCompatible} - isCcCompatible={isCcCompatible} - /> - )} - {/* Codex CLI Guide Modal */} - setCodexCliGuideOpen(false)} /> - {/* Codex Import Auth Modal */} - {providerId === "codex" && importCodexModalOpen && ( - setImportCodexModalOpen(false)} - onSuccess={() => { - setImportCodexModalOpen(false); - void fetchConnections(); - }} - /> - )} - {providerId === "codex" && externalLinkModalOpen && ( - setExternalLinkModalOpen(false)} - loading={externalLinkLoading} - error={externalLinkError} - url={externalLinkUrl} - copied={externalLinkCopied} - onCopy={externalLinkCopy} - /> - )} - {/* Claude Apply Auth Modal */} - {providerId === "claude" && applyClaudeModalConnectionId && ( - setApplyClaudeModalConnectionId(null)} - /> - )} - {/* Claude Import Auth Modal */} - {providerId === "claude" && importClaudeModalOpen && ( - setImportClaudeModalOpen(false)} - onSuccess={() => { - setImportClaudeModalOpen(false); - void fetchConnections(); - }} - /> - )} - {/* Gemini Apply Auth Modal */} - {providerId === "gemini-cli" && applyGeminiModalConnectionId && ( - setApplyGeminiModalConnectionId(null)} - /> - )} - {/* Gemini Import Auth Modal */} - {providerId === "gemini-cli" && importGeminiModalOpen && ( - setImportGeminiModalOpen(false)} - onSuccess={() => { - setImportGeminiModalOpen(false); - void fetchConnections(); - }} - /> - )} - {/* Batch Test Results Modal */} - setBatchTestResults(null)} - t={t} - /> - {/* Proxy Config Modal */} - {proxyTarget && ( - setProxyTarget(null)} - level={proxyTarget.level} - levelId={proxyTarget.id} - levelLabel={proxyTarget.label} - onSaved={() => { - void fetchProxyConfig(); - }} - /> - )} - {/* Import Progress Modal — Phase 1k: extracted to components/ImportProgressModal.tsx */} - { - if (importProgress.phase === "done" || importProgress.phase === "error") { - setShowImportModal(false); - } - }} + showImportModal={showImportModal} + setShowImportModal={setShowImportModal} + showTutorialModal={showTutorialModal} + setShowTutorialModal={setShowTutorialModal} t={t} /> - - {/* Adapta Web — Tutorial Modal */} - {providerId === "adapta-web" && ( - setShowTutorialModal(false)} - /> - )}
); } -// ModelRow, ModelVisibilityToolbar, PassthroughModelsSection, PassthroughModelRow, -// CustomModelsSection, CompatibleModelsSection → components/ (Phase 1e — Issue #3501) - -// Phase 1d: CooldownTimer, inferErrorType, getStatusPresentation, ConnectionRow → components/ConnectionRow.tsx -// Phase 1d: ModelCompatPopover, recordToHeaderRows → components/ModelCompatPopover.tsx -// Phase 1d: SiliconFlowEndpointModal → components/SiliconFlowEndpointModal.tsx diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx new file mode 100644 index 0000000000..1b01f2eb7d --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx @@ -0,0 +1,121 @@ +"use client"; + +// Phase 1t.2 extraction — Issue #3501 +import { useRouter } from "next/navigation"; +import { Card, Button } from "@/shared/components"; +import { getApiLabel, getApiPath } from "../providerPageHelpers"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface ProviderNode { + baseUrl?: string; + apiType?: string; + chatPath?: string; + prefix?: string; + [key: string]: unknown; +} + +interface CompatibleNodeCardProps { + providerId: string; + providerNode: ProviderNode; + isCcCompatible: boolean; + isAnthropicCompatible: boolean; + isAnthropicProtocolCompatible: boolean; + gateConnectionFlow: (callback: () => void) => void; + openApiKeyAddFlow: () => void; + onOpenEditNodeModal: () => void; + t: ProviderMessageTranslator; +} + +export default function CompatibleNodeCard({ + providerId, + providerNode, + isCcCompatible, + isAnthropicCompatible, + isAnthropicProtocolCompatible, + gateConnectionFlow, + openApiKeyAddFlow, + onOpenEditNodeModal, + t, +}: CompatibleNodeCardProps) { + const router = useRouter(); + + return ( + +
+
+

+ {isCcCompatible + ? t("ccCompatibleDetailsTitle") + : isAnthropicCompatible + ? t("anthropicCompatibleDetails") + : t("openaiCompatibleDetails")} +

+

+ {getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} ·{" "} + {(providerNode.baseUrl || "").replace(/\/$/, "")}/ + {getApiPath( + isCcCompatible, + isAnthropicCompatible, + providerNode?.apiType, + providerNode?.chatPath + )} +

+
+
+ + + +
+
+ {isCcCompatible && ( +
+
+ + warning + +

{t("ccCompatibleValidationHint")}

+
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/EmptyConnectionsPlaceholder.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/EmptyConnectionsPlaceholder.tsx new file mode 100644 index 0000000000..79d073ecf3 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/EmptyConnectionsPlaceholder.tsx @@ -0,0 +1,131 @@ +"use client"; + +// Phase 1t.6 extraction — Issue #3501 +import { Button } from "@/shared/components"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface CommandCodeAuthState { + phase: string; + [key: string]: unknown; +} + +interface EmptyConnectionsPlaceholderProps { + isOAuth: boolean; + isCompatible: boolean; + isCommandCode: boolean; + providerId: string; + providerSupportsPat: boolean; + commandCodeAuthState: CommandCodeAuthState; + gateConnectionFlow: (callback: () => void) => void; + openApiKeyAddFlow: () => void; + openPrimaryAddFlow: () => void; + handleOpenCommandCodeConnect: () => void; + onOpenOAuthModal: () => void; + onOpenImportCodex: () => void; + onOpenImportClaude: () => void; + onOpenImportGemini: () => void; + t: ProviderMessageTranslator; +} + +export default function EmptyConnectionsPlaceholder({ + isOAuth, + isCompatible, + isCommandCode, + providerId, + providerSupportsPat, + commandCodeAuthState, + gateConnectionFlow, + openApiKeyAddFlow, + openPrimaryAddFlow, + handleOpenCommandCodeConnect, + onOpenOAuthModal, + onOpenImportCodex, + onOpenImportClaude, + onOpenImportGemini, + t, +}: EmptyConnectionsPlaceholderProps) { + return ( +
+
+ + {isOAuth ? "lock" : "key"} + +
+

{t("noConnectionsYet")}

+

{t("addFirstConnectionHint")}

+ {!isCompatible && ( +
+ {isCommandCode ? ( + <> + + + + ) : ( + <> + + {providerId === "qoder" && ( + + )} + {providerId === "codex" && ( + + )} + {providerId === "claude" && ( + + )} + {providerId === "gemini-cli" && ( + + )} + + )} +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx new file mode 100644 index 0000000000..7959d7e431 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx @@ -0,0 +1,431 @@ +"use client"; + +// Phase 1t.5 extraction — Issue #3501 +// Pure composition of all modal elements rendered by ProviderDetailPageClient. +import { ConfirmModal, OAuthModal, KiroOAuthWrapper, CursorAuthModal, TraeAuthModal, ProxyConfigModal } from "@/shared/components"; +import RiskNoticeModal from "../../components/RiskNoticeModal"; +import CodexCliGuideModal from "../../components/CodexCliGuideModal"; +import SiliconFlowEndpointModal from "./SiliconFlowEndpointModal"; +import AddApiKeyModal from "./modals/AddApiKeyModal"; +import EditConnectionModal from "./modals/EditConnectionModal"; +import EditCompatibleNodeModal from "./modals/EditCompatibleNodeModal"; +import ExternalLinkModal from "./ExternalLinkModal"; +import BatchTestResultsModal from "./BatchTestResultsModal"; +import ImportProgressModal from "./ImportProgressModal"; +import { AdaptaTutorialModal } from "./AdaptaTutorialModal"; +import { + ImportCodexAuthModal, + ApplyCodexAuthModal, +} from "./modals/ImportCodexAuthModal"; +import { + ImportClaudeAuthModal, + ApplyClaudeAuthModal, +} from "./modals/ImportClaudeAuthModal"; +import { + ImportGeminiAuthModal, + ApplyGeminiAuthModal, +} from "./modals/ImportGeminiAuthModal"; +import { type ConnectionRowConnection } from "./ConnectionRow"; +import { type BatchTestResults } from "../hooks/useProviderConnections"; +import { type ImportProgress } from "../hooks/useModelImportHandlers"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface ProviderInfo { + name: string; + riskNoticeVariant?: string; + [key: string]: unknown; +} + +interface ProxyTarget { + level: string; + id: string; + label: string; +} + +interface ProviderModalsPanelProps { + providerId: string; + providerInfo: ProviderInfo; + isCompatible: boolean; + isAnthropicProtocolCompatible: boolean; + isCcCompatible: boolean; + isCommandCode: boolean; + isUpstreamProxyProvider: boolean; + subscriptionRisk: boolean; + // Risk notice + showRiskNoticeModal: boolean; + handleConfirmRiskNotice: () => void; + handleCancelRiskNotice: () => void; + // OAuth + showOAuthModal: boolean; + reauthConnection: ConnectionRowConnection | null; + handleOAuthSuccess: () => void; + setShowOAuthModal: (show: boolean) => void; + // SiliconFlow + showSiliconFlowEndpointModal: boolean; + setSiliconFlowInitialBaseUrl: (url: string | undefined) => void; + setShowSiliconFlowEndpointModal: (open: boolean) => void; + setShowAddApiKeyModal: (open: boolean) => void; + // AddApiKey + showAddApiKeyModal: boolean; + siliconFlowInitialBaseUrl: string | undefined; + commandCodeAuthState: { phase: string; [key: string]: unknown }; + handleStartCommandCodeAuth: () => void; + handleSaveApiKey: (data: any) => Promise; + handleCloseAddApiKeyModal: () => void; + // Batch delete confirm + batchDeleteConfirmOpen: boolean; + setBatchDeleteConfirmOpen: (open: boolean) => void; + handleBatchDeleteConfirm: () => void; + selectedIds: Set; + batchDeleting: boolean; + // Codex auth + applyCodexModalConnectionId: string | null; + setApplyCodexModalConnectionId: (id: string | null) => void; + applyingCodexAuthId: string | null; + handleApplyCodexAuthLocal: (id: string) => Promise; + importCodexModalOpen: boolean; + setImportCodexModalOpen: (open: boolean) => void; + fetchConnections: () => Promise; + // External link + externalLinkModalOpen: boolean; + setExternalLinkModalOpen: (open: boolean) => void; + externalLinkLoading: boolean; + externalLinkError: string | null; + externalLinkUrl: string | null; + externalLinkCopied: boolean; + externalLinkCopy: () => void; + // Edit connection + showEditModal: boolean; + setShowEditModal: (open: boolean) => void; + selectedConnection: { id: string } | null; + handleUpdateConnection: (data: any) => Promise; + // Edit compatible node + showEditNodeModal: boolean; + setShowEditNodeModal: (open: boolean) => void; + providerNode: any; + handleUpdateNode: (data: any) => Promise; + // Codex CLI guide + codexCliGuideOpen: boolean; + setCodexCliGuideOpen: (open: boolean) => void; + // Claude auth + applyClaudeModalConnectionId: string | null; + setApplyClaudeModalConnectionId: (id: string | null) => void; + applyingClaudeAuthId: string | null; + handleApplyClaudeAuthLocal: (id: string) => Promise; + importClaudeModalOpen: boolean; + setImportClaudeModalOpen: (open: boolean) => void; + // Gemini auth + applyGeminiModalConnectionId: string | null; + setApplyGeminiModalConnectionId: (id: string | null) => void; + applyingGeminiAuthId: string | null; + handleApplyGeminiAuthLocal: (id: string) => Promise; + importGeminiModalOpen: boolean; + setImportGeminiModalOpen: (open: boolean) => void; + // Batch test results + batchTestResults: BatchTestResults | null; + setBatchTestResults: (r: BatchTestResults | null) => void; + emailsVisible: boolean; + // Proxy config + proxyTarget: ProxyTarget | null; + setProxyTarget: (t: ProxyTarget | null) => void; + fetchProxyConfig: () => Promise; + // Import progress + importProgress: ImportProgress; + showImportModal: boolean; + setShowImportModal: (open: boolean) => void; + // Tutorial + showTutorialModal: boolean; + setShowTutorialModal: (open: boolean) => void; + t: ProviderMessageTranslator; +} + +export default function ProviderModalsPanel({ + providerId, + providerInfo, + isCompatible, + isAnthropicProtocolCompatible, + isCcCompatible, + isUpstreamProxyProvider, + subscriptionRisk, + showRiskNoticeModal, + handleConfirmRiskNotice, + handleCancelRiskNotice, + showOAuthModal, + reauthConnection, + handleOAuthSuccess, + setShowOAuthModal, + showSiliconFlowEndpointModal, + setSiliconFlowInitialBaseUrl, + setShowSiliconFlowEndpointModal, + setShowAddApiKeyModal, + showAddApiKeyModal, + siliconFlowInitialBaseUrl, + commandCodeAuthState, + handleStartCommandCodeAuth, + handleSaveApiKey, + handleCloseAddApiKeyModal, + isCommandCode, + batchDeleteConfirmOpen, + setBatchDeleteConfirmOpen, + handleBatchDeleteConfirm, + selectedIds, + batchDeleting, + applyCodexModalConnectionId, + setApplyCodexModalConnectionId, + applyingCodexAuthId, + handleApplyCodexAuthLocal, + importCodexModalOpen, + setImportCodexModalOpen, + fetchConnections, + externalLinkModalOpen, + setExternalLinkModalOpen, + externalLinkLoading, + externalLinkError, + externalLinkUrl, + externalLinkCopied, + externalLinkCopy, + showEditModal, + setShowEditModal, + selectedConnection, + handleUpdateConnection, + showEditNodeModal, + setShowEditNodeModal, + providerNode, + handleUpdateNode, + codexCliGuideOpen, + setCodexCliGuideOpen, + applyClaudeModalConnectionId, + setApplyClaudeModalConnectionId, + applyingClaudeAuthId, + handleApplyClaudeAuthLocal, + importClaudeModalOpen, + setImportClaudeModalOpen, + applyGeminiModalConnectionId, + setApplyGeminiModalConnectionId, + applyingGeminiAuthId, + handleApplyGeminiAuthLocal, + importGeminiModalOpen, + setImportGeminiModalOpen, + batchTestResults, + setBatchTestResults, + emailsVisible, + proxyTarget, + setProxyTarget, + fetchProxyConfig, + importProgress, + showImportModal, + setShowImportModal, + showTutorialModal, + setShowTutorialModal, + t, +}: ProviderModalsPanelProps) { + return ( + <> + {showRiskNoticeModal && subscriptionRisk && ( + + )} + {!isUpstreamProxyProvider && + (providerId === "kiro" || providerId === "amazon-q" ? ( + setShowOAuthModal(false)} + /> + ) : providerId === "cursor" ? ( + setShowOAuthModal(false)} + /> + ) : providerId === "trae" ? ( + setShowOAuthModal(false)} + /> + ) : ( + setShowOAuthModal(false)} + /> + ))} + {providerId === "siliconflow" && ( + { + setSiliconFlowInitialBaseUrl(baseUrl); + setShowSiliconFlowEndpointModal(false); + setShowAddApiKeyModal(true); + }} + onClose={() => { + setShowSiliconFlowEndpointModal(false); + setSiliconFlowInitialBaseUrl(undefined); + }} + /> + )} + {!isUpstreamProxyProvider && ( + + )} + setBatchDeleteConfirmOpen(false)} + onConfirm={handleBatchDeleteConfirm} + title={t("batchDeleteConfirmTitle", "Delete connections")} + message={t("batchDeleteConfirm", { count: selectedIds.size })} + confirmText={t("batchDeleteConfirmButton", "Delete")} + cancelText={t("cancel", "Cancel")} + loading={batchDeleting} + /> + {providerId === "codex" && applyCodexModalConnectionId && ( + setApplyCodexModalConnectionId(null)} + /> + )} + {!isUpstreamProxyProvider && ( + setShowEditModal(false)} + /> + )} + {!isUpstreamProxyProvider && isCompatible && ( + setShowEditNodeModal(false)} + isAnthropic={isAnthropicProtocolCompatible} + isCcCompatible={isCcCompatible} + /> + )} + setCodexCliGuideOpen(false)} /> + {providerId === "codex" && importCodexModalOpen && ( + setImportCodexModalOpen(false)} + onSuccess={() => { + setImportCodexModalOpen(false); + void fetchConnections(); + }} + /> + )} + {providerId === "codex" && externalLinkModalOpen && ( + setExternalLinkModalOpen(false)} + loading={externalLinkLoading} + error={externalLinkError} + url={externalLinkUrl} + copied={externalLinkCopied} + onCopy={externalLinkCopy} + /> + )} + {providerId === "claude" && applyClaudeModalConnectionId && ( + setApplyClaudeModalConnectionId(null)} + /> + )} + {providerId === "claude" && importClaudeModalOpen && ( + setImportClaudeModalOpen(false)} + onSuccess={() => { + setImportClaudeModalOpen(false); + void fetchConnections(); + }} + /> + )} + {providerId === "gemini-cli" && applyGeminiModalConnectionId && ( + setApplyGeminiModalConnectionId(null)} + /> + )} + {providerId === "gemini-cli" && importGeminiModalOpen && ( + setImportGeminiModalOpen(false)} + onSuccess={() => { + setImportGeminiModalOpen(false); + void fetchConnections(); + }} + /> + )} + setBatchTestResults(null)} + t={t} + /> + {proxyTarget && ( + setProxyTarget(null)} + level={proxyTarget.level} + levelId={proxyTarget.id} + levelLabel={proxyTarget.label} + onSaved={() => { + void fetchProxyConfig(); + }} + /> + )} + { + if (importProgress.phase === "done" || importProgress.phase === "error") { + setShowImportModal(false); + } + }} + t={t} + /> + {providerId === "adapta-web" && ( + setShowTutorialModal(false)} + /> + )} + + ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPageHeader.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPageHeader.tsx new file mode 100644 index 0000000000..2179a0f11b --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPageHeader.tsx @@ -0,0 +1,96 @@ +"use client"; + +// Phase 1t.1 extraction — Issue #3501 +import Link from "next/link"; +import ProviderIcon from "@/shared/components/ProviderIcon"; +import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle"; +import { getHeaderIconProviderId } from "../providerPageHelpers"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface ProviderInfo { + id: string; + name: string; + website?: string; + color: string; + apiType?: string; +} + +interface ProviderPageHeaderProps { + providerId: string; + providerInfo: ProviderInfo; + connectionsCount: number; + isOpenAICompatible: boolean; + isAnthropicProtocolCompatible: boolean; + onOpenTutorial: () => void; + t: ProviderMessageTranslator; +} + +export default function ProviderPageHeader({ + providerId, + providerInfo, + connectionsCount, + isOpenAICompatible, + isAnthropicProtocolCompatible, + onOpenTutorial, + t, +}: ProviderPageHeaderProps) { + return ( +
+ + arrow_back + {t("backToProviders")} + +
+
+ +
+
+ {providerInfo.website ? ( + + {providerInfo.name} + open_in_new + + ) : ( +

{providerInfo.name}

+ )} +
+

+ {t("connectionCountLabel", { count: connectionsCount })} +

+ + {providerId === "adapta-web" && ( + + )} +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/SearchProviderCard.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/SearchProviderCard.tsx new file mode 100644 index 0000000000..0972380816 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/SearchProviderCard.tsx @@ -0,0 +1,37 @@ +"use client"; + +// Phase 1t.7 extraction — Issue #3501 +import { Card } from "@/shared/components"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface SearchProviderCardProps { + providerId: string; + t: ProviderMessageTranslator; +} + +export default function SearchProviderCard({ providerId, t }: SearchProviderCardProps) { + return ( + +

{t("searchProvider")}

+

{t("searchProviderDesc")}

+ {providerId === "perplexity-search" && ( +
+ link +

{t("perplexitySearchSharedKeyInfo")}

+
+ )} + {providerId === "google-pse-search" && ( +
+ tune +

{t("googlePseInfo")}

+
+ )} + {providerId === "searxng-search" && ( +
+ dns +

{t("searxngInfo")}

+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/UpstreamProxyCard.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/UpstreamProxyCard.tsx new file mode 100644 index 0000000000..f93810c4d1 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/UpstreamProxyCard.tsx @@ -0,0 +1,48 @@ +"use client"; + +// Phase 1t.7 extraction — Issue #3501 +import Link from "next/link"; +import { Card } from "@/shared/components"; +import { providerText } from "../providerPageHelpers"; +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface UpstreamProxyCardProps { + t: ProviderMessageTranslator; +} + +export default function UpstreamProxyCard({ t }: UpstreamProxyCardProps) { + return ( + +
+
+

+ {providerText(t, "upstreamProxyManagedTitle", "Managed via Upstream Proxy Settings")} +

+

+ {providerText( + t, + "upstreamProxyManagedDescription", + "CLIProxyAPI is configured as an upstream proxy layer, not as a direct provider connection. Manage the binary/runtime in CLI Tools and enable proxy routing on each provider via the provider proxy controls." + )} +

+
+
+ + terminal + {t("openCliTools")} + + + settings + {t("openSettings")} + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionGate.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionGate.ts new file mode 100644 index 0000000000..c892fece2a --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useConnectionGate.ts @@ -0,0 +1,48 @@ +// Phase 1t.3 extraction — Issue #3501 +// Encapsulates gateConnectionFlow + risk-notice modal state + confirm/cancel handlers. +import { useState, useRef, useCallback } from "react"; +import { isRiskAcknowledged, useRiskAcknowledged } from "../../hooks/useRiskAcknowledged"; + +interface UseConnectionGateParams { + providerId: string; + subscriptionRisk: boolean; +} + +export function useConnectionGate({ providerId, subscriptionRisk }: UseConnectionGateParams) { + const [showRiskNoticeModal, setShowRiskNoticeModal] = useState(false); + const pendingRiskActionRef = useRef<(() => void) | null>(null); + const { acknowledged: riskAcknowledged, acknowledge: acknowledgeRisk } = + useRiskAcknowledged(providerId); + + const gateConnectionFlow = useCallback( + (callback: () => void) => { + if (subscriptionRisk && !riskAcknowledged && !isRiskAcknowledged(providerId)) { + pendingRiskActionRef.current = callback; + setShowRiskNoticeModal(true); + return; + } + callback(); + }, + [providerId, riskAcknowledged, subscriptionRisk] + ); + + const handleConfirmRiskNotice = useCallback(() => { + acknowledgeRisk(); + setShowRiskNoticeModal(false); + const pendingAction = pendingRiskActionRef.current; + pendingRiskActionRef.current = null; + pendingAction?.(); + }, [acknowledgeRisk]); + + const handleCancelRiskNotice = useCallback(() => { + pendingRiskActionRef.current = null; + setShowRiskNoticeModal(false); + }, []); + + return { + showRiskNoticeModal, + gateConnectionFlow, + handleConfirmRiskNotice, + handleCancelRiskNotice, + }; +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderNodeActions.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderNodeActions.ts new file mode 100644 index 0000000000..0b7c29979e --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderNodeActions.ts @@ -0,0 +1,63 @@ +// Phase 1t.4 extraction — Issue #3501 +// Encapsulates handleUpdateNode and handleUpdateConnection async handlers. +import type { ProviderMessageTranslator } from "../providerPageHelpers"; + +interface UseProviderNodeActionsParams { + providerId: string; + fetchConnections: () => Promise; + selectedConnection: { id: string } | null; + setProviderNode: (node: any) => void; + setShowEditNodeModal: (open: boolean) => void; + setShowEditModal: (open: boolean) => void; + t: ProviderMessageTranslator; +} + +export function useProviderNodeActions({ + providerId, + fetchConnections, + selectedConnection, + setProviderNode, + setShowEditNodeModal, + setShowEditModal, + t, +}: UseProviderNodeActionsParams) { + const handleUpdateNode = async (formData: any) => { + try { + const res = await fetch(`/api/provider-nodes/${providerId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + const data = await res.json(); + if (res.ok) { + setProviderNode(data.node); + await fetchConnections(); + setShowEditNodeModal(false); + } + } catch (error) { + console.log("Error updating provider node:", error); + } + }; + + const handleUpdateConnection = async (formData: any) => { + try { + const res = await fetch(`/api/providers/${selectedConnection?.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + if (res.ok) { + await fetchConnections(); + setShowEditModal(false); + return null; + } + const data = await res.json().catch(() => ({})); + return data.error?.message || data.error || t("failedSaveConnection"); + } catch (error) { + console.log("Error updating connection:", error); + return t("failedSaveConnectionRetry"); + } + }; + + return { handleUpdateNode, handleUpdateConnection }; +}