refactor(#3501): god-component Phase 1t — client 1376→781 LOC (≤800 TARGET REACHED ) (#3727)

Phase 1t of #3501: client 1376→781 LOC (≤800 reached). Original god-component 12,882→781 (−94%).

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-12 11:31:33 -03:00
committed by GitHub
parent dd0b377b81
commit 37e80306b2
10 changed files with 1143 additions and 762 deletions

View File

@@ -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.)."
}

View File

@@ -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 (
<Card>
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold">
{isCcCompatible
? t("ccCompatibleDetailsTitle")
: isAnthropicCompatible
? t("anthropicCompatibleDetails")
: t("openaiCompatibleDetails")}
</h2>
<p className="text-sm text-text-muted">
{getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} ·{" "}
{(providerNode.baseUrl || "").replace(/\/$/, "")}/
{getApiPath(
isCcCompatible,
isAnthropicCompatible,
providerNode?.apiType,
providerNode?.chatPath
)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button size="sm" icon="add" onClick={() => gateConnectionFlow(openApiKeyAddFlow)}>
{t("add")}
</Button>
<Button
size="sm"
variant="secondary"
icon="edit"
onClick={onOpenEditNodeModal}
>
{t("edit")}
</Button>
<Button
size="sm"
variant="secondary"
icon="delete"
onClick={async () => {
if (
!confirm(
t("deleteCompatibleNodeConfirm", {
type: isCcCompatible
? t("ccCompatibleLabel")
: isAnthropicCompatible
? t("anthropic")
: t("openai"),
})
)
)
return;
try {
const res = await fetch(`/api/provider-nodes/${providerId}`, {
method: "DELETE",
});
if (res.ok) {
router.push("/dashboard/providers");
}
} catch (error) {
console.error("Error deleting provider node:", error);
}
}}
>
{t("delete")}
</Button>
</div>
</div>
{isCcCompatible && (
<div className="mb-4 rounded-lg border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-sm text-text-muted">
<div className="flex items-start gap-2">
<span className="material-symbols-outlined mt-0.5 text-[18px] text-amber-500">
warning
</span>
<p>{t("ccCompatibleValidationHint")}</p>
</div>
</div>
)}
</Card>
);
}

View File

@@ -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 (
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<span className="material-symbols-outlined text-[32px]">
{isOAuth ? "lock" : "key"}
</span>
</div>
<p className="text-text-main font-medium mb-1">{t("noConnectionsYet")}</p>
<p className="text-sm text-text-muted mb-4">{t("addFirstConnectionHint")}</p>
{!isCompatible && (
<div className="flex items-center justify-center gap-2">
{isCommandCode ? (
<>
<Button
icon="open_in_new"
loading={
commandCodeAuthState.phase === "starting" ||
commandCodeAuthState.phase === "polling" ||
commandCodeAuthState.phase === "applying"
}
onClick={() => gateConnectionFlow(handleOpenCommandCodeConnect)}
>
Connect
</Button>
<Button
variant="secondary"
icon="add"
onClick={() => gateConnectionFlow(openApiKeyAddFlow)}
>
Manual API key
</Button>
</>
) : (
<>
<Button icon="add" onClick={() => gateConnectionFlow(openPrimaryAddFlow)}>
{providerSupportsPat ? "Add PAT" : t("addConnection")}
</Button>
{providerId === "qoder" && (
<Button
variant="secondary"
onClick={() => gateConnectionFlow(onOpenOAuthModal)}
>
Experimental OAuth
</Button>
)}
{providerId === "codex" && (
<Button
variant="secondary"
icon="upload_file"
onClick={() => gateConnectionFlow(onOpenImportCodex)}
>
{typeof t.has === "function" && t.has("importCodexAuth")
? t("importCodexAuth")
: "Import auth"}
</Button>
)}
{providerId === "claude" && (
<Button
variant="secondary"
icon="upload_file"
onClick={() => gateConnectionFlow(onOpenImportClaude)}
>
{typeof t.has === "function" && t.has("importClaudeAuth")
? t("importClaudeAuth")
: "Import auth"}
</Button>
)}
{providerId === "gemini-cli" && (
<Button
variant="secondary"
icon="upload_file"
onClick={() => gateConnectionFlow(onOpenImportGemini)}
>
{typeof t.has === "function" && t.has("importGeminiAuth")
? t("importGeminiAuth")
: "Import auth"}
</Button>
)}
</>
)}
</div>
)}
</div>
);
}

View File

@@ -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<void>;
handleCloseAddApiKeyModal: () => void;
// Batch delete confirm
batchDeleteConfirmOpen: boolean;
setBatchDeleteConfirmOpen: (open: boolean) => void;
handleBatchDeleteConfirm: () => void;
selectedIds: Set<string>;
batchDeleting: boolean;
// Codex auth
applyCodexModalConnectionId: string | null;
setApplyCodexModalConnectionId: (id: string | null) => void;
applyingCodexAuthId: string | null;
handleApplyCodexAuthLocal: (id: string) => Promise<void>;
importCodexModalOpen: boolean;
setImportCodexModalOpen: (open: boolean) => void;
fetchConnections: () => Promise<void>;
// 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<string | null>;
// Edit compatible node
showEditNodeModal: boolean;
setShowEditNodeModal: (open: boolean) => void;
providerNode: any;
handleUpdateNode: (data: any) => Promise<void>;
// 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<void>;
importClaudeModalOpen: boolean;
setImportClaudeModalOpen: (open: boolean) => void;
// Gemini auth
applyGeminiModalConnectionId: string | null;
setApplyGeminiModalConnectionId: (id: string | null) => void;
applyingGeminiAuthId: string | null;
handleApplyGeminiAuthLocal: (id: string) => Promise<void>;
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<void>;
// 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 && (
<RiskNoticeModal
variant={(providerInfo.riskNoticeVariant as string) ?? "oauth"}
providerId={providerId}
providerName={providerInfo.name}
onConfirm={handleConfirmRiskNotice}
onCancel={handleCancelRiskNotice}
/>
)}
{!isUpstreamProxyProvider &&
(providerId === "kiro" || providerId === "amazon-q" ? (
<KiroOAuthWrapper
isOpen={showOAuthModal}
reauthConnection={reauthConnection}
providerInfo={{ ...providerInfo, id: providerId }}
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
) : providerId === "cursor" ? (
<CursorAuthModal
isOpen={showOAuthModal}
reauthConnection={reauthConnection}
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
) : providerId === "trae" ? (
<TraeAuthModal
isOpen={showOAuthModal}
reauthConnection={reauthConnection}
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
) : (
<OAuthModal
isOpen={showOAuthModal}
reauthConnection={reauthConnection}
provider={providerId}
providerInfo={providerInfo}
onSuccess={handleOAuthSuccess}
onClose={() => setShowOAuthModal(false)}
/>
))}
{providerId === "siliconflow" && (
<SiliconFlowEndpointModal
isOpen={showSiliconFlowEndpointModal}
onSelect={(baseUrl) => {
setSiliconFlowInitialBaseUrl(baseUrl);
setShowSiliconFlowEndpointModal(false);
setShowAddApiKeyModal(true);
}}
onClose={() => {
setShowSiliconFlowEndpointModal(false);
setSiliconFlowInitialBaseUrl(undefined);
}}
/>
)}
{!isUpstreamProxyProvider && (
<AddApiKeyModal
isOpen={showAddApiKeyModal}
provider={providerId}
providerName={providerInfo.name}
initialBaseUrl={siliconFlowInitialBaseUrl}
isCompatible={isCompatible}
isAnthropic={isAnthropicProtocolCompatible}
isCcCompatible={isCcCompatible}
isCommandCode={isCommandCode}
commandCodeAuthState={commandCodeAuthState}
onStartCommandCodeAuth={handleStartCommandCodeAuth}
onSave={handleSaveApiKey}
onClose={handleCloseAddApiKeyModal}
/>
)}
<ConfirmModal
isOpen={batchDeleteConfirmOpen}
onClose={() => 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 && (
<ApplyCodexAuthModal
key={applyCodexModalConnectionId}
connectionId={applyCodexModalConnectionId}
inProgress={!!applyingCodexAuthId}
onConfirm={handleApplyCodexAuthLocal}
onClose={() => setApplyCodexModalConnectionId(null)}
/>
)}
{!isUpstreamProxyProvider && (
<EditConnectionModal
isOpen={showEditModal}
connection={selectedConnection}
onSave={handleUpdateConnection}
onClose={() => setShowEditModal(false)}
/>
)}
{!isUpstreamProxyProvider && isCompatible && (
<EditCompatibleNodeModal
isOpen={showEditNodeModal}
node={providerNode}
onSave={handleUpdateNode}
onClose={() => setShowEditNodeModal(false)}
isAnthropic={isAnthropicProtocolCompatible}
isCcCompatible={isCcCompatible}
/>
)}
<CodexCliGuideModal isOpen={codexCliGuideOpen} onClose={() => setCodexCliGuideOpen(false)} />
{providerId === "codex" && importCodexModalOpen && (
<ImportCodexAuthModal
key="import-codex-modal"
onClose={() => setImportCodexModalOpen(false)}
onSuccess={() => {
setImportCodexModalOpen(false);
void fetchConnections();
}}
/>
)}
{providerId === "codex" && externalLinkModalOpen && (
<ExternalLinkModal
isOpen={externalLinkModalOpen}
onClose={() => setExternalLinkModalOpen(false)}
loading={externalLinkLoading}
error={externalLinkError}
url={externalLinkUrl}
copied={externalLinkCopied}
onCopy={externalLinkCopy}
/>
)}
{providerId === "claude" && applyClaudeModalConnectionId && (
<ApplyClaudeAuthModal
key={applyClaudeModalConnectionId}
connectionId={applyClaudeModalConnectionId}
inProgress={!!applyingClaudeAuthId}
onConfirm={handleApplyClaudeAuthLocal}
onClose={() => setApplyClaudeModalConnectionId(null)}
/>
)}
{providerId === "claude" && importClaudeModalOpen && (
<ImportClaudeAuthModal
key="import-claude-modal"
onClose={() => setImportClaudeModalOpen(false)}
onSuccess={() => {
setImportClaudeModalOpen(false);
void fetchConnections();
}}
/>
)}
{providerId === "gemini-cli" && applyGeminiModalConnectionId && (
<ApplyGeminiAuthModal
key={applyGeminiModalConnectionId}
connectionId={applyGeminiModalConnectionId}
inProgress={!!applyingGeminiAuthId}
onConfirm={handleApplyGeminiAuthLocal}
onClose={() => setApplyGeminiModalConnectionId(null)}
/>
)}
{providerId === "gemini-cli" && importGeminiModalOpen && (
<ImportGeminiAuthModal
key="import-gemini-modal"
onClose={() => setImportGeminiModalOpen(false)}
onSuccess={() => {
setImportGeminiModalOpen(false);
void fetchConnections();
}}
/>
)}
<BatchTestResultsModal
batchTestResults={batchTestResults}
providerInfo={providerInfo}
providerId={providerId}
emailsVisible={emailsVisible}
onClose={() => setBatchTestResults(null)}
t={t}
/>
{proxyTarget && (
<ProxyConfigModal
isOpen={!!proxyTarget}
onClose={() => setProxyTarget(null)}
level={proxyTarget.level}
levelId={proxyTarget.id}
levelLabel={proxyTarget.label}
onSaved={() => {
void fetchProxyConfig();
}}
/>
)}
<ImportProgressModal
importProgress={importProgress}
isOpen={showImportModal}
onClose={() => {
if (importProgress.phase === "done" || importProgress.phase === "error") {
setShowImportModal(false);
}
}}
t={t}
/>
{providerId === "adapta-web" && (
<AdaptaTutorialModal
isOpen={showTutorialModal}
onClose={() => setShowTutorialModal(false)}
/>
)}
</>
);
}

View File

@@ -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 (
<div>
<Link
href="/dashboard/providers"
className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
>
<span className="material-symbols-outlined text-lg">arrow_back</span>
{t("backToProviders")}
</Link>
<div className="flex items-center gap-4">
<div
className="rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${providerInfo.color}15` }}
>
<ProviderIcon
providerId={getHeaderIconProviderId(
isOpenAICompatible,
isAnthropicProtocolCompatible,
providerInfo.id,
providerInfo.apiType
)}
size={48}
type="color"
/>
</div>
<div>
{providerInfo.website ? (
<a
href={providerInfo.website}
target="_blank"
rel="noopener noreferrer"
className="text-3xl font-semibold tracking-tight hover:underline inline-flex items-center gap-2"
style={{ color: providerInfo.color }}
>
{providerInfo.name}
<span className="material-symbols-outlined text-lg opacity-60">open_in_new</span>
</a>
) : (
<h1 className="text-3xl font-semibold tracking-tight">{providerInfo.name}</h1>
)}
<div className="flex items-center gap-2">
<p className="text-text-muted">
{t("connectionCountLabel", { count: connectionsCount })}
</p>
<EmailPrivacyToggle size="md" />
{providerId === "adapta-web" && (
<button
onClick={onOpenTutorial}
className="text-sm font-medium underline underline-offset-2 opacity-70 hover:opacity-100 transition-opacity"
style={{ color: providerInfo.color }}
>
Tutorial
</button>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -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 (
<Card>
<h2 className="text-lg font-semibold mb-4">{t("searchProvider")}</h2>
<p className="text-sm text-text-muted">{t("searchProviderDesc")}</p>
{providerId === "perplexity-search" && (
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
<span className="material-symbols-outlined text-sm text-blue-400">link</span>
<p className="text-xs text-blue-300">{t("perplexitySearchSharedKeyInfo")}</p>
</div>
)}
{providerId === "google-pse-search" && (
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
<span className="material-symbols-outlined text-sm text-amber-300">tune</span>
<p className="text-xs text-amber-200">{t("googlePseInfo")}</p>
</div>
)}
{providerId === "searxng-search" && (
<div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<span className="material-symbols-outlined text-sm text-emerald-300">dns</span>
<p className="text-xs text-emerald-200">{t("searxngInfo")}</p>
</div>
)}
</Card>
);
}

View File

@@ -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 (
<Card>
<div className="flex flex-col gap-3">
<div>
<h2 className="text-lg font-semibold">
{providerText(t, "upstreamProxyManagedTitle", "Managed via Upstream Proxy Settings")}
</h2>
<p className="text-sm text-text-muted mt-1">
{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."
)}
</p>
</div>
<div className="flex flex-wrap gap-2">
<Link
href="/dashboard/cli-code"
className="inline-flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-text-main hover:border-primary/40 hover:text-text-primary transition-colors"
>
<span className="material-symbols-outlined text-base">terminal</span>
{t("openCliTools")}
</Link>
<Link
href="/dashboard/settings"
className="inline-flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-text-main hover:border-primary/40 hover:text-text-primary transition-colors"
>
<span className="material-symbols-outlined text-base">settings</span>
{t("openSettings")}
</Link>
</div>
</div>
</Card>
);
}

View File

@@ -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,
};
}

View File

@@ -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<void>;
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 };
}