"use client"; import { useEffect, useRef, useState } from "react"; import { Check, ChevronsUpDown, Loader2, LogIn, LogOut, RefreshCw, UserCheck, } from "lucide-react"; import { Button } from "./ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; interface CodexConfigProps { codexModel: string; onInputChange: (value: string | boolean, field: string) => void; } type AuthStatus = "checking" | "unauthenticated" | "polling" | "authenticated"; interface StatusResponse { status: string; account_id?: string; detail?: string; } interface CodexModel { id: string; name: string; } const CHATGPT_MODELS: CodexModel[] = [ { id: "gpt-5.1", name: "GPT-5.1" }, { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" }, { id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" }, { id: "gpt-5.2", name: "GPT-5.2" }, { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, { id: "gpt-5.4", name: "GPT-5.4" }, { id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" }, ]; const DEFAULT_CODEX_MODEL = "gpt-5.1"; export default function CodexConfig({ codexModel, onInputChange, }: CodexConfigProps) { const [authStatus, setAuthStatus] = useState("checking"); const [accountId, setAccountId] = useState(null); const [sessionId, setSessionId] = useState(null); const [manualCode, setManualCode] = useState(""); const [isExchanging, setIsExchanging] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [openModelSelect, setOpenModelSelect] = useState(false); const pollIntervalRef = useRef | null>(null); const stopPolling = () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }; // Check current auth state on mount useEffect(() => { checkCurrentAuthStatus(); return () => stopPolling(); }, []); const checkCurrentAuthStatus = async () => { try { const res = await fetch("/api/v1/ppt/codex/auth/status"); if (!res.ok) { setAuthStatus("unauthenticated"); return; } const data: StatusResponse = await res.json(); if (data.status === "authenticated") { setAuthStatus("authenticated"); setAccountId(data.account_id ?? null); } else { setAuthStatus("unauthenticated"); } } catch { setAuthStatus("unauthenticated"); } }; const handleSignIn = async () => { try { const res = await fetch("/api/v1/ppt/codex/auth/initiate", { method: "POST", }); if (!res.ok) throw new Error("Failed to initiate auth"); const data = await res.json(); const { session_id, url } = data; setSessionId(session_id); setAuthStatus("polling"); window.open(url, "_blank", "noopener,noreferrer"); // Start polling the status endpoint every 2s pollIntervalRef.current = setInterval(async () => { try { const pollRes = await fetch( `/api/v1/ppt/codex/auth/status/${session_id}` ); if (!pollRes.ok) return; const pollData: StatusResponse = await pollRes.json(); if (pollData.status === "success") { stopPolling(); setAuthStatus("authenticated"); setAccountId(pollData.account_id ?? null); setSessionId(null); // Set a sensible default model if none chosen if (!codexModel) { onInputChange(DEFAULT_CODEX_MODEL, "codex_model"); } toast.success("Signed in to ChatGPT successfully"); } else if (pollData.status === "failed") { stopPolling(); setAuthStatus("unauthenticated"); toast.error("Authentication failed. Please try again."); } } catch { // keep polling on transient errors } }, 2000); } catch (err) { toast.error("Failed to start sign-in flow"); setAuthStatus("unauthenticated"); } }; const handleManualExchange = async () => { if (!sessionId || !manualCode.trim()) return; setIsExchanging(true); try { const res = await fetch("/api/v1/ppt/codex/auth/exchange", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, code: manualCode.trim() }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Exchange failed"); } const data = await res.json(); stopPolling(); setAuthStatus("authenticated"); setAccountId(data.account_id); setSessionId(null); setManualCode(""); if (!codexModel) { onInputChange(DEFAULT_CODEX_MODEL, "codex_model"); } toast.success("Signed in to ChatGPT successfully"); } catch (err: any) { toast.error(err.message || "Code exchange failed"); } finally { setIsExchanging(false); } }; const handleCancelPolling = () => { stopPolling(); setSessionId(null); setManualCode(""); setAuthStatus("unauthenticated"); }; const handleSignOut = async () => { setIsLoggingOut(true); try { await fetch("/api/v1/ppt/codex/auth/logout", { method: "POST" }); setAuthStatus("unauthenticated"); setAccountId(null); onInputChange("", "codex_model"); toast.success("Signed out from ChatGPT"); } catch { toast.error("Sign out failed"); } finally { setIsLoggingOut(false); } }; const handleRefreshToken = async () => { setIsRefreshing(true); try { const res = await fetch("/api/v1/ppt/codex/auth/refresh", { method: "POST", }); if (!res.ok) throw new Error("Refresh failed"); const data = await res.json(); if (data.account_id) setAccountId(data.account_id); toast.success("Token refreshed successfully"); } catch { toast.error("Token refresh failed. Please sign in again."); setAuthStatus("unauthenticated"); } finally { setIsRefreshing(false); } }; // ─── Checking ──────────────────────────────────────────────────────────── if (authStatus === "checking") { return (
Checking authentication status…
); } // ─── Polling / waiting ─────────────────────────────────────────────────── if (authStatus === "polling") { return (

Waiting for authentication…

Complete the sign-in in the browser tab that just opened.

{/* Manual fallback */}

Didn't get redirected automatically?

After completing the sign-in, paste the full redirect URL or authorization code below.

setManualCode(e.target.value)} />
); } // ─── Authenticated ─────────────────────────────────────────────────────── if (authStatus === "authenticated") { return (
{/* Account info */}

Signed in to ChatGPT

{accountId && (

Account: {accountId}

)}
{/* Model selection */}
No model found. {CHATGPT_MODELS.map((model) => ( { onInputChange(value, "codex_model"); setOpenModelSelect(false); }} > {model.name} ))}

Model availability depends on your ChatGPT subscription tier.

); } // ─── Unauthenticated ───────────────────────────────────────────────────── return (

ChatGPT Plus / Pro

Sign in with your OpenAI account to use ChatGPT models directly via OAuth — no API key required.

A browser window will open for you to authenticate with your OpenAI account. Your credentials are stored locally and never shared.

); }