From 172b8e657d5ada81a5ec765583a91a4a92562bf8 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Fri, 3 Apr 2026 13:26:07 +0545 Subject: [PATCH 01/47] feat: enhance Codex user profile management with username, email, and subscription status --- .../api/v1/ppt/endpoints/codex_auth.py | 92 +++++++++++++++---- .../servers/fastapi/models/user_config.py | 3 + electron/servers/fastapi/utils/get_env.py | 12 +++ .../fastapi/utils/oauth/openai_codex.py | 59 +++++++++++- electron/servers/fastapi/utils/set_env.py | 12 +++ electron/servers/fastapi/utils/user_config.py | 22 +++++ .../nextjs/app/api/user-config/route.ts | 3 + .../servers/nextjs/components/CodexConfig.tsx | 59 ++++++++++-- .../nextjs/components/SettingCodex.tsx | 58 ++++++++++-- electron/servers/nextjs/types/llm_config.ts | 3 + 10 files changed, 290 insertions(+), 33 deletions(-) diff --git a/electron/servers/fastapi/api/v1/ppt/endpoints/codex_auth.py b/electron/servers/fastapi/api/v1/ppt/endpoints/codex_auth.py index c6576888..da28db6d 100644 --- a/electron/servers/fastapi/api/v1/ppt/endpoints/codex_auth.py +++ b/electron/servers/fastapi/api/v1/ppt/endpoints/codex_auth.py @@ -16,25 +16,32 @@ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from utils.oauth.openai_codex import ( + CodexAccountProfile, OAuthCallbackServer, TokenSuccess, create_authorization_flow, exchange_authorization_code, - get_account_id, + get_account_profile, parse_authorization_input, refresh_access_token, ) from utils.get_env import ( get_codex_access_token_env, + get_codex_email_env, + get_codex_is_pro_env, get_codex_refresh_token_env, get_codex_token_expires_env, + get_codex_username_env, ) from utils.set_env import ( set_codex_access_token_env, set_codex_account_id_env, + set_codex_email_env, + set_codex_is_pro_env, set_codex_refresh_token_env, set_codex_token_expires_env, set_codex_model_env, + set_codex_username_env, ) from utils.user_config import save_codex_tokens_to_user_config @@ -60,6 +67,9 @@ class InitiateResponse(BaseModel): class StatusResponse(BaseModel): status: str # "pending" | "success" | "failed" account_id: Optional[str] = None + username: Optional[str] = None + email: Optional[str] = None + is_pro: Optional[bool] = None detail: Optional[str] = None @@ -69,11 +79,17 @@ class ExchangeRequest(BaseModel): class ExchangeResponse(BaseModel): - account_id: str + account_id: Optional[str] = None + username: Optional[str] = None + email: Optional[str] = None + is_pro: Optional[bool] = None class RefreshResponse(BaseModel): account_id: Optional[str] + username: Optional[str] = None + email: Optional[str] = None + is_pro: Optional[bool] = None detail: str @@ -81,16 +97,31 @@ class RefreshResponse(BaseModel): # Helper # --------------------------------------------------------------------------- -def _store_token(result: TokenSuccess) -> Optional[str]: - """Persist token fields in env vars and userConfig.json. Returns account_id or None.""" +def _parse_optional_bool(value: Optional[str]) -> Optional[bool]: + if value is None: + return None + normalized = value.strip().lower() + if normalized in {"true", "1", "yes", "y"}: + return True + if normalized in {"false", "0", "no", "n"}: + return False + return None + + +def _store_token(result: TokenSuccess) -> CodexAccountProfile: + """Persist token fields in env vars and userConfig.json. Returns parsed profile.""" set_codex_access_token_env(result.access) set_codex_refresh_token_env(result.refresh) set_codex_token_expires_env(str(result.expires)) - account_id = get_account_id(result.access) - if account_id: - set_codex_account_id_env(account_id) + + profile = get_account_profile(result.access, result.id_token) + set_codex_account_id_env(profile.account_id or "") + set_codex_username_env(profile.username or "") + set_codex_email_env(profile.email or "") + set_codex_is_pro_env("" if profile.is_pro is None else str(profile.is_pro)) + save_codex_tokens_to_user_config() - return account_id + return profile # --------------------------------------------------------------------------- @@ -166,8 +197,14 @@ async def poll_codex_auth_status(session_id: str): if not isinstance(result, TokenSuccess): return StatusResponse(status="failed", detail=result.reason) - account_id = _store_token(result) - return StatusResponse(status="success", account_id=account_id) + profile = _store_token(result) + return StatusResponse( + status="success", + account_id=profile.account_id, + username=profile.username, + email=profile.email, + is_pro=profile.is_pro, + ) @CODEX_AUTH_ROUTER.post("/exchange", response_model=ExchangeResponse) @@ -207,11 +244,16 @@ async def exchange_codex_code(body: ExchangeRequest): if not isinstance(result, TokenSuccess): raise HTTPException(status_code=502, detail=f"Token exchange failed: {result.reason}") - account_id = _store_token(result) - if not account_id: + profile = _store_token(result) + if not profile.account_id: raise HTTPException(status_code=502, detail="Token exchanged but could not extract account ID") - return ExchangeResponse(account_id=account_id) + return ExchangeResponse( + account_id=profile.account_id, + username=profile.username, + email=profile.email, + is_pro=profile.is_pro, + ) @CODEX_AUTH_ROUTER.post("/refresh", response_model=RefreshResponse) @@ -232,9 +274,12 @@ async def refresh_codex_token(): if not isinstance(result, TokenSuccess): raise HTTPException(status_code=502, detail=f"Token refresh failed: {result.reason}") - account_id = _store_token(result) + profile = _store_token(result) return RefreshResponse( - account_id=account_id, + account_id=profile.account_id, + username=profile.username, + email=profile.email, + is_pro=profile.is_pro, detail="Token refreshed successfully", ) @@ -260,8 +305,18 @@ async def get_codex_auth_status(): except (ValueError, TypeError): pass - account_id = get_account_id(access_token) - return StatusResponse(status="authenticated", account_id=account_id) + profile = get_account_profile(access_token) + return StatusResponse( + status="authenticated", + account_id=profile.account_id, + username=profile.username or get_codex_username_env(), + email=profile.email or get_codex_email_env(), + is_pro=( + profile.is_pro + if profile.is_pro is not None + else _parse_optional_bool(get_codex_is_pro_env()) + ), + ) @CODEX_AUTH_ROUTER.post("/logout") @@ -273,6 +328,9 @@ async def logout_codex(): set_codex_refresh_token_env("") set_codex_token_expires_env("") set_codex_account_id_env("") + set_codex_username_env("") + set_codex_email_env("") + set_codex_is_pro_env("") set_codex_model_env("") save_codex_tokens_to_user_config() return {"detail": "Logged out successfully"} diff --git a/electron/servers/fastapi/models/user_config.py b/electron/servers/fastapi/models/user_config.py index c26a6cb0..05b050d7 100644 --- a/electron/servers/fastapi/models/user_config.py +++ b/electron/servers/fastapi/models/user_config.py @@ -55,3 +55,6 @@ class UserConfig(BaseModel): CODEX_REFRESH_TOKEN: Optional[str] = None CODEX_TOKEN_EXPIRES: Optional[str] = None CODEX_ACCOUNT_ID: Optional[str] = None + CODEX_USERNAME: Optional[str] = None + CODEX_EMAIL: Optional[str] = None + CODEX_IS_PRO: Optional[bool] = None diff --git a/electron/servers/fastapi/utils/get_env.py b/electron/servers/fastapi/utils/get_env.py index 53680acd..bce31306 100644 --- a/electron/servers/fastapi/utils/get_env.py +++ b/electron/servers/fastapi/utils/get_env.py @@ -136,6 +136,18 @@ def get_codex_account_id_env(): return os.getenv("CODEX_ACCOUNT_ID") +def get_codex_username_env(): + return os.getenv("CODEX_USERNAME") + + +def get_codex_email_env(): + return os.getenv("CODEX_EMAIL") + + +def get_codex_is_pro_env(): + return os.getenv("CODEX_IS_PRO") + + def get_codex_model_env(): return os.getenv("CODEX_MODEL") diff --git a/electron/servers/fastapi/utils/oauth/openai_codex.py b/electron/servers/fastapi/utils/oauth/openai_codex.py index f1e2f1ae..c94b75eb 100644 --- a/electron/servers/fastapi/utils/oauth/openai_codex.py +++ b/electron/servers/fastapi/utils/oauth/openai_codex.py @@ -244,6 +244,7 @@ class TokenSuccess: access: str refresh: str expires: int # Unix ms timestamp when the token expires + id_token: Optional[str] = None @dataclass @@ -261,6 +262,14 @@ class AuthorizationFlow: url: str +@dataclass +class CodexAccountProfile: + account_id: Optional[str] = None + username: Optional[str] = None + email: Optional[str] = None + is_pro: Optional[bool] = None + + # --------------------------------------------------------------------------- # JWT helpers # --------------------------------------------------------------------------- @@ -296,6 +305,51 @@ def get_account_id(access_token: str) -> Optional[str]: return None +def _as_non_empty_str(value) -> Optional[str]: + if isinstance(value, str): + stripped = value.strip() + return stripped or None + return None + + +def get_account_profile(access_token: str, id_token: Optional[str] = None) -> CodexAccountProfile: + """Extract profile from exact observed JWT paths in access/id tokens.""" + access_payload = _decode_jwt_payload(access_token) or {} + access_auth = access_payload.get(JWT_CLAIM_PATH) + access_auth = access_auth if isinstance(access_auth, dict) else {} + + access_profile = access_payload.get("https://api.openai.com/profile") + access_profile = access_profile if isinstance(access_profile, dict) else {} + + id_payload = _decode_jwt_payload(id_token) if id_token else None + id_payload = id_payload if isinstance(id_payload, dict) else {} + id_auth = id_payload.get(JWT_CLAIM_PATH) + id_auth = id_auth if isinstance(id_auth, dict) else {} + + account_id = _as_non_empty_str(access_auth.get("chatgpt_account_id")) or _as_non_empty_str( + id_auth.get("chatgpt_account_id") + ) + username = _as_non_empty_str(id_payload.get("name")) + email = _as_non_empty_str(access_profile.get("email")) or _as_non_empty_str( + id_payload.get("email") + ) + + plan_type = _as_non_empty_str(access_auth.get("chatgpt_plan_type")) or _as_non_empty_str( + id_auth.get("chatgpt_plan_type") + ) + if plan_type: + is_pro = plan_type.strip().lower() != "free" + else: + is_pro = None + + return CodexAccountProfile( + account_id=account_id, + username=username, + email=email, + is_pro=is_pro, + ) + + # --------------------------------------------------------------------------- # Authorization URL + PKCE # --------------------------------------------------------------------------- @@ -474,6 +528,7 @@ def exchange_authorization_code( return TokenFailure(reason=f"HTTP {response.status_code}: {response.text[:200]}") body = response.json() + access = body.get("access_token") refresh = body.get("refresh_token") expires_in = body.get("expires_in") @@ -482,7 +537,9 @@ def exchange_authorization_code( return TokenFailure(reason=f"Token response missing fields: {list(body.keys())}") expires_ms = int(time.time() * 1000) + int(expires_in) * 1000 - return TokenSuccess(access=access, refresh=refresh, expires=expires_ms) + id_token = body.get("id_token") + id_token = id_token if isinstance(id_token, str) else None + return TokenSuccess(access=access, refresh=refresh, expires=expires_ms, id_token=id_token) except Exception as exc: return TokenFailure(reason=str(exc)) diff --git a/electron/servers/fastapi/utils/set_env.py b/electron/servers/fastapi/utils/set_env.py index 6f26b34f..f626f4df 100644 --- a/electron/servers/fastapi/utils/set_env.py +++ b/electron/servers/fastapi/utils/set_env.py @@ -122,5 +122,17 @@ def set_codex_account_id_env(value: str): os.environ["CODEX_ACCOUNT_ID"] = value +def set_codex_username_env(value: str): + os.environ["CODEX_USERNAME"] = value + + +def set_codex_email_env(value: str): + os.environ["CODEX_EMAIL"] = value + + +def set_codex_is_pro_env(value: str): + os.environ["CODEX_IS_PRO"] = value + + def set_codex_model_env(value: str): os.environ["CODEX_MODEL"] = value diff --git a/electron/servers/fastapi/utils/user_config.py b/electron/servers/fastapi/utils/user_config.py index f83d3047..ab1d91da 100644 --- a/electron/servers/fastapi/utils/user_config.py +++ b/electron/servers/fastapi/utils/user_config.py @@ -32,6 +32,9 @@ from utils.get_env import ( get_codex_refresh_token_env, get_codex_token_expires_env, get_codex_account_id_env, + get_codex_username_env, + get_codex_email_env, + get_codex_is_pro_env, get_codex_model_env, ) from utils.parsers import parse_bool_or_none @@ -64,6 +67,9 @@ from utils.set_env import ( set_codex_refresh_token_env, set_codex_token_expires_env, set_codex_account_id_env, + set_codex_username_env, + set_codex_email_env, + set_codex_is_pro_env, set_codex_model_env, ) @@ -133,6 +139,13 @@ def get_user_config(): CODEX_REFRESH_TOKEN=existing_config.CODEX_REFRESH_TOKEN or get_codex_refresh_token_env(), CODEX_TOKEN_EXPIRES=existing_config.CODEX_TOKEN_EXPIRES or get_codex_token_expires_env(), CODEX_ACCOUNT_ID=existing_config.CODEX_ACCOUNT_ID or get_codex_account_id_env(), + CODEX_USERNAME=existing_config.CODEX_USERNAME or get_codex_username_env(), + CODEX_EMAIL=existing_config.CODEX_EMAIL or get_codex_email_env(), + CODEX_IS_PRO=( + existing_config.CODEX_IS_PRO + if existing_config.CODEX_IS_PRO is not None + else parse_bool_or_none(get_codex_is_pro_env()) + ), ) @@ -196,6 +209,12 @@ def update_env_with_user_config(): set_codex_token_expires_env(user_config.CODEX_TOKEN_EXPIRES) if user_config.CODEX_ACCOUNT_ID: set_codex_account_id_env(user_config.CODEX_ACCOUNT_ID) + if user_config.CODEX_USERNAME: + set_codex_username_env(user_config.CODEX_USERNAME) + if user_config.CODEX_EMAIL: + set_codex_email_env(user_config.CODEX_EMAIL) + if user_config.CODEX_IS_PRO is not None: + set_codex_is_pro_env(str(user_config.CODEX_IS_PRO)) def save_codex_tokens_to_user_config() -> None: @@ -220,6 +239,9 @@ def save_codex_tokens_to_user_config() -> None: existing["CODEX_REFRESH_TOKEN"] = get_codex_refresh_token_env() existing["CODEX_TOKEN_EXPIRES"] = get_codex_token_expires_env() existing["CODEX_ACCOUNT_ID"] = get_codex_account_id_env() + existing["CODEX_USERNAME"] = get_codex_username_env() + existing["CODEX_EMAIL"] = get_codex_email_env() + existing["CODEX_IS_PRO"] = parse_bool_or_none(get_codex_is_pro_env()) try: with open(user_config_path, "w") as f: diff --git a/electron/servers/nextjs/app/api/user-config/route.ts b/electron/servers/nextjs/app/api/user-config/route.ts index 80d306bf..680f6a44 100644 --- a/electron/servers/nextjs/app/api/user-config/route.ts +++ b/electron/servers/nextjs/app/api/user-config/route.ts @@ -50,6 +50,9 @@ export async function POST(request: Request) { CODEX_REFRESH_TOKEN: existingConfig.CODEX_REFRESH_TOKEN, CODEX_TOKEN_EXPIRES: existingConfig.CODEX_TOKEN_EXPIRES, CODEX_ACCOUNT_ID: existingConfig.CODEX_ACCOUNT_ID, + CODEX_USERNAME: existingConfig.CODEX_USERNAME, + CODEX_EMAIL: existingConfig.CODEX_EMAIL, + CODEX_IS_PRO: existingConfig.CODEX_IS_PRO, USE_CUSTOM_URL: userConfig.USE_CUSTOM_URL === undefined ? existingConfig.USE_CUSTOM_URL diff --git a/electron/servers/nextjs/components/CodexConfig.tsx b/electron/servers/nextjs/components/CodexConfig.tsx index c85d270e..2ecc0dfb 100644 --- a/electron/servers/nextjs/components/CodexConfig.tsx +++ b/electron/servers/nextjs/components/CodexConfig.tsx @@ -6,6 +6,7 @@ import { Loader2, RefreshCw, Trash2, + Crown, User, UserCheck, ArrowRight, @@ -34,6 +35,9 @@ type AuthStatus = "checking" | "unauthenticated" | "polling" | "authenticated"; interface StatusResponse { status: string; account_id?: string; + username?: string; + email?: string; + is_pro?: boolean; detail?: string; } @@ -60,6 +64,9 @@ export default function CodexConfig({ }: CodexConfigProps) { const [authStatus, setAuthStatus] = useState("checking"); const [accountId, setAccountId] = useState(null); + const [username, setUsername] = useState(null); + const [email, setEmail] = useState(null); + const [isPro, setIsPro] = useState(null); const [sessionId, setSessionId] = useState(null); const [manualCode, setManualCode] = useState(""); const [isExchanging, setIsExchanging] = useState(false); @@ -80,22 +87,32 @@ export default function CodexConfig({ return () => stopPolling(); }, []); + const applyProfile = (data: Partial) => { + setAccountId(data.account_id ?? null); + setUsername(data.username ?? null); + setEmail(data.email ?? null); + setIsPro(typeof data.is_pro === "boolean" ? data.is_pro : null); + }; + const checkCurrentAuthStatus = async () => { try { const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/status")); if (!res.ok) { setAuthStatus("unauthenticated"); + applyProfile({}); return; } const data: StatusResponse = await res.json(); if (data.status === "authenticated") { setAuthStatus("authenticated"); - setAccountId(data.account_id ?? null); + applyProfile(data); } else { setAuthStatus("unauthenticated"); + applyProfile({}); } } catch { setAuthStatus("unauthenticated"); + applyProfile({}); } }; @@ -125,7 +142,7 @@ export default function CodexConfig({ if (pollData.status === "success") { stopPolling(); setAuthStatus("authenticated"); - setAccountId(pollData.account_id ?? null); + applyProfile(pollData); setSessionId(null); if (!codexModel) { onInputChange(DEFAULT_CODEX_MODEL, "codex_model"); @@ -134,6 +151,7 @@ export default function CodexConfig({ } else if (pollData.status === "failed") { stopPolling(); setAuthStatus("unauthenticated"); + applyProfile({}); toast.error("Authentication failed. Please try again."); } } catch { @@ -143,6 +161,7 @@ export default function CodexConfig({ } catch (err) { toast.error("Failed to start sign-in flow"); setAuthStatus("unauthenticated"); + applyProfile({}); } }; @@ -162,7 +181,7 @@ export default function CodexConfig({ const data = await res.json(); stopPolling(); setAuthStatus("authenticated"); - setAccountId(data.account_id); + applyProfile(data); setSessionId(null); setManualCode(""); if (!codexModel) { @@ -189,6 +208,9 @@ export default function CodexConfig({ await fetch(getApiUrl("/api/v1/ppt/codex/auth/logout"), { method: "POST" }); setAuthStatus("unauthenticated"); setAccountId(null); + setUsername(null); + setEmail(null); + setIsPro(null); onInputChange("", "codex_model"); toast.success("Signed out from ChatGPT"); } catch { @@ -206,11 +228,12 @@ export default function CodexConfig({ }); if (!res.ok) throw new Error("Refresh failed"); const data = await res.json(); - if (data.account_id) setAccountId(data.account_id); + applyProfile(data); toast.success("Token refreshed successfully"); } catch { toast.error("Token refresh failed. Please sign in again."); setAuthStatus("unauthenticated"); + applyProfile({}); } finally { setIsRefreshing(false); } @@ -287,17 +310,39 @@ export default function CodexConfig({ } if (authStatus === "authenticated") { + const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown"; + return (
-
- {accountId && ( +
+

- Acc: {accountId} + {username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}

+ + {isPro === true ? ( + + ) : ( + + )} + {planLabel} + +
+ {email && username && ( +

{email}

+ )} + {!email && accountId && ( +

ID: {accountId}

)}

Signed in to ChatGPT

diff --git a/electron/servers/nextjs/components/SettingCodex.tsx b/electron/servers/nextjs/components/SettingCodex.tsx index dc0c31fd..5533fe85 100644 --- a/electron/servers/nextjs/components/SettingCodex.tsx +++ b/electron/servers/nextjs/components/SettingCodex.tsx @@ -6,6 +6,7 @@ import { Loader2, RefreshCw, Trash2, + Crown, User, UserCheck, } from "lucide-react"; @@ -33,6 +34,9 @@ type AuthStatus = "checking" | "unauthenticated" | "polling" | "authenticated"; interface StatusResponse { status: string; account_id?: string; + username?: string; + email?: string; + is_pro?: boolean; detail?: string; } @@ -59,6 +63,9 @@ export default function CodexConfig({ }: CodexConfigProps) { const [authStatus, setAuthStatus] = useState("checking"); const [accountId, setAccountId] = useState(null); + const [username, setUsername] = useState(null); + const [email, setEmail] = useState(null); + const [isPro, setIsPro] = useState(null); const [sessionId, setSessionId] = useState(null); const [manualCode, setManualCode] = useState(""); const [isExchanging, setIsExchanging] = useState(false); @@ -79,22 +86,32 @@ export default function CodexConfig({ return () => stopPolling(); }, []); + const applyProfile = (data: Partial) => { + setAccountId(data.account_id ?? null); + setUsername(data.username ?? null); + setEmail(data.email ?? null); + setIsPro(typeof data.is_pro === "boolean" ? data.is_pro : null); + }; + const checkCurrentAuthStatus = async () => { try { const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/status")); if (!res.ok) { setAuthStatus("unauthenticated"); + applyProfile({}); return; } const data: StatusResponse = await res.json(); if (data.status === "authenticated") { setAuthStatus("authenticated"); - setAccountId(data.account_id ?? null); + applyProfile(data); } else { setAuthStatus("unauthenticated"); + applyProfile({}); } } catch { setAuthStatus("unauthenticated"); + applyProfile({}); } }; @@ -122,7 +139,7 @@ export default function CodexConfig({ if (pollData.status === "success") { stopPolling(); setAuthStatus("authenticated"); - setAccountId(pollData.account_id ?? null); + applyProfile(pollData); setSessionId(null); if (!codexModel) { onInputChange(DEFAULT_CODEX_MODEL, "codex_model"); @@ -131,6 +148,7 @@ export default function CodexConfig({ } else if (pollData.status === "failed") { stopPolling(); setAuthStatus("unauthenticated"); + applyProfile({}); toast.error("Authentication failed. Please try again."); } } catch { @@ -140,6 +158,7 @@ export default function CodexConfig({ } catch (err) { toast.error("Failed to start sign-in flow"); setAuthStatus("unauthenticated"); + applyProfile({}); } }; @@ -159,7 +178,7 @@ export default function CodexConfig({ const data = await res.json(); stopPolling(); setAuthStatus("authenticated"); - setAccountId(data.account_id); + applyProfile(data); setSessionId(null); setManualCode(""); if (!codexModel) { @@ -185,7 +204,7 @@ export default function CodexConfig({ try { await fetch(getApiUrl("/api/v1/ppt/codex/auth/logout"), { method: "POST" }); setAuthStatus("unauthenticated"); - setAccountId(null); + applyProfile({}); onInputChange("", "codex_model"); toast.success("Signed out from ChatGPT"); } catch { @@ -203,11 +222,12 @@ export default function CodexConfig({ }); if (!res.ok) throw new Error("Refresh failed"); const data = await res.json(); - if (data.account_id) setAccountId(data.account_id); + applyProfile(data); toast.success("Token refreshed successfully"); } catch { toast.error("Token refresh failed. Please sign in again."); setAuthStatus("unauthenticated"); + applyProfile({}); } finally { setIsRefreshing(false); } @@ -266,15 +286,37 @@ export default function CodexConfig({ } if (authStatus === "authenticated") { + const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown"; + return (
-
+
- {accountId && ( +

- Acc: {accountId} + {username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}

+ + {isPro === true ? ( + + ) : ( + + )} + {planLabel} + +
+ {email && username && ( +

{email}

+ )} + {!email && accountId && ( +

ID: {accountId}

)}

Signed in to ChatGPT

diff --git a/electron/servers/nextjs/types/llm_config.ts b/electron/servers/nextjs/types/llm_config.ts index 7c0cc9de..493cd72c 100644 --- a/electron/servers/nextjs/types/llm_config.ts +++ b/electron/servers/nextjs/types/llm_config.ts @@ -49,6 +49,9 @@ export interface LLMConfig { CODEX_REFRESH_TOKEN?: string; CODEX_TOKEN_EXPIRES?: string; CODEX_ACCOUNT_ID?: string; + CODEX_USERNAME?: string; + CODEX_EMAIL?: string; + CODEX_IS_PRO?: boolean; // Only used in UI settings USE_CUSTOM_URL?: boolean; From a95a43a5a2a2f10b64fd91e6f87ca1d40fa57b32 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Fri, 3 Apr 2026 18:43:24 +0545 Subject: [PATCH 02/47] refactor: update Ui components in settings & templates --- .../(dashboard)/settings/ImageProvider.tsx | 2 +- .../(dashboard)/settings/SettingSideBar.tsx | 21 +-- .../(dashboard)/settings/TextProvider.tsx | 170 ++++++++---------- .../(dashboard)/settings/loading.tsx | 159 +++++++++------- .../components/CreateCustomTemplate.tsx | 4 +- .../templates/components/TemplatePanel.tsx | 138 ++++---------- .../components/TemplatePreviewComponents.tsx | 125 +++++++++++++ .../outline/components/CustomTemplateCard.tsx | 110 ++++-------- .../outline/components/TemplateSelection.tsx | 106 ++++------- .../presentation/components/SortableSlide.tsx | 7 - .../nextjs/app/hooks/useCustomTemplates.ts | 2 +- .../servers/nextjs/public/placeholder.jpg | Bin 0 -> 80776 bytes .../servers/nextjs/public/placeholder.svg | 7 + 13 files changed, 426 insertions(+), 425 deletions(-) create mode 100644 electron/servers/nextjs/app/(presentation-generator)/components/TemplatePreviewComponents.tsx create mode 100644 electron/servers/nextjs/public/placeholder.jpg create mode 100644 electron/servers/nextjs/public/placeholder.svg diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx index 5fbee460..1ad7983a 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx @@ -175,7 +175,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx index 84ea55de..d18db184 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx @@ -13,19 +13,20 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }

FILTER BY:

Select Mode

-
- + >Presenton +
- -
} { mode === 'nanobanana' &&
- - )}
- - - {/* Model Selection - only show if models are available */} - {selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? ( -
-
- -
- - - - - + {/* Model Selection - only show if models are available */} + {selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? ( +
+
+ +
+ + + + + + + + + No model found. + + {availableModels.map((model, index) => ( + { + if (currentModelField) { + onInputChange(value, currentModelField); + } + setOpenModelSelect(false); + }} + > + +
+
+
+ + {model} +
- - ))} - - - - - -
+
+ + ))} + + + + +
- ) : null} -
+
+ ) : null}
{/* Show message if no models found */} @@ -526,7 +520,7 @@ const TextProvider = ({ )} - {/* Web Grounding Toggle - show at the end, below models dropdown */} +
@@ -536,7 +530,6 @@ const TextProvider = ({

-
- -
- {/*
*/}
- -
- -
) } diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/loading.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/loading.tsx index 04f33049..114cde8b 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/loading.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/loading.tsx @@ -1,76 +1,113 @@ -import { Card } from "@/components/ui/card"; - -export default function LoadingProfile() { +function Shimmer({ className }: { className?: string }) { return ( -
- {/* Header Skeleton */} -
-
-
-
-
-
-
+
+ ); +} + +export default function LoadingSettings() { + return ( +
+
+ +
+ {/* SettingSideBar structure */} +
+
+ +
+
+ +
+ + +
+ +
+ {[0, 1].map((i) => ( +
+ + +
+ ))} +
+
+
+ +
+ +
-
- {/* Main Content Skeleton */} -
-
- {/* LLM Selection Content Skeleton */} -
- {/* Page Title */} -
-
-
+ {/* Main column — matches SettingPage + TextProvider default */} +
+
+
+ +
+
- {/* LLM Provider Cards */} -
- {[...Array(3)].map((_, index) => ( - -
-
-
-
-
-
-
-
-
-
- - {/* Configuration Fields */} -
- {[...Array(2)].map((_, fieldIndex) => ( -
-
-
-
- ))} -
- - ))} -
- - {/* Model Selection */} - -
-
-
+
+ {/* TextProvider top card: white panel, icon + copy left, controls right */} +
+
+ + + +
- +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + {/* TextProvider “Advanced” card */} +
+
+ + +
+
+ + +
+
- {/* Fixed Bottom Button Skeleton */} -
-
-
-
+ {/* Fixed save button — matches SettingPage placement */} +
+
); diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/CreateCustomTemplate.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/CreateCustomTemplate.tsx index 93f2a487..2c2d59b4 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/CreateCustomTemplate.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/CreateCustomTemplate.tsx @@ -11,7 +11,7 @@ const CreateCustomTemplate = () => { trackEvent(MixpanelEvent.Templates_Build_Template_Clicked); router.push('/custom-template') }} - className='w-full rounded-xl border border-[#EDEEEF] cursor-pointer font-syne'> + className='w-full rounded-[22px] border border-[#EDEEEF] cursor-pointer font-syne'>
{
-
+
diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx index ca6f9671..af936b0b 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx @@ -2,20 +2,24 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; -import { ArrowUpRight, ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react"; +import { ArrowUpRight, ChevronRight, Loader2 } from "lucide-react"; import { templates } from "@/app/presentation-templates"; -import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; +import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; import { useCustomTemplateSummaries, useCustomTemplatePreview, CustomTemplates, } from "@/app/hooks/useCustomTemplates"; -import { CompiledLayout } from "@/app/hooks/compileLayout"; import CreateCustomTemplate from "./CreateCustomTemplate"; import Link from "next/link"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; +import { + TemplatePreviewStage, + LayoutsBadge, + InbuiltTemplatePreview, + CustomTemplatePreview, +} from "../../../components/TemplatePreviewComponents"; -// Component for rendering custom template card with lazy-loaded previews export const CustomTemplateCard = React.memo(function CustomTemplateCard({ template }: { template: CustomTemplates }) { const router = useRouter(); const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`); @@ -26,73 +30,29 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ } else { router.push(`/template-preview?slug=custom-${template.id}`) } - } - , [router, template.id, template.name]); + }, [router, template.id, template.name]); return ( - - - - {totalLayouts} {totalLayouts === 1 ? 'Layout' : 'Layouts'} - -
- - {/* Layout previews */} -
- {loading ? ( - // Loading placeholders - [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( -
- -
- )) - ) : previewLayouts.length > 0 && ( - // Actual layout previews - previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - }) - )} -
- - -
-
-

- {template.name} -

- -
- - -
+ + + + +
+

{template.name}

+
); }, (prev, next) => { - // Custom templates may be refetched, producing new object references; compare on fields we render/use. return ( - prev.template.id === next.template.id && prev.template.id === next.template.id && prev.template.name === next.template.name && prev.template.layoutCount === next.template.layoutCount @@ -106,54 +66,24 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({ template: TemplateLayoutsWithSettings; onOpen: (id: string) => void; }) { - const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]); const handleOpen = useCallback(() => onOpen(template.id), [onOpen, template.id]); return ( - - {template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'} - - -
-
- {previewLayouts.map((layout: TemplateWithData, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - })} -
-
-
-
- -

- {template.name} -

-

- {template.description} -

-
-
- - + + + + +
+
+

{template.name}

+

{template.description}

+
); @@ -254,7 +184,7 @@ const LayoutPreview = () => { {/* Inbuilt Templates Section: non-neo first, then Report (neo) */} {tab === 'default' && (
-
+
{nonNeoInbuilt.map((template) => ( {

Report

-
+
{neoInbuilt.map((template) => ( { Loading custom templates...
) : ( -
+
{customTemplateCards}
diff --git a/electron/servers/nextjs/app/(presentation-generator)/components/TemplatePreviewComponents.tsx b/electron/servers/nextjs/app/(presentation-generator)/components/TemplatePreviewComponents.tsx new file mode 100644 index 00000000..7a3f0bd2 --- /dev/null +++ b/electron/servers/nextjs/app/(presentation-generator)/components/TemplatePreviewComponents.tsx @@ -0,0 +1,125 @@ +"use client"; +import React, { memo, useMemo } from "react"; +import { Loader2 } from "lucide-react"; +import { TemplateWithData } from "@/app/presentation-templates/utils"; +import { CompiledLayout } from "@/app/hooks/compileLayout"; + + + + +export function TemplatePreviewStage({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +export const LayoutsBadge = memo(function LayoutsBadge({ count }: { count: number }) { + return ( + + Layouts-{count} + + ); +}); + +export const ScaledSlidePreview = memo(function ScaledSlidePreview({ + children, + id, + index, + isOutline = false, +}: { + children: React.ReactNode; + id: string; + index: number; + isOutline?: boolean; +}) { + const PREVIEW_SCALE = isOutline ? 0.2 : 0.24; + const SLIDE_HEIGHT = 720 * PREVIEW_SCALE; + const SLIDE_WIDTH = 1280; + const SLIDE_NATIVE_HEIGHT = 720; + return ( +
+
+ {children} +
+
+ ); +}); + +export const InbuiltTemplatePreview = memo(function InbuiltTemplatePreview({ + layouts, + templateId, + isOutline = false, +}: { + layouts: TemplateWithData[]; + templateId: string; + isOutline?: boolean; +}) { + const previewLayouts = useMemo(() => layouts.slice(0, 2), [layouts]); + return ( +
+ {previewLayouts.map((layout, index) => { + const LayoutComponent = layout.component; + return ( + + + + ); + })} +
+ ); +}); + +export const CustomTemplatePreview = memo(function CustomTemplatePreview({ + previewLayouts, + loading, + templateId, + isOutline = false, +}: { + previewLayouts: CompiledLayout[]; + loading: boolean; + templateId: string; + isOutline?: boolean; +}) { + return ( +
+ {loading ? ( + [...Array(2)].map((_, index) => ( +
+ +
+ )) + ) : ( + previewLayouts.slice(0, 2).map((layout, index) => { + const LayoutComponent = layout.component; + return ( + + + + ); + }) + )} +
+ ); +}); diff --git a/electron/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx b/electron/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx index 6ae8c5c9..89daa4da 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx @@ -1,94 +1,50 @@ "use client"; import React, { memo } from "react"; import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates"; -import { Loader2 } from "lucide-react"; -import { CompiledLayout } from "@/app/hooks/compileLayout"; +import { + TemplatePreviewStage, + LayoutsBadge, + CustomTemplatePreview, +} from "../../components/TemplatePreviewComponents"; -// Memoized preview component to prevent re-renders during scroll -export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); -}); -LayoutPreview.displayName = 'LayoutPreview'; - -export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => { - - const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id); +export const CustomTemplateCard = memo(function CustomTemplateCard({ + template, + onSelectTemplate, + selectedTemplate, +}: { + template: CustomTemplates; + onSelectTemplate: (template: string) => void; + selectedTemplate: string | null; +}) { + const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id); const isSelected = selectedTemplate === template.id; return ( - onSelectTemplate(template.id)} > - - - - Layouts- {totalLayouts} - -
- - {/* Layout previews */} -
- {customLoading ? ( - // Loading placeholders - [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( -
- -
- )) - ) : previewLayouts.length > 0 && ( - // Actual layout previews - previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - }) - )} -
- - -
-
-

+ + + + +
+

{template.name}

- -
); }); -CustomTemplateCard.displayName = 'CustomTemplateCard'; - diff --git a/electron/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx b/electron/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx index 5afe9425..4bf94643 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx @@ -4,72 +4,49 @@ import React, { useEffect, useMemo, useCallback, memo } from "react"; import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; import { templates } from "@/app/presentation-templates"; import { Card } from "@/components/ui/card"; -import { TemplateWithData } from "@/app/presentation-templates/utils"; +import { cn } from "@/lib/utils"; import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates"; import { Loader2 } from "lucide-react"; -import { CustomTemplateCard } from "./CustomTemplateCard"; + import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate"; +import { CustomTemplateCard } from "./CustomTemplateCard"; +import { + TemplatePreviewStage, + LayoutsBadge, + InbuiltTemplatePreview, +} from "../../components/TemplatePreviewComponents"; -// Memoized layout preview for built-in templates -const BuiltInLayoutPreview = memo(({ layout, templateId, index }: { - layout: TemplateWithData; - templateId: string; - index: number; -}) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); -}); -BuiltInLayoutPreview.displayName = 'BuiltInLayoutPreview'; - -// Memoized built-in template card -const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: { +const BuiltInTemplateCard = memo(function BuiltInTemplateCard({ + template, + isSelected, + onSelect, +}: { template: TemplateLayoutsWithSettings; isSelected: boolean; onSelect: (template: TemplateLayoutsWithSettings) => void; -}) => { - const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]); +}) { const handleClick = useCallback(() => onSelect(template), [onSelect, template]); return ( - - {template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'} - - -
-
- {previewLayouts.map((layout: TemplateWithData, index: number) => ( - - ))} -
-
-
-
+ + + + +
+

{template.name}

-

+

{template.description}

@@ -77,17 +54,16 @@ const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: { ); }); -BuiltInTemplateCard.displayName = 'BuiltInTemplateCard'; interface TemplateSelectionProps { selectedTemplate: (TemplateLayoutsWithSettings | string) | null; onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void; } -const TemplateSelection: React.FC = memo(({ +const TemplateSelection: React.FC = memo(function TemplateSelection({ selectedTemplate, - onSelectTemplate -}) => { + onSelectTemplate, +}) { useEffect(() => { const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' @@ -102,50 +78,44 @@ const TemplateSelection: React.FC = memo(({ const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries(); - // Stable callback for custom template selection const handleCustomSelect = useCallback( (template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template), [onSelectTemplate] ); - // Stable callback for built-in template selection const handleBuiltInSelect = useCallback( (template: TemplateLayoutsWithSettings) => onSelectTemplate(template), [onSelectTemplate] ); - // Derive the selected custom template id only when selectedTemplate changes const selectedCustomId = useMemo( - () => (typeof selectedTemplate === 'string' ? selectedTemplate : null), + () => (typeof selectedTemplate === "string" ? selectedTemplate : null), [selectedTemplate] ); - // Derive the selected built-in template id only when selectedTemplate changes const selectedBuiltInId = useMemo( - () => (typeof selectedTemplate !== 'string' ? selectedTemplate?.id ?? null : null), + () => (typeof selectedTemplate !== "string" ? selectedTemplate?.id ?? null : null), [selectedTemplate] ); - // Memoize the custom templates section const customTemplateCards = useMemo(() => { if (customLoading) { return (
- + Loading custom templates...
); } if (customTemplates.length === 0) { return ( -
- +
); } return ( -
+
{customTemplates.map((template: CustomTemplates) => ( = memo(({ ); }, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]); - // Memoize the built-in templates list const builtInTemplateCards = useMemo( () => templates.map((template: TemplateLayoutsWithSettings) => ( @@ -174,23 +143,20 @@ const TemplateSelection: React.FC = memo(({ return (
- {/* Custom AI Templates */}

Custom

{customTemplateCards}
- {/* In Built Templates */}

In Built

-
+
{builtInTemplateCards}
); }); -TemplateSelection.displayName = 'TemplateSelection'; export default TemplateSelection; diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx index 92068719..f189e66d 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx @@ -73,14 +73,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
- {/*
-
-
- -
-
*/}
); } \ No newline at end of file diff --git a/electron/servers/nextjs/app/hooks/useCustomTemplates.ts b/electron/servers/nextjs/app/hooks/useCustomTemplates.ts index 641cf1f0..f619c730 100644 --- a/electron/servers/nextjs/app/hooks/useCustomTemplates.ts +++ b/electron/servers/nextjs/app/hooks/useCustomTemplates.ts @@ -403,7 +403,7 @@ export function useCustomTemplatePreview(presentationId: string) { setTotalLayouts(data.layouts.length); // Compile first 4 layouts for preview const compiled: CompiledLayout[] = []; - const layoutsToPreview = data.layouts.slice(0, 4); + const layoutsToPreview = data.layouts.slice(0, 2); for (const layout of layoutsToPreview) { try { diff --git a/electron/servers/nextjs/public/placeholder.jpg b/electron/servers/nextjs/public/placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9477065c50491b95f79c6fe84cc94995ff951140 GIT binary patch literal 80776 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