refactor: Update Onboarding UI

This commit is contained in:
shiva raj badu 2026-04-16 19:00:24 +05:45
parent cfc7233447
commit 41f9eae61d
No known key found for this signature in database
25 changed files with 997 additions and 845 deletions

View file

@ -307,7 +307,7 @@ const TextProvider = ({
<CommandList>
<CommandEmpty>No provider found.</CommandEmpty>
<CommandGroup>
{Object.values(LLM_PROVIDERS).map(
{Object.values([{ value: 'codex', label: 'ChatGPT', description: 'ChatGPT Plus/Pro via OAuth', icon: '/providers/openai.png' }, ...Object.values(LLM_PROVIDERS)]).map(
(provider, index) => (
<CommandItem
key={index}

View file

@ -90,6 +90,8 @@ export default function CodexConfig({
}
const data: StatusResponse = await res.json();
if (data.status === "authenticated") {
onInputChange('chatgpt', 'LLM');
onInputChange(DEFAULT_CODEX_MODEL, 'codex_model');
setAuthStatus("authenticated");
applyProfile(data);
} else {
@ -106,7 +108,7 @@ export default function CodexConfig({
try {
trackEvent(MixpanelEvent.Codex_SignIn_API_Call);
onInputChange('codex', 'LLM');
onInputChange('chatgpt', 'LLM');
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/initiate"), {
method: "POST",
@ -199,6 +201,7 @@ export default function CodexConfig({
setUsername(null);
setEmail(null);
setIsPro(null);
onInputChange("openai", "LLM");
onInputChange("", "codex_model");
toast.success("Signed out from ChatGPT");
} catch {
@ -229,13 +232,13 @@ export default function CodexConfig({
if (authStatus === "checking") {
return (
<div className="mb-5 w-full p-3 bg-[#010100] font-syne rounded-[8px] flex items-center gap-6">
<div className="mb-5 w-full p-3 border border-[#EDEEEF] font-syne rounded-[8px] flex items-center gap-6">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-10 h-10 text-white animate-spin" />
<Loader2 className="w-10 h-10 text-[#191919] animate-spin" />
</div>
<div className="text-start flex-1 min-w-0">
<h4 className="text-white text-lg font-medium">Checking status</h4>
<p className="text-[#808080] text-sm font-normal">
<h4 className="text-[#191919] text-lg font-medium">Checking status</h4>
<p className="text-[#B3B3B3] text-sm font-normal">
Verifying your ChatGPT connection
</p>
</div>
@ -246,14 +249,14 @@ export default function CodexConfig({
if (authStatus === "polling") {
return (
<div className="mb-5 space-y-4 font-syne">
<div className="w-full p-3 bg-[#010100] rounded-[8px] flex items-center justify-between gap-4">
<div className="w-full p-3 border border-[#EDEEEF] rounded-[8px] flex items-center justify-between gap-4">
<div className="flex items-center gap-6 min-w-0 flex-1">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-10 h-10 text-white animate-spin" />
<div className="w-[40px] h-[40px] bg-[#EDEEEF] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-5 h-5 text-[#191919] animate-spin" />
</div>
<div className="text-start min-w-0">
<h4 className="text-white text-lg font-medium">Waiting for sign-in</h4>
<p className="text-[#808080] text-sm font-normal">
<h4 className="text-[#191919] text-lg font-medium">Waiting for sign-in</h4>
<p className="text-[#B3B3B3] text-sm font-normal">
Complete sign-in in the browser tab we opened.
</p>
</div>
@ -261,21 +264,21 @@ export default function CodexConfig({
<button
type="button"
onClick={handleCancelPolling}
className="shrink-0 text-sm text-[#808080] hover:text-white underline underline-offset-2 transition-colors"
className="shrink-0 text-sm text-[#B3B3B3] hover:text-[#191919] underline underline-offset-2 transition-colors"
>
Cancel
</button>
</div>
<div className="space-y-2 rounded-[8px] border border-[#333333] bg-[#010100] p-3">
<p className="text-white text-xs font-normal">
<div className="space-y-2 rounded-[8px] border border-[#EDEEEF] p-3">
<p className="text-[#191919] text-xs font-normal">
Paste redirect URL or code if you were not redirected automatically
</p>
<div className="flex gap-2">
<input
type="text"
placeholder="Paste URL or code…"
className="flex-1 min-w-0 px-3 py-2.5 outline-none border border-[#333333] rounded-[8px] bg-[#1a1a1a] text-sm text-white placeholder:text-[#666666] focus:border-[#555555] transition-colors"
className="flex-1 min-w-0 px-3 py-2.5 outline-none border border-[#EDEEEF] rounded-[8px] text-sm text-[#191919] placeholder:text-[#666666] focus:border-[#555555] transition-colors"
value={manualCode}
onChange={(e) => setManualCode(e.target.value)}
/>
@ -283,7 +286,7 @@ export default function CodexConfig({
type="button"
onClick={handleManualExchange}
disabled={isExchanging || !manualCode.trim()}
className="shrink-0 px-4 py-2.5 bg-[#333333] hover:bg-[#444444] disabled:opacity-40 disabled:hover:bg-[#333333] rounded-[8px] text-sm font-medium text-white transition-colors flex items-center justify-center min-w-[88px]"
className="shrink-0 px-4 py-2.5 bg-[#EDEEEF] hover:bg-[#E4E5E6] disabled:opacity-40 disabled:hover:bg-[#EDEEEF] rounded-[8px] text-sm font-medium text-[#191919] transition-colors flex items-center justify-center min-w-[88px]"
>
{isExchanging ? (
<Loader2 className="w-5 h-5 animate-spin" />
@ -298,28 +301,27 @@ export default function CodexConfig({
}
if (authStatus === "authenticated") {
const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown";
return (
<div className=" mb-5">
<div className="flex items-center justify-between gap-3 p-5 border border-[#EDEEEF] rounded-[8px]">
<div className="flex items-center gap-3">
<UserCheck className="w-6 h-6 text-black shrink-0" />
<UserCheck className="w-6 h-6 text-[#191919] shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">
<p className="text-sm font-medium text-[#191919] truncate">
{username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}
</p>
</div>
{email && username && (
<p className="text-xs text-gray-500 truncate">{email}</p>
<p className="text-xs text-[#B3B3B3] truncate">{email}</p>
)}
{!email && accountId && (
<p className="text-xs text-gray-500 truncate">ID: {accountId}</p>
<p className="text-xs text-[#B3B3B3] truncate">ID: {accountId}</p>
)}
<p className="text-xs text-gray-400">Signed in to ChatGPT</p>
<p className="text-xs text-[#B3B3B3]">Signed in to ChatGPT</p>
</div>
</div>
<div className="flex gap-1.5 shrink-0">
@ -330,9 +332,9 @@ export default function CodexConfig({
className="flex items-center justify-center px-3.5 py-2.5 bg-[#EDEEEF] rounded-[58px] minid:opacity-40 transition-colors"
>
{isRefreshing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin text-black" />
<Loader2 className="w-3.5 h-3.5 animate-spin text-[#191919]" />
) : (
<RefreshCw className="w-3.5 h-3.5 text-black" />
<RefreshCw className="w-3.5 h-3.5 text-[#191919]" />
)}
</button>
<button
@ -342,9 +344,9 @@ export default function CodexConfig({
className="flex items-center justify-center px-3.5 py-2.5 bg-[#EDEEEF] rounded-[58px] hover:bg-[#E4E5E6] disabled:opacity-40 transition-colors"
>
{isLoggingOut ? (
<Loader2 className="w-3.5 h-3.5 animate-spin text-black" />
<Loader2 className="w-3.5 h-3.5 animate-spin text-[#191919]" />
) : (
<Trash2 className="w-3.5 h-3.5 text-black" />
<Trash2 className="w-3.5 h-3.5 text-[#191919]" />
)}
</button>
</div>
@ -358,19 +360,19 @@ export default function CodexConfig({
return (
<button
onClick={handleSignIn}
className="mb-5 w-full p-3 bg-[#010100] font-syne rounded-[8px] flex items-center justify-between "
className=" w-full p-5 border border-[#EDEEEF] font-syne rounded-[12px] flex items-center justify-between "
>
<div className="flex items-center gap-6">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center" >
<div className="flex items-center gap-2 flex-1">
<div className="w-[40px] h-[40px] bg-[#333333] rounded-full flex items-center justify-center" >
<img src="/providers/OpenAI-white.png" alt="openai Logo" className="w-[52px] h-[52px]" />
<img src="/providers/OpenAI-white.png" alt="openai Logo" className="w-[27px] h-[27px]" />
</div>
<div className="text-start">
<h4 className="text-white text-lg font-medium">Sign in with ChatGPT</h4>
<p className="text-[#808080] text-sm font-normal">Use your ChatGPT account no API <br /> key required</p>
<div className="text-start flex-1">
<h4 className="text-[#191919] text-sm font-medium">Sign in with ChatGPT</h4>
<p className="text-[#B3B3B3] text-xs font-normal">Use your ChatGPT account no API key required</p>
</div>
</div>
<ArrowRight className="w-[22px] h-[22px] text-white" />
<ArrowRight className="w-[22px] h-[22px] text-[#4C4C4C]" />
</button>
);
}

View file

@ -3,7 +3,7 @@ import React from 'react'
const OnBoardingHeader = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => {
return (
<div className='relative z-20 flex items-center font-syne justify-end gap-1 mt-7 mb-[52px]'>
<div className='sticky top-8 z-20 flex items-center font-syne justify-end gap-1 mt-7 mb-[52px]'>
<div className='flex items-center gap-1 cursor-pointer'
onClick={() => {

View file

@ -3,7 +3,7 @@ import React from 'react'
const OnBoardingSlidebar = ({ step }: { step: number }) => {
return (
<div className={`${step === 3 ? "bg-white" : "bg-[#F6F6F9]"} w-[300px] relative`}>
<img src="/Logo.png" alt="Presenton logo" className="absolute top-0 left-0 w-[128px] m-6" />
<img src="/Logo.png" alt="Presenton logo" className="sticky top-6 left-0 w-[128px] m-6" />
{step !== 3 && <svg xmlns="http://www.w3.org/2000/svg" width="296" height="591" viewBox="0 0 296 591" fill="none">
<path d="M291.5 183.5C311.916 183.5 328.5 200.271 328.5 221C328.5 241.729 311.916 258.5 291.5 258.5C271.084 258.5 254.5 241.729 254.5 221C254.5 200.271 271.084 183.5 291.5 183.5Z" stroke="#EDEEEF" strokeWidth="3" />
<path d="M291.5 131.238C340.408 131.238 380.089 171.407 380.09 220.998C380.09 270.589 340.408 310.758 291.5 310.758C242.591 310.758 202.91 270.589 202.91 220.998C202.91 171.407 242.591 131.238 291.5 131.238Z" stroke="#EDEEEF" strokeWidth="3" />

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Button } from '../ui/button';
import { Check, CheckCircle, ChevronLeft, ChevronUp, Download, Eye, EyeOff, Loader2 } from 'lucide-react';
import { ArrowUpRight, Check, CheckCircle, ChevronLeft, ChevronUp, Download, Eye, EyeOff, Info, Loader2 } from 'lucide-react';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
import { DALLE_3_QUALITY_OPTIONS, GPT_IMAGE_1_5_QUALITY_OPTIONS, IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants';
import { cn } from '@/lib/utils';
@ -231,7 +231,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
DALL·E 3 Image Quality
</label>
<div className="">
<Select value={llmConfig.DALL_E_3_QUALITY} onValueChange={(value) => setLlmConfig((prev) => ({
<Select value={llmConfig.DALL_E_3_QUALITY || 'standard'} onValueChange={(value) => setLlmConfig((prev) => ({
...prev,
DALL_E_3_QUALITY: value
}))}>
@ -258,7 +258,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</label>
<div className="">
<Select
value={llmConfig.GPT_IMAGE_1_5_QUALITY}
value={llmConfig.GPT_IMAGE_1_5_QUALITY || 'low'}
onValueChange={(value) => setLlmConfig((prev) => ({
...prev,
GPT_IMAGE_1_5_QUALITY: value
@ -350,25 +350,20 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
}, [llmConfig.LLM, modelsChecked, modelsLoading]);
return (
<div className='w-full max-w-[640px] font-syne'>
<div className='w-full max-w-[660px] font-syne pb-10'>
<p className='px-2.5 py-0.5 w-fit text-[#7A5AF8] rounded-[50px] border border-[#EDEEEF] text-[10px] font-medium mb-5 font-syne'>PRESENTON</p>
<div className='mb-[54px]'>
<div className=''>
<h2 className='mb-4 text-black text-[26px] font-normal font-unbounded '>Choose your content providers</h2>
<p className='text-[#000000CC] text-xl font-normal font-syne'>Select the AI engines that will generate your slide text and visuals.</p>
</div>
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig(prev => ({
...prev,
[normalizedField]: value
}));
}}
/>
<div className='flex items-center gap-2 bg-[#F0F3F9B2] rounded-[8px] px-6 py-2.5 my-[54px]'>
<Info className='w-4 h-4 fill-[#003399] stroke-white' />
<p className='text-sm text-[#5F6062] font-medium'>Runs locally on your device. Your API keys and generation setup stay on your machine.</p>
</div>
{/* Text Provider */}
<div className='p-3 border border-[#EDEEEF] rounded-[11px] '>
<div className='p-3 border border-[#EDEEEF] rounded-[11px] bg-white '>
<div className="flex items-center gap-[24.3px] mb-[42px]">
<div className='w-[74px] h-[74px] rounded-[4px] pt-[16.8px] pr-[17.15px] pb-[17.2px] pl-[16.85px] flex items-center justify-center'
style={{ backgroundColor: '#4C55541A' }}
@ -387,7 +382,22 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</p>
</div>
</div>
<div className='flex items-start gap-4 '>
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig(prev => ({
...prev,
[normalizedField]: value
}));
}}
/>
<div className='flex items-center gap-2.5 my-[30px]'>
<div className='w-full h-[1px] bg-[#E1E1E5]' />
<p className='text-xs font-normal text-[#999999]'>OR</p>
<div className='w-full h-[1px] bg-[#E1E1E5]' />
</div>
<div className='flex flex-col items-start gap-4 '>
<div className="flex flex-col justify-start w-full ">
<label className="block text-sm font-medium text-gray-700 mb-2">
@ -416,8 +426,8 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[215px] "
align="start"
className="p-0 w-full "
align="end"
>
<Command>
@ -473,7 +483,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
USE_CUSTOM_URL: true,
OLLAMA_URL: prev.OLLAMA_URL || 'http://localhost:11434'
}))}
className="mt-8 py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border border-[#EDEEEF] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
className="py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border border-[#EDEEEF] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
>
Use Ollama URL
</button>
@ -508,7 +518,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</>
)}
</>
) : llmConfig.LLM === 'codex' ? (
) : llmConfig.LLM === 'chatgpt' ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select GPT Model
@ -570,9 +580,14 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
) : (
<>
<label className="block text-sm font-medium capitalize text-gray-700 mb-2">
{llmConfig.LLM === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
</label>
<div className='flex items-center justify-between mb-2'>
<label className="block text-sm font-medium capitalize text-gray-700 ">
{llmConfig.LLM === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
</label>
{llmConfig.LLM && LLM_PROVIDERS[llmConfig.LLM!]?.getApiKeyUrl && <a href={LLM_PROVIDERS[llmConfig.LLM!]?.getApiKeyUrl || ""} target='_blank' className='text-[#666666] text-xs font-normal flex items-center gap-1'>Get API Key <ArrowUpRight className='w-3.5 h-3.5' /></a>}
</div>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
@ -611,7 +626,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
{llmConfig.LLM !== 'ollama' && llmConfig.LLM !== 'codex' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
{llmConfig.LLM !== 'ollama' && llmConfig.LLM !== 'codex' && llmConfig.LLM !== 'chatgpt' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
onClick={fetchAvailableModels}
@ -622,9 +637,9 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
(llmConfig.LLM === 'anthropic' && !currentApiKey) ||
(llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL)
}
className={`mt-4 py-2.5 bg-[#EDEEEF] disabled:opacity-50 disabled:cursor-not-allowed px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
className={`mt-4 py-2.5 bg-[#EDEEEF] disabled:opacity-50 disabled:cursor-not-allowed px-3.5 w-full rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-[#101323] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
: " border-[#EDEEEF] text-[#101323] hover:bg-[#EDEEEF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
>
{modelsLoading ? (
@ -633,7 +648,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
Checking for models...
</span>
) : (
"Check models"
"Validate & Load Models"
)}
</button>
)}
@ -641,7 +656,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
<div className='flex items-start gap-4 mt-4'>
<p className='text-sm font-medium text-gray-700 mb-2 w-full'></p>
{/* Model Selection - only show if models are available */}
{llmConfig.LLM !== 'codex' && modelsChecked && availableModels.length > 0 && (
@ -729,7 +744,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</div>
{/* Image Provider */}
<div className={`p-3 border border-[#EDEEEF] rounded-[11px] relative mt-5 ${llmConfig.DISABLE_IMAGE_GENERATION ? "bg-[#F9FAFB]" : ""}`}>
<div className={`p-3 border border-[#EDEEEF] rounded-[11px] relative mt-5 bg-white ${llmConfig.DISABLE_IMAGE_GENERATION ? "bg-[#F9FAFB]" : ""}`}>
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center absolute top-3 right-3'>
<div className='flex justify-end items-center'>
<Switch
@ -758,7 +773,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</div>
{!llmConfig.DISABLE_IMAGE_GENERATION && (
<div className='flex gap-4'>
<div className='flex flex-col gap-4'>
{/* Image Provider Selection */}
<div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-2">
@ -884,9 +899,13 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
// Show API key input for other providers
return (
<div className="w-full ">
<label className="block text-sm font-medium text-gray-700 mb-2">
{provider.apiKeyFieldLabel}
</label>
<div className='flex items-center justify-between mb-2'>
<label className="block text-sm font-medium text-gray-700">
{provider.apiKeyFieldLabel}
</label>
{provider.getApiKeyUrl && <a href={provider.getApiKeyUrl || ""} target='_blank' className='text-[#666666] text-xs font-normal flex items-center gap-1'>Get API Key <ArrowUpRight className='w-3.5 h-3.5' /></a>}
</div>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
@ -917,9 +936,9 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
)}
{!llmConfig.DISABLE_IMAGE_GENERATION && <div className='flex justify-end items-center mt-[18px]'>
{!llmConfig.DISABLE_IMAGE_GENERATION && <div className='flex flex-col justify-end items-center mt-[18px]'>
<div className='w-full flex items-center gap-4'>
<p className='w-full'></p>
{renderQualitySelector(llmConfig)}
</div>
{llmConfig.IMAGE_PROVIDER === "comfyui" && <div className='w-full'>

View file

@ -14,6 +14,7 @@ export interface ImageProviderOption {
requiresApiKey?: boolean;
apiKeyField?: string;
apiKeyFieldLabel?: string;
getApiKeyUrl?: string;
}
export interface LLMProviderOption {
@ -24,8 +25,10 @@ export interface LLMProviderOption {
model_label?: string;
url?: string;
icon?: string;
getApiKeyUrl?: string;
}
export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
pexels: {
value: "pexels",
@ -35,6 +38,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "PEXELS_API_KEY",
apiKeyFieldLabel: "Pexels API Key",
getApiKeyUrl: "https://docs.presenton.ai/help/get-api-keys/get-pexels-api-key",
},
pixabay: {
value: "pixabay",
@ -44,6 +48,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "PIXABAY_API_KEY",
apiKeyFieldLabel: "Pixabay API Key",
getApiKeyUrl: "https://docs.presenton.ai/help/get-api-keys/get-pixabay-api-keyhttps://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
"dall-e-3": {
value: "dall-e-3",
@ -53,6 +58,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "OPENAI_API_KEY",
apiKeyFieldLabel: "OpenAI API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
"gpt-image-1.5": {
value: "gpt-image-1.5",
@ -62,6 +68,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "OPENAI_API_KEY",
apiKeyFieldLabel: "OpenAI API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
gemini_flash: {
value: "gemini_flash",
@ -71,6 +78,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "GOOGLE_API_KEY",
apiKeyFieldLabel: "Google API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
nanobanana_pro: {
value: "nanobanana_pro",
@ -80,6 +88,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
requiresApiKey: true,
apiKeyField: "GOOGLE_API_KEY",
apiKeyFieldLabel: "Google API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
comfyui: {
value: "comfyui",
@ -93,18 +102,20 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
};
export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
codex: {
value: "codex",
label: "ChatGPT",
description: "ChatGPT Plus/Pro via OAuth",
icon: "/providers/openai.png",
},
// codex: {
// value: "codex",
// label: "ChatGPT",
// description: "ChatGPT Plus/Pro via OAuth",
// icon: "/providers/openai.png",
// getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
// },
openai: {
value: "openai",
label: "OpenAI",
description: "OpenAI's latest text generation model",
url: "https://api.openai.com/v1",
icon: "/providers/openai.png",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
google: {
value: "google",
@ -112,6 +123,7 @@ export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
description: "Google's primary text generation model",
url: "https://api.google.com/v1",
icon: "/providers/gemini-color.svg",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
anthropic: {
value: "anthropic",
@ -119,6 +131,7 @@ export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
description: "Anthropic's Claude models",
url: "https://api.anthropic.com/v1",
icon: "/providers/claude-color.svg",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+anthropic+api+key&sxsrf=ANbL-n7lsueZQ88L56HhqC1ch2PGD0rbNQ%3A1776339632265",
},
ollama: {
value: "ollama",

View file

@ -57,7 +57,7 @@ export const getLLMConfigValidationError = (
if (!isProvided(llmConfig.CUSTOM_MODEL)) {
return 'No model selected for your custom endpoint. Use "Check models" after entering the URL, then choose a model.';
}
} else if (llm === "codex") {
} else if (llm === "codex" || llm === "chatgpt") {
if (!isProvided(llmConfig.CODEX_MODEL)) {
return "Select a Codex model.";
}
@ -151,7 +151,7 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
if (validationError) {
throw new Error(validationError);
}
// Check if running in Electron environment
if (typeof window !== 'undefined' && window.electron?.setUserConfig) {
// Use Electron IPC handler

View file

@ -60,7 +60,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
DALL·E 3 Image Quality
</label>
<div className="">
<Select value={llmConfig.DALL_E_3_QUALITY} onValueChange={(value) => input_field_changed(value, "DALL_E_3_QUALITY")}>
<Select value={llmConfig.DALL_E_3_QUALITY || 'standard'} onValueChange={(value) => input_field_changed(value, "DALL_E_3_QUALITY")}>
<SelectTrigger className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between">
<SelectValue placeholder="Select a quality" />
</SelectTrigger>
@ -84,7 +84,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
</label>
<div className="">
<Select
value={llmConfig.GPT_IMAGE_1_5_QUALITY}
value={llmConfig.GPT_IMAGE_1_5_QUALITY || 'low'}
onValueChange={(value) => input_field_changed(value, "GPT_IMAGE_1_5_QUALITY")}
>
<SelectTrigger
@ -175,7 +175,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
style={{ width: "300px" }}
>
<Command>
<CommandInput placeholder="Search provider..." />

View file

@ -117,6 +117,7 @@ export default function CodexConfig({
const handleSignIn = async () => {
try {
onInputChange('codex', 'LLM');
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/initiate"), {
method: "POST",
});

View file

@ -19,7 +19,11 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import SettingSideBar from "./SettingSideBar";
import TextProvider from "./TextProvider";
import ImageProvider from "./ImageProvider";
import PrivacySettings from "./PrivacySettings";
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
import { ImagesApi } from "@/app/(presentation-generator)/services/api/images";
const STOCK_IMAGE_PROVIDERS = new Set(["pexels", "pixabay"]);
// Button state interface
interface ButtonState {
@ -35,7 +39,7 @@ const SettingsPage = () => {
const router = useRouter();
const pathname = usePathname();
const [mode, setMode] = useState<'nanobanana' | 'presenton'>('presenton')
const [selectedProvider, setSelectedProvider] = useState<'text-provider' | 'image-provider'>('text-provider')
const [selectedProvider, setSelectedProvider] = useState<'text-provider' | 'image-provider' | 'privacy'>('text-provider')
const userConfigState = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(
userConfigState.llm_config
@ -71,6 +75,36 @@ const SettingsPage = () => {
return 0;
}, [downloadingModel?.downloaded, downloadingModel?.size]);
const ensureSelectedStockProviderReady = async (): Promise<boolean> => {
if (llmConfig.DISABLE_IMAGE_GENERATION) {
return true;
}
const provider = (llmConfig.IMAGE_PROVIDER || "").toLowerCase();
if (!STOCK_IMAGE_PROVIDERS.has(provider)) {
return true;
}
const providerApiKey =
provider === "pexels" ? llmConfig.PEXELS_API_KEY : llmConfig.PIXABAY_API_KEY;
try {
await ImagesApi.searchStockImages("business", 1, {
provider,
apiKey: providerApiKey,
strictApiKey: true,
});
return true;
} catch (error: any) {
notify.error(
"Cannot save settings",
error?.message ||
`Unable to reach ${provider} with the provided API key. Please verify your settings and try again.`
);
return false;
}
};
const handleSaveConfig = async () => {
trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname });
const validationError = getLLMConfigValidationError(llmConfig);
@ -78,6 +112,12 @@ const SettingsPage = () => {
notify.error("Cannot save settings", validationError);
return;
}
const providerReady = await ensureSelectedStockProviderReady();
if (!providerReady) {
return;
}
try {
setButtonState(prev => ({
...prev,
@ -118,8 +158,7 @@ const SettingsPage = () => {
isDisabled: false,
text: "Save Configuration",
}));
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
router.push("/upload");
} catch (error) {
const message =
error instanceof Error
@ -211,7 +250,6 @@ const SettingsPage = () => {
return null;
}
const textProviderKey = llmConfig.LLM || "openai";
const textProviderLabel =
LLM_PROVIDERS[textProviderKey]?.label || textProviderKey;
@ -226,7 +264,9 @@ const SettingsPage = () => {
? llmConfig.OLLAMA_MODEL
: textProviderKey === "custom"
? llmConfig.CUSTOM_MODEL
: "";
: textProviderKey === "codex"
? llmConfig.CODEX_MODEL
: "";
const textSummary = selectedTextModel
? `${textProviderLabel} (${selectedTextModel})`
: textProviderLabel;
@ -237,6 +277,67 @@ const SettingsPage = () => {
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label || llmConfig.IMAGE_PROVIDER
: "No image provider";
useEffect(() => {
if (llmConfig.LLM === "codex" && !llmConfig.CODEX_MODEL || llmConfig.LLM === "openai" && !llmConfig.OPENAI_MODEL || llmConfig.LLM === "google" && !llmConfig.GOOGLE_MODEL || llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_MODEL || llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL || llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) {
notify.error("Cannot save settings", "Please select a model for the selected provider");
const currentUrl = window.location.href;
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
console.log("beforeunload");
e.preventDefault();
e.returnValue = "";
};
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement | null;
const link = target?.closest("a");
if (!link) return;
const href = link.getAttribute("href");
const targetAttr = link.getAttribute("target");
if (
href &&
href !== "#" &&
!href.startsWith("javascript:") &&
targetAttr !== "_blank"
) {
// notify.error("Cannot save settings", "Please select a model for the selected provider");
e.preventDefault();
window.history.pushState(null, "", pathname);
}
};
const handlePopState = () => {
console.log("popstate");
window.history.pushState(null, "", pathname);
};
window.addEventListener("beforeunload", handleBeforeUnload);
window.addEventListener("popstate", handlePopState);
document.addEventListener("click", handleClick, true);
// keep current page in history
window.history.pushState(null, "", currentUrl);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
window.removeEventListener("popstate", handlePopState);
document.removeEventListener("click", handleClick, true);
};
}
}, [llmConfig, pathname]);
return (
<div className="h-screen font-syne flex flex-col overflow-hidden relative">
<div
@ -278,6 +379,7 @@ const SettingsPage = () => {
llmConfig={llmConfig}
/>}
{mode === 'presenton' && selectedProvider === 'image-provider' && <ImageProvider llmConfig={llmConfig} setLlmConfig={setLlmConfig} />}
{selectedProvider === 'privacy' && <PrivacySettings />}
</div>
</main>

View file

@ -1,30 +1,39 @@
import React from 'react'
const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }: { mode: 'nanobanana' | 'presenton', setMode: (mode: 'nanobanana' | 'presenton') => void, selectedProvider: 'text-provider' | 'image-provider', setSelectedProvider: (provider: 'text-provider' | 'image-provider') => void }) => {
import { Shield } from 'lucide-react'
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants'
import { useSelector } from 'react-redux'
import { RootState } from '@/store/store'
const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }: { mode: 'nanobanana' | 'presenton', setMode: (mode: 'nanobanana' | 'presenton') => void, selectedProvider: 'text-provider' | 'image-provider' | 'privacy', setSelectedProvider: (provider: 'text-provider' | 'image-provider' | 'privacy') => void }) => {
const { llm_config } = useSelector((state: RootState) => state.userConfig)
const textProviderIcon = LLM_PROVIDERS[llm_config.LLM as keyof typeof LLM_PROVIDERS]?.icon
const imageProviderIcon = IMAGE_PROVIDERS[llm_config.IMAGE_PROVIDER as keyof typeof IMAGE_PROVIDERS]?.icon || '/providers/pexel.png'
return (
<div className='w-full max-w-[230px] h-screen px-4 pt-[22px] bg-[#F9FAFB]'>
<div className='w-full max-w-[230px] h-screen px-3 pt-[22px] bg-[#F9FAFB] flex flex-col'>
<p className='text-xs text-black font-medium border-b mt-[3.15rem] border-[#E1E1E5] pb-3.5'>FILTER BY:</p>
<div className='mt-6'>
<div className='mt-6 flex-1'>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Mode</p>
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
<button className='px-3 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
<div className='p-0.5 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
<button className='px-3 font-syne h-[26px] text-[10px] font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setMode('presenton')}
style={{
background: mode === 'presenton' ? '#F4F3FF' : 'transparent',
color: mode === 'presenton' ? '#5146E5' : '#3A3A3A'
}}
>Presenton</button>
>Template Based
</button>
<svg xmlns="http://www.w3.org/2000/svg" className='mx-1' width="2" height="17" viewBox="0 0 2 17" fill="none">
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<div className='relative'>
<button className='px-3 py-2 text-xs font-medium rounded-[70px] cursor-not-allowed opacity-60'
<button className='px-3 font-syne h-[26px] text-[10px] font-medium rounded-[70px] cursor-not-allowed opacity-60'
disabled
style={{
background: 'transparent',
color: '#9CA3AF'
}}
>
Nanobanana
Image Based
</button>
<span className='absolute -top-2 -right-5 text-[7px] uppercase tracking-wide bg-[#F4F3FF] text-[#5146E5] border border-[#D9D6FE] rounded-full px-1.5 py-0.5 whitespace-nowrap'>
Coming soon
@ -35,24 +44,24 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
</div>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Provider</p>
{mode === 'presenton' && <div className='space-y-2.5'>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#E1E1E5]'}`} onClick={() => setSelectedProvider('text-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
<img src={textProviderIcon} className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Text Provider</p>
</button>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#E1E1E5]'}`} onClick={() => setSelectedProvider('image-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/image-provider.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src={imageProviderIcon} className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Image Provider</p>
</button>
</div>}
{
mode === 'nanobanana' && <div>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
@ -61,6 +70,19 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
</div>
}
</div>
<div className='border-t border-[#E1E1E5] py-5 relative z-50'>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Other</p>
<button
className={`w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'privacy' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`}
onClick={() => setSelectedProvider('privacy')}
>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF] flex items-center justify-center bg-white'>
<Shield className='w-3.5 h-3.5 text-[#5146E5]' />
</div>
<p className='text-[#191919] text-xs font-medium'>Usage Analytics</p>
</button>
</div>
</div>
)
}

View file

@ -1,17 +1,15 @@
import ToolTip from '@/components/ToolTip';
import { Button } from '@/components/ui/button';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';
import { LLMConfig } from '@/types/llm_config';
import { getApiUrl } from '@/utils/api';
import { LLM_PROVIDERS } from '@/utils/providerConstants';
import { Check, Loader2, Eye, EyeOff, ChevronUp } from 'lucide-react';
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { notify } from '@/components/ui/sonner';
import { toast } from 'sonner';
import { getApiUrl } from '@/utils/api';
import CodexConfig from '@/components/CodexConfig';
import CodexConfig from './SettingCodex';
interface OpenAIConfigProps {
@ -19,6 +17,13 @@ interface OpenAIConfigProps {
onInputChange: (value: string | boolean, field: string) => void;
llmConfig: LLMConfig;
}
interface ModelOption {
value: string;
label: string;
size?: string;
}
const TextProvider = ({
onInputChange,
@ -28,7 +33,7 @@ const TextProvider = ({
) => {
const [openProviderSelect, setOpenProviderSelect] = useState(false);
const [openModelSelect, setOpenModelSelect] = useState(false);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [availableModels, setAvailableModels] = useState<ModelOption[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [modelsChecked, setModelsChecked] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
@ -159,19 +164,48 @@ const TextProvider = ({
if (response.ok) {
const data = await response.json();
const normalizedModels: string[] = selectedProvider === 'ollama'
const normalizedModels: ModelOption[] = selectedProvider === 'ollama'
? Array.isArray(data)
? data.map((model: { value?: string; label?: string }) => model.value || model.label || '').filter(Boolean)
? data
.map((model) => {
if (typeof model === 'string') {
return {
value: model,
label: model,
};
}
if (model && typeof model === 'object') {
const typedModel = model as { value?: string; label?: string; size?: string };
return {
value: typedModel.value || typedModel.label || '',
label: typedModel.label || typedModel.value || '',
size: typedModel.size,
};
}
return {
value: '',
label: '',
};
})
.filter((model: ModelOption) => Boolean(model.value))
: []
: Array.isArray(data)
? data
.filter((model): model is string => typeof model === 'string')
.map((model) => ({
value: model,
label: model,
}))
: [];
setAvailableModels(normalizedModels);
setModelsChecked(true);
if (normalizedModels.length > 0 && currentModelField) {
if (currentModel && normalizedModels.includes(currentModel)) {
const modelValues = normalizedModels.map((model) => model.value);
if (currentModel && modelValues.includes(currentModel)) {
onInputChange(currentModel, currentModelField);
return;
}
@ -183,16 +217,19 @@ const TextProvider = ({
? 'models/gemini-2.5-flash'
: selectedProvider === 'anthropic'
? 'claude-sonnet-4-20250514'
: normalizedModels[0];
: modelValues[0];
const nextModel = normalizedModels.includes(preferredDefault) ? preferredDefault : normalizedModels[0];
const nextModel = modelValues.includes(preferredDefault) ? preferredDefault : modelValues[0];
onInputChange(nextModel, currentModelField);
}
} else {
console.error('Failed to fetch models');
setAvailableModels([]);
setModelsChecked(true);
toast.error(`Failed to fetch ${modelLabel} models`);
notify.error(
'Could not load models',
`The server could not list ${modelLabel} models. Check your API key or endpoint and try again.`
);
}
} catch (error) {
console.error('Error fetching models:', error);
@ -215,8 +252,8 @@ const TextProvider = ({
return (
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] ">
{/* API Key Input */}
<div className="mb-4 flex items-center justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
<div className=" max-w-[290px] pb-[50px]">
<div className="mb-4 flex items-end justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
<div className=" max-w-[290px] ">
<div className='w-[60px] h-[60px] rounded-[4px] flex items-center justify-center'
style={{ backgroundColor: '#4C55541A' }}
>
@ -231,11 +268,10 @@ const TextProvider = ({
Choosing where text content comes from
</p>
</div>
<div>
<div className={`flex gap-4 justify-end ${selectedProvider === 'codex' ? 'items-end' : 'items-start'}`}>
<div className="relative w-[205px] ">
<div className='flex flex-col justify-end items-end gap-4'>
<div className={`flex gap-4 justify-end ${selectedProvider === 'codex' ? 'items-end' : 'items-start'}`}>
<div className={`relative ${selectedProvider === 'codex' ? 'w-[240px]' : 'w-[222px]'}`}>
<div className="flex flex-col justify-start ">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Text Provider
</label>
@ -248,7 +284,7 @@ const TextProvider = ({
variant="outline"
role="combobox"
aria-expanded={openProviderSelect}
className="w-[205px] h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
className="w-[222px] h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
@ -264,14 +300,14 @@ const TextProvider = ({
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
style={{ width: "300px" }}
>
<Command>
<CommandInput placeholder="Search provider..." />
<CommandList>
<CommandEmpty>No provider found.</CommandEmpty>
<CommandGroup>
{Object.values(LLM_PROVIDERS).map(
{Object.values([{ value: 'codex', label: 'ChatGPT', description: 'ChatGPT Plus/Pro via OAuth', icon: '/providers/openai.png' }, ...Object.values(LLM_PROVIDERS)]).map(
(provider, index) => (
<CommandItem
key={index}
@ -310,10 +346,8 @@ const TextProvider = ({
</PopoverContent>
</Popover>
</div>
</div>
<div className="relative flex flex-col justify-end items-end w-[205px] ">
<div className={`relative flex flex-col justify-end ${selectedProvider === 'codex' ? 'items-end w-[262px] max-w-full' : 'items-end w-[222px]'}`}>
<div className="flex flex-col justify-start w-full ">
{selectedProvider === 'ollama' ? (
<>
@ -357,8 +391,9 @@ const TextProvider = ({
</>
)}
</>
) : selectedProvider === 'codex' ? (
<div className="w-full mt-0 rounded-[12px]">
) : selectedProvider === 'codex' ?
<div className='w-full mt-0 rounded-[12px] '>
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
@ -367,7 +402,7 @@ const TextProvider = ({
}}
/>
</div>
) : (
: (
<>
<label className="block text-sm font-medium capitalize text-gray-700 mb-2">
{selectedProvider === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
@ -402,8 +437,6 @@ const TextProvider = ({
</div>
{selectedProvider !== 'ollama' && selectedProvider !== 'codex' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
@ -429,93 +462,101 @@ const TextProvider = ({
"Check models"
)}
</button>
)}
</div>
{/* Model Selection - only show if models are available */}
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
<div className="w-[205px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<span className="text-sm truncate font-medium text-gray-900">
{currentModel
? availableModels.find(model => model === currentModel) || currentModel
: "Select a model"}
</span>
<ChevronUp className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
</div>
{/* Model Selection - only show if models are available */}
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
<div className="w-[222px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
if (currentModelField) {
onInputChange(value, currentModelField);
}
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
currentModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
<span className="text-sm truncate font-medium text-gray-900">
{(() => {
if (!currentModel) return "Select a model";
const selectedModel = availableModels.find((model) => model.value === currentModel);
if (!selectedModel) return currentModel;
if (selectedProvider === 'ollama' && selectedModel.size) {
return `${selectedModel.label} (${selectedModel.size})`;
}
return selectedModel.label;
})()}
</span>
<ChevronUp className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model) => (
<CommandItem
key={model.value}
value={model.value}
onSelect={() => {
if (currentModelField) {
onInputChange(model.value, currentModelField);
}
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
currentModel === model.value
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model.label}
</span>
{selectedProvider === 'ollama' && model.size ? (
<span className="text-xs font-medium text-gray-500">
{model.size}
</span>
</div>
) : null}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
</div>
{/* Show message if no models found */}
{selectedProvider !== 'codex' && modelsChecked && availableModels.length === 0 && (
{modelsChecked && availableModels.length === 0 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No models found. Please make sure your provider credentials are valid and the selected provider is reachable.
@ -524,8 +565,8 @@ const TextProvider = ({
)}
{/* Web Grounding Toggle - show at the end, below models dropdown */}
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
{/* <div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
<div className=' max-w-[290px]'>
<h4 className="text-xl font-normal text-[#191919]">Advanced</h4>
@ -534,8 +575,7 @@ const TextProvider = ({
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-[205px]">
<div className="w-[222px]">
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={!!llmConfig.WEB_GROUNDING}
@ -545,16 +585,9 @@ const TextProvider = ({
Enable Web Grounding
</label>
</div>
</div>
{/* <div className="w-[295px]"></div> */}
</div>
</div>
</div> */}
</div>
)
}

View file

@ -1,76 +1,113 @@
import { Card } from "@/components/ui/card";
export default function LoadingProfile() {
function Shimmer({ className }: { className?: string }) {
return (
<div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
{/* Header Skeleton */}
<div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
<div className="container mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<div className="h-8 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="flex items-center gap-4">
<div className="h-8 w-8 bg-gray-200 animate-pulse rounded-full" />
<div className="h-8 w-24 bg-gray-200 animate-pulse rounded-md" />
<div
className={`bg-[#E1E1E5] animate-pulse rounded-md ${className ?? ""}`}
aria-hidden
/>
);
}
export default function LoadingSettings() {
return (
<div className="h-screen font-syne flex flex-col overflow-hidden relative">
<div
className="fixed z-0 bottom-[-14.5rem] left-0 w-full h-full pointer-events-none"
style={{
height: "341px",
borderRadius: "1440px",
background:
"radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)",
}}
/>
<main className="w-full mx-auto gap-6 overflow-hidden flex">
{/* SettingSideBar structure */}
<div className="w-full max-w-[230px] h-screen px-4 pt-[22px] bg-[#F9FAFB] flex flex-col shrink-0">
<div className="mt-[3.15rem] border-b border-[#E1E1E5] pb-3.5">
<Shimmer className="h-3 w-16" />
</div>
<div className="mt-6 flex-1 min-h-0">
<Shimmer className="h-3 w-24 mb-2.5" />
<div className="p-0.5 rounded-[40px] bg-white w-full max-w-[210px] border border-[#EDEEEF] flex items-center mb-[34px] h-[30px]">
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5" />
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5 opacity-70" />
</div>
<Shimmer className="h-3 w-28 mb-2.5" />
<div className="space-y-2.5">
{[0, 1].map((i) => (
<div
key={i}
className="w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white"
>
<Shimmer className="h-[18px] w-[18px] rounded-full shrink-0" />
<Shimmer className="h-3 flex-1 max-w-[100px]" />
</div>
))}
</div>
</div>
<div className="border-t border-[#E1E1E5] py-5">
<Shimmer className="h-3 w-12 mb-2.5" />
<div className="w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white">
<Shimmer className="h-6 w-6 rounded-full shrink-0" />
<Shimmer className="h-3 w-16" />
</div>
</div>
</div>
</div>
{/* Main Content Skeleton */}
<main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
<div className="flex-1 overflow-hidden">
{/* LLM Selection Content Skeleton */}
<div className="space-y-6 p-6">
{/* Page Title */}
<div className="space-y-2">
<div className="h-8 w-48 bg-gray-200 animate-pulse rounded-md" />
<div className="h-5 w-72 bg-gray-200 animate-pulse rounded-md" />
{/* Main column — matches SettingPage + TextProvider default */}
<div className="w-full min-w-0 flex flex-col">
<div className="sticky top-0 right-0 z-50 py-[28px] backdrop-blur mb-4">
<div className="flex gap-3 items-center flex-wrap">
<Shimmer className="h-8 w-[132px] rounded-md" />
<Shimmer className="h-[22px] w-[min(320px,55%)] rounded-[50px]" />
</div>
</div>
{/* LLM Provider Cards */}
<div className="space-y-4">
{[...Array(3)].map((_, index) => (
<Card key={index} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-gray-200 animate-pulse rounded-md" />
<div className="space-y-1">
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="h-4 w-48 bg-gray-200 animate-pulse rounded-md" />
</div>
</div>
<div className="h-6 w-6 bg-gray-200 animate-pulse rounded-full" />
</div>
{/* Configuration Fields */}
<div className="space-y-4">
{[...Array(2)].map((_, fieldIndex) => (
<div key={fieldIndex} className="space-y-2">
<div className="h-4 w-24 bg-gray-200 animate-pulse rounded-md" />
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
</div>
))}
</div>
</Card>
))}
</div>
{/* Model Selection */}
<Card className="p-6">
<div className="space-y-4">
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] pr-4 sm:pr-7">
{/* TextProvider top card: white panel, icon + copy left, controls right */}
<div className="mb-4 flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 rounded-[12px] bg-white pt-5 pb-10 px-6 sm:px-10">
<div className="max-w-[290px] shrink-0">
<Shimmer className="w-[60px] h-[60px] rounded-[4px]" />
<Shimmer className="h-6 w-48 mt-2.5 mb-2" />
<Shimmer className="h-4 w-full max-w-[260px]" />
<Shimmer className="h-4 w-40 mt-1.5" />
</div>
</Card>
<div className="flex flex-col items-stretch lg:items-end gap-4 flex-1 min-w-0">
<div className="flex flex-col sm:flex-row gap-4 sm:justify-end w-full">
<div className="w-full sm:w-[222px]">
<Shimmer className="h-4 w-36 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
<div className="w-full sm:w-[222px]">
<Shimmer className="h-4 w-28 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
</div>
<div className="w-full sm:w-[222px] sm:ml-auto">
<Shimmer className="h-4 w-40 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
</div>
</div>
{/* TextProvider “Advanced” card */}
<div className="bg-white flex flex-col sm:flex-row sm:justify-between sm:items-center gap-6 p-6 sm:p-10 rounded-[12px]">
<div className="max-w-[290px] shrink-0">
<Shimmer className="h-6 w-28 mb-2" />
<Shimmer className="h-4 w-52" />
</div>
<div className="flex items-center gap-2.5 w-full sm:w-[222px] sm:justify-start">
<Shimmer className="h-6 w-11 rounded-full shrink-0" />
<Shimmer className="h-4 flex-1 max-w-[160px]" />
</div>
</div>
</div>
</div>
</main>
{/* Fixed Bottom Button Skeleton */}
<div className="flex-shrink-0 bg-white border-t border-gray-200 p-4">
<div className="container mx-auto max-w-3xl">
<div className="h-12 w-full bg-gray-200 animate-pulse rounded-lg" />
</div>
{/* Fixed save button — matches SettingPage placement */}
<div className="mx-auto fixed bottom-20 right-5 z-40">
<Shimmer className="h-12 w-[200px] sm:w-[240px] rounded-[58px]" />
</div>
</div>
);

View file

@ -1,6 +1,6 @@
import ToolTip from '@/components/ToolTip'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
@ -66,6 +66,8 @@ const AdvanceSettings = ({ config, onConfigChange }: ConfigurationSelectsProps)
<DialogContent className="max-w-2xl font-instrument_sans">
<DialogHeader>
<DialogTitle>Advanced settings</DialogTitle>
<DialogDescription>Adjust Presentation Behavior</DialogDescription>
<Button onClick={handleSaveAdvanced} className="bg-[#5141e5] text-white hover:bg-[#5141e5]/90">Save</Button>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
@ -159,10 +161,10 @@ const AdvanceSettings = ({ config, onConfigChange }: ConfigurationSelectsProps)
</div>
</div>
<DialogFooter>
{/* <DialogFooter>
<Button variant="outline" onClick={() => handleOpenAdvancedChange(false)}>Cancel</Button>
<Button onClick={handleSaveAdvanced} className="bg-[#5141e5] text-white hover:bg-[#5141e5]/90">Save</Button>
</DialogFooter>
</DialogFooter> */}
</DialogContent>
</Dialog>
</div>

View file

@ -90,6 +90,8 @@ export default function CodexConfig({
}
const data: StatusResponse = await res.json();
if (data.status === "authenticated") {
onInputChange('chatgpt', 'LLM');
onInputChange(DEFAULT_CODEX_MODEL, 'codex_model');
setAuthStatus("authenticated");
applyProfile(data);
} else {
@ -106,7 +108,7 @@ export default function CodexConfig({
try {
trackEvent(MixpanelEvent.Codex_SignIn_API_Call);
onInputChange('codex', 'LLM');
onInputChange('chatgpt', 'LLM');
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/initiate"), {
method: "POST",
@ -199,6 +201,7 @@ export default function CodexConfig({
setUsername(null);
setEmail(null);
setIsPro(null);
onInputChange("openai", "LLM");
onInputChange("", "codex_model");
toast.success("Signed out from ChatGPT");
} catch {
@ -229,13 +232,13 @@ export default function CodexConfig({
if (authStatus === "checking") {
return (
<div className="mb-5 w-full p-3 bg-[#010100] font-syne rounded-[8px] flex items-center gap-6">
<div className="mb-5 w-full p-3 border border-[#EDEEEF] font-syne rounded-[8px] flex items-center gap-6">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-10 h-10 text-white animate-spin" />
<Loader2 className="w-10 h-10 text-[#191919] animate-spin" />
</div>
<div className="text-start flex-1 min-w-0">
<h4 className="text-white text-lg font-medium">Checking status</h4>
<p className="text-[#808080] text-sm font-normal">
<h4 className="text-[#191919] text-lg font-medium">Checking status</h4>
<p className="text-[#B3B3B3] text-sm font-normal">
Verifying your ChatGPT connection
</p>
</div>
@ -246,14 +249,14 @@ export default function CodexConfig({
if (authStatus === "polling") {
return (
<div className="mb-5 space-y-4 font-syne">
<div className="w-full p-3 bg-[#010100] rounded-[8px] flex items-center justify-between gap-4">
<div className="w-full p-3 border border-[#EDEEEF] rounded-[8px] flex items-center justify-between gap-4">
<div className="flex items-center gap-6 min-w-0 flex-1">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-10 h-10 text-white animate-spin" />
<div className="w-[40px] h-[40px] bg-[#EDEEEF] rounded-full flex items-center justify-center shrink-0">
<Loader2 className="w-5 h-5 text-[#191919] animate-spin" />
</div>
<div className="text-start min-w-0">
<h4 className="text-white text-lg font-medium">Waiting for sign-in</h4>
<p className="text-[#808080] text-sm font-normal">
<h4 className="text-[#191919] text-lg font-medium">Waiting for sign-in</h4>
<p className="text-[#B3B3B3] text-sm font-normal">
Complete sign-in in the browser tab we opened.
</p>
</div>
@ -261,21 +264,21 @@ export default function CodexConfig({
<button
type="button"
onClick={handleCancelPolling}
className="shrink-0 text-sm text-[#808080] hover:text-white underline underline-offset-2 transition-colors"
className="shrink-0 text-sm text-[#B3B3B3] hover:text-[#191919] underline underline-offset-2 transition-colors"
>
Cancel
</button>
</div>
<div className="space-y-2 rounded-[8px] border border-[#333333] bg-[#010100] p-3">
<p className="text-white text-xs font-normal">
<div className="space-y-2 rounded-[8px] border border-[#EDEEEF] p-3">
<p className="text-[#191919] text-xs font-normal">
Paste redirect URL or code if you were not redirected automatically
</p>
<div className="flex gap-2">
<input
type="text"
placeholder="Paste URL or code…"
className="flex-1 min-w-0 px-3 py-2.5 outline-none border border-[#333333] rounded-[8px] bg-[#1a1a1a] text-sm text-white placeholder:text-[#666666] focus:border-[#555555] transition-colors"
className="flex-1 min-w-0 px-3 py-2.5 outline-none border border-[#EDEEEF] rounded-[8px] text-sm text-[#191919] placeholder:text-[#666666] focus:border-[#555555] transition-colors"
value={manualCode}
onChange={(e) => setManualCode(e.target.value)}
/>
@ -283,7 +286,7 @@ export default function CodexConfig({
type="button"
onClick={handleManualExchange}
disabled={isExchanging || !manualCode.trim()}
className="shrink-0 px-4 py-2.5 bg-[#333333] hover:bg-[#444444] disabled:opacity-40 disabled:hover:bg-[#333333] rounded-[8px] text-sm font-medium text-white transition-colors flex items-center justify-center min-w-[88px]"
className="shrink-0 px-4 py-2.5 bg-[#EDEEEF] hover:bg-[#E4E5E6] disabled:opacity-40 disabled:hover:bg-[#EDEEEF] rounded-[8px] text-sm font-medium text-[#191919] transition-colors flex items-center justify-center min-w-[88px]"
>
{isExchanging ? (
<Loader2 className="w-5 h-5 animate-spin" />
@ -298,28 +301,27 @@ export default function CodexConfig({
}
if (authStatus === "authenticated") {
const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown";
return (
<div className=" mb-5">
<div className="flex items-center justify-between gap-3 p-5 border border-[#EDEEEF] rounded-[8px]">
<div className="flex items-center gap-3">
<UserCheck className="w-6 h-6 text-black shrink-0" />
<UserCheck className="w-6 h-6 text-[#191919] shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">
<p className="text-sm font-medium text-[#191919] truncate">
{username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}
</p>
</div>
{email && username && (
<p className="text-xs text-gray-500 truncate">{email}</p>
<p className="text-xs text-[#B3B3B3] truncate">{email}</p>
)}
{!email && accountId && (
<p className="text-xs text-gray-500 truncate">ID: {accountId}</p>
<p className="text-xs text-[#B3B3B3] truncate">ID: {accountId}</p>
)}
<p className="text-xs text-gray-400">Signed in to ChatGPT</p>
<p className="text-xs text-[#B3B3B3]">Signed in to ChatGPT</p>
</div>
</div>
<div className="flex gap-1.5 shrink-0">
@ -330,9 +332,9 @@ export default function CodexConfig({
className="flex items-center justify-center px-3.5 py-2.5 bg-[#EDEEEF] rounded-[58px] minid:opacity-40 transition-colors"
>
{isRefreshing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin text-black" />
<Loader2 className="w-3.5 h-3.5 animate-spin text-[#191919]" />
) : (
<RefreshCw className="w-3.5 h-3.5 text-black" />
<RefreshCw className="w-3.5 h-3.5 text-[#191919]" />
)}
</button>
<button
@ -342,9 +344,9 @@ export default function CodexConfig({
className="flex items-center justify-center px-3.5 py-2.5 bg-[#EDEEEF] rounded-[58px] hover:bg-[#E4E5E6] disabled:opacity-40 transition-colors"
>
{isLoggingOut ? (
<Loader2 className="w-3.5 h-3.5 animate-spin text-black" />
<Loader2 className="w-3.5 h-3.5 animate-spin text-[#191919]" />
) : (
<Trash2 className="w-3.5 h-3.5 text-black" />
<Trash2 className="w-3.5 h-3.5 text-[#191919]" />
)}
</button>
</div>
@ -358,19 +360,19 @@ export default function CodexConfig({
return (
<button
onClick={handleSignIn}
className="mb-5 w-full p-3 bg-[#010100] font-syne rounded-[8px] flex items-center justify-between "
className=" w-full p-5 border border-[#EDEEEF] font-syne rounded-[12px] flex items-center justify-between "
>
<div className="flex items-center gap-6">
<div className="w-[74px] h-[74px] bg-[#333333] rounded-full flex items-center justify-center" >
<div className="flex items-center gap-2 flex-1">
<div className="w-[40px] h-[40px] bg-[#333333] rounded-full flex items-center justify-center" >
<img src="/providers/OpenAI-white.png" alt="openai Logo" className="w-[52px] h-[52px]" />
<img src="/providers/OpenAI-white.png" alt="openai Logo" className="w-[27px] h-[27px]" />
</div>
<div className="text-start">
<h4 className="text-white text-lg font-medium">Sign in with ChatGPT</h4>
<p className="text-[#808080] text-sm font-normal">Use your ChatGPT account no API <br /> key required</p>
<div className="text-start flex-1">
<h4 className="text-[#191919] text-sm font-medium">Sign in with ChatGPT</h4>
<p className="text-[#B3B3B3] text-xs font-normal">Use your ChatGPT account no API key required</p>
</div>
</div>
<ArrowRight className="w-[22px] h-[22px] text-white" />
<ArrowRight className="w-[22px] h-[22px] text-[#4C4C4C]" />
</button>
);
}

View file

@ -31,48 +31,6 @@ interface ButtonState {
status?: string;
}
const FINAL_STEP_CONFETTI_PIECES = [
// left: denser at top
{ side: "left", offset: 1, top: 3, width: 28, height: 10, color: "#F59E0B", rotate: 12 },
{ side: "left", offset: 7, top: 5, width: 18, height: 7, color: "#7C3AED", rotate: -10 },
{ side: "left", offset: 12, top: 7, width: 20, height: 7, color: "#14B8A6", rotate: 22 },
{ side: "left", offset: 3, top: 10, width: 22, height: 8, color: "#22C55E", rotate: -18 },
{ side: "left", offset: 9, top: 12, width: 24, height: 8, color: "#E11D48", rotate: 18 },
{ side: "left", offset: 14, top: 15, width: 18, height: 7, color: "#F43F5E", rotate: 23 },
{ side: "left", offset: 5, top: 18, width: 20, height: 7, color: "#0EA5E9", rotate: -12 },
{ side: "left", offset: 11, top: 21, width: 26, height: 9, color: "#2563EB", rotate: 20 },
{ side: "left", offset: 2, top: 24, width: 19, height: 7, color: "#14B8A6", rotate: -16 },
{ side: "left", offset: 8, top: 28, width: 21, height: 8, color: "#FB7185", rotate: 27 },
{ side: "left", offset: 13, top: 32, width: 20, height: 7, color: "#06B6D4", rotate: 16 },
{ side: "left", offset: 3, top: 36, width: 24, height: 9, color: "#EAB308", rotate: -22 },
{ side: "left", offset: 10, top: 41, width: 18, height: 7, color: "#A855F7", rotate: -14 },
{ side: "left", offset: 2, top: 50, width: 30, height: 10, color: "#EC4899", rotate: -28 },
{ side: "left", offset: 13, top: 58, width: 19, height: 7, color: "#22C55E", rotate: 17 },
{ side: "left", offset: 5, top: 66, width: 24, height: 8, color: "#8B5CF6", rotate: 14 },
{ side: "left", offset: 11, top: 74, width: 18, height: 7, color: "#3B82F6", rotate: 12 },
{ side: "left", offset: 4, top: 82, width: 20, height: 7, color: "#14B8A6", rotate: 21 },
{ side: "left", offset: 7, top: 90, width: 24, height: 8, color: "#D946EF", rotate: -26 },
// right: denser at top
{ side: "right", offset: 1, top: 4, width: 30, height: 10, color: "#F97316", rotate: -14 },
{ side: "right", offset: 8, top: 6, width: 19, height: 7, color: "#0EA5E9", rotate: 12 },
{ side: "right", offset: 13, top: 9, width: 20, height: 7, color: "#22C55E", rotate: -20 },
{ side: "right", offset: 4, top: 12, width: 24, height: 8, color: "#EC4899", rotate: 20 },
{ side: "right", offset: 10, top: 15, width: 22, height: 8, color: "#06B6D4", rotate: -18 },
{ side: "right", offset: 15, top: 18, width: 20, height: 7, color: "#22C55E", rotate: -25 },
{ side: "right", offset: 5, top: 21, width: 18, height: 7, color: "#8B5CF6", rotate: 19 },
{ side: "right", offset: 12, top: 24, width: 21, height: 8, color: "#F43F5E", rotate: 14 },
{ side: "right", offset: 2, top: 28, width: 26, height: 9, color: "#84CC16", rotate: 15 },
{ side: "right", offset: 9, top: 33, width: 21, height: 8, color: "#F97316", rotate: -11 },
{ side: "right", offset: 14, top: 38, width: 20, height: 7, color: "#A855F7", rotate: -19 },
{ side: "right", offset: 4, top: 44, width: 19, height: 7, color: "#F43F5E", rotate: 20 },
{ side: "right", offset: 2, top: 52, width: 28, height: 10, color: "#FACC15", rotate: 25 },
{ side: "right", offset: 12, top: 60, width: 18, height: 7, color: "#14B8A6", rotate: -15 },
{ side: "right", offset: 6, top: 68, width: 24, height: 8, color: "#22C55E", rotate: -17 },
{ side: "right", offset: 1, top: 76, width: 20, height: 7, color: "#A855F7", rotate: 14 },
{ side: "right", offset: 13, top: 84, width: 20, height: 7, color: "#3B82F6", rotate: -24 },
{ side: "right", offset: 5, top: 92, width: 26, height: 9, color: "#EAB308", rotate: 18 },
] as const;
const getTaperedSideOffset = (offset: number, top: number) => {
const taperMultiplier = Math.max(0.72, 1.85 - top * 0.012);
@ -200,150 +158,21 @@ export default function Home() {
}
return (
// <div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
// <main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
// {/* Branding Header */}
// <div className="text-center mb-2 mt-4 flex-shrink-0">
// <div className="flex items-center justify-center gap-3 mb-2">
// <img src="/Logo.png" alt="Presenton Logo" className="h-12" />
// </div>
// <p className="text-gray-600 text-sm">
// Open-source AI presentation generator
// </p>
// </div>
// {/* Main Configuration Card */}
// <div className="flex-1 overflow-hidden">
// <LLMProviderSelection
// initialLLMConfig={llmConfig}
// onConfigChange={setLlmConfig}
// buttonState={buttonState}
// setButtonState={setButtonState}
// />
// </div>
// </main>
// {/* Download Progress Modal */}
// {showDownloadModal && downloadingModel && (
// <div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
// <div className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl max-w-md w-full p-6 relative">
// {/* Modal Content */}
// <div className="text-center">
// {/* Icon */}
// <div className="mb-4">
// {downloadingModel.done ? (
// <CheckCircle className="w-12 h-12 text-green-600 mx-auto" />
// ) : (
// <Download className="w-12 h-12 text-blue-600 mx-auto animate-pulse" />
// )}
// </div>
// {/* Title */}
// <h3 className="text-lg font-semibold text-gray-900 mb-2">
// {downloadingModel.done ? "Download Complete!" : "Downloading Model"}
// </h3>
// {/* Model Name */}
// <p className="text-sm text-gray-600 mb-6">
// {llmConfig.OLLAMA_MODEL}
// </p>
// {/* Progress Bar */}
// {downloadProgress > 0 && (
// <div className="mb-4">
// <div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
// <div
// className="bg-blue-600 h-3 rounded-full transition-all duration-300 ease-out"
// style={{ width: `${downloadProgress}%` }}
// />
// </div>
// <p className="text-sm text-gray-600 mt-2">
// {downloadProgress}% Complete
// </p>
// </div>
// )}
// {/* Status */}
// {downloadingModel.status && (
// <div className="flex items-center justify-center gap-2 mb-4">
// <CheckCircle className="w-4 h-4 text-green-600" />
// <span className="text-sm font-medium text-green-700 capitalize">
// {downloadingModel.status}
// </span>
// </div>
// )}
// {/* Status Message */}
// {downloadingModel.status && downloadingModel.status !== "pulled" && (
// <div className="text-xs text-gray-500">
// {downloadingModel.status === "downloading" && "Downloading model files..."}
// {downloadingModel.status === "verifying" && "Verifying model integrity..."}
// {downloadingModel.status === "pulling" && "Pulling model from registry..."}
// </div>
// )}
// {/* Download Info */}
// {downloadingModel.downloaded && downloadingModel.size && (
// <div className="mt-4 p-3 bg-gray-50 rounded-lg">
// <div className="flex justify-between text-xs text-gray-600">
// <span>Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB</span>
// <span>Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB</span>
// </div>
// </div>
// )}
// </div>
// </div>
// </div>
// )}
// {/* Fixed Bottom Button */}
// <div className="flex-shrink-0 bg-white border-t border-gray-200 p-4">
// <div className="container mx-auto max-w-3xl">
// <button
// onClick={handleSaveConfig}
// disabled={buttonState.isDisabled}
// className={`w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${buttonState.isDisabled
// ? "bg-gray-400 cursor-not-allowed"
// : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
// } text-white`}
// >
// {buttonState.isLoading ? (
// <div className="flex items-center justify-center gap-2">
// <Loader2 className="w-4 h-4 animate-spin" />
// {buttonState.text}
// </div>
// ) : (
// buttonState.text
// )}
// </button>
// </div>
// </div>
// </div>
<div className="flex h-screen">
<OnBoardingSlidebar />
<div className="flex min-h-screen relative">
<div
className='fixed z-0 -bottom-[14.5rem] left-0 w-full h-full'
style={{
height: "341px",
borderRadius: '1440px',
background: 'radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)',
}}
/>
<OnBoardingSlidebar step={step} />
<main className="w-full pl-20 pr-8 max-w-[1440px] mx-auto relative z-10">
{step === 3 && (
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden>
{FINAL_STEP_CONFETTI_PIECES.map((piece, index) => (
<span
key={`${piece.side}-${index}`}
className="absolute rounded-[3px]"
style={{
top: `${piece.top}%`,
...(piece.side === "left"
? { left: `${getTaperedSideOffset(piece.offset, piece.top)}%` }
: { right: `${getTaperedSideOffset(piece.offset, piece.top)}%` }),
width: `${piece.width}px`,
height: `${piece.height}px`,
backgroundColor: piece.color,
transform: `rotate(${piece.rotate}deg)`,
}}
/>
))}
</div>
)}
<OnBoardingHeader currentStep={step} />
{step === 1 && <ModeSelectStep setStep={setStep} setSelectedMode={setSelectedMode} />}
<OnBoardingHeader currentStep={step} setStep={setStep} />
{step === 1 && <ModeSelectStep selectedMode={selectedMode} setStep={setStep} setSelectedMode={setSelectedMode} />}
{step === 2 && selectedMode === "presenton" && <PresentonMode currentStep={step} setStep={setStep} />}
{step === 2 && selectedMode === "image" && <GenerationWithImage />}
{step === 3 && <FinalStep />}

View file

@ -1,140 +1,115 @@
"use client";
import { ArrowRight, PartyPopper } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";
import { ArrowRight, PartyPopper } from 'lucide-react'
import { usePathname, useRouter } from 'next/navigation'
import React, { useCallback, useEffect, useState } from 'react'
import { trackEvent, MixpanelEvent, setTelemetryEnabled } from "@/utils/mixpanel";
import { Switch } from "../ui/switch";
import { Switch } from '../ui/switch';
import confetti from 'canvas-confetti';
const CONFETTI_COLORS = ["#ff00c5", "#f3ff00", "#9500d0", "#00d2f2", "#00ea9b", "#ff7f36"];
const CONFETTI_COLORS = ['#ff00c5', '#f3ff00', '#9500d0', '#00d2f2', '#00ea9b', '#ff7f36'];
function fireRealisticConfetti() {
void import("canvas-confetti").then((mod) => {
const confetti = mod.default;
confetti({
particleCount: 300,
spread: 360,
origin: { x: 0.5, y: 0.5 },
colors: CONFETTI_COLORS,
startVelocity: 60,
scalar: 1.8,
gravity: 0.6,
ticks: 300,
decay: 0.93,
zIndex: 9999,
particleCount: 300,
spread: 360,
origin: { x: 0.5, y: 0.5 },
colors: CONFETTI_COLORS,
startVelocity: 60,
scalar: 1.8,
gravity: 0.6,
ticks: 300,
decay: 0.93,
zIndex: 9999,
});
});
}
const FinalStep = () => {
const router = useRouter();
const pathname = usePathname();
const [trackingEnabled, setTrackingEnabled] = useState<boolean | null>(null);
const router = useRouter()
const pathname = usePathname()
const [trackingEnabled, setTrackingEnabled] = useState<boolean | null>(null);
useEffect(() => {
fireRealisticConfetti();
}, []);
useEffect(() => {
fireRealisticConfetti();
}, []);
useEffect(() => {
async function fetchStatus() {
try {
const res = await fetch("/api/telemetry-status");
const data = await res.json();
setTrackingEnabled(Boolean(data.telemetryEnabled));
} catch {
setTrackingEnabled(true);
}
useEffect(() => {
async function fetchStatus() {
try {
if (window.electron?.telemetryStatus) {
const data = await window.electron.telemetryStatus();
setTrackingEnabled(data.telemetryEnabled);
} else {
const res = await fetch('/api/telemetry-status');
const data = await res.json();
setTrackingEnabled(data.telemetryEnabled);
}
} catch {
setTrackingEnabled(true);
}
}
fetchStatus();
}, []);
const handleTrackingToggle = useCallback(async (enabled: boolean) => {
const prev = trackingEnabled;
setTrackingEnabled(enabled);
setTelemetryEnabled(enabled);
try {
if (window.electron?.setUserConfig) {
await window.electron.setUserConfig({
DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : 'true',
} as any);
} else {
await fetch('/api/user-config', {
method: 'POST',
body: JSON.stringify({
DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : 'true',
}),
});
}
} catch {
setTrackingEnabled(prev);
setTelemetryEnabled(prev ?? true);
}
}, [trackingEnabled]);
const handleGoToDashboard = () => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" });
router.push('/dashboard')
}
void fetchStatus();
}, []);
const handleGoToUpload = () => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
router.push('/upload')
}
return (
<div className='fixed top-0 left-0 w-full h-full flex flex-col items-center justify-center'>
<div className='flex flex-col items-center justify-center'>
const handleTrackingToggle = useCallback(
async (enabled: boolean) => {
const prev = trackingEnabled;
setTrackingEnabled(enabled);
setTelemetryEnabled(enabled);
try {
await fetch("/api/user-config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
DISABLE_ANONYMOUS_TRACKING: enabled ? "" : "true",
}),
});
} catch {
setTrackingEnabled(prev);
setTelemetryEnabled(prev ?? true);
}
},
[trackingEnabled]
);
<img src="/final_onboarding.png" alt="presenton" className='w-[118px] h-[98px] object-contain' />
<h1 className='text-black text-[30px] font-normal font-unbounded py-2.5'>Welcome on board!</h1>
<p className='text-[#000000CC] text-xl font-normal font-syne'>Youre all set. Lets create your first presentation.</p>
const handleGoToDashboard = () => {
trackEvent(MixpanelEvent.Onboarding_Completed, {
pathname,
destination: "/dashboard",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" });
router.push("/dashboard");
};
const handleGoToUpload = () => {
trackEvent(MixpanelEvent.Onboarding_Completed, {
pathname,
destination: "/upload",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
router.push("/upload");
};
return (
<div className="fixed top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center">
<img
src="/final_onboarding.png"
alt="presenton"
className="w-[118px] h-[98px] object-contain"
/>
<h1 className="text-black text-[30px] font-normal font-unbounded py-2.5">Welcome on board!</h1>
<p className="text-[#000000CC] text-xl font-normal font-syne">
Youre all set. Lets create your first presentation.
</p>
{trackingEnabled !== null && (
<div className='flex items-center gap-3 mt-8 px-5 py-3.5 rounded-[10px] border border-[#EDEEEF] bg-white'>
<div>
<p className='text-sm font-medium text-[#191919] font-syne'>Usage analytics</p>
<p className='text-[11px] text-[#9CA3AF] font-syne leading-tight mt-0.5'>Help improve Presenton by sharing anonymous usage data.</p>
</div>
<Switch
checked={trackingEnabled}
onCheckedChange={handleTrackingToggle}
className='data-[state=checked]:bg-[#7C51F8]'
/>
</div>
)}
{trackingEnabled !== null && (
<div className="flex items-center gap-3 mt-8 px-5 py-3.5 rounded-[10px] border border-[#EDEEEF] bg-white">
<div>
<p className="text-sm font-medium text-[#191919] font-syne">Usage analytics</p>
<p className="text-[11px] text-[#9CA3AF] font-syne leading-tight mt-0.5">
Help improve Presenton by sharing anonymous usage data.
</p>
<button onClick={handleGoToUpload} className='bg-[#7C51F8] px-[23px] mt-8 py-[15px] rounded-[70px] text-white text-lg font-syne font-semibold'>My First Presentation 🚀</button>
<button onClick={fireRealisticConfetti} className='mt-3 flex items-center gap-1.5 text-sm text-[#7A5AF8] font-syne font-medium hover:underline'>
<PartyPopper className='w-4 h-4' /> Celebrate again!
</button>
</div>
<Switch
checked={trackingEnabled}
onCheckedChange={handleTrackingToggle}
className="data-[state=checked]:bg-[#7C51F8]"
/>
</div>
)}
<button onClick={handleGoToDashboard} className='absolute uppercase bottom-20 text-[#7A5AF8] flex items-center gap-2 right-10 text-xs font-normal font-syne'>Go to your dashboard <ArrowRight className='w-4 h-4 text-[#7A5AF8]' /></button>
</div>
)
}
<button
onClick={handleGoToUpload}
className="bg-[#7C51F8] px-[23px] mt-8 py-[15px] rounded-[70px] text-white text-lg font-syne font-semibold"
>
My First Presentation 🚀
</button>
<button
onClick={fireRealisticConfetti}
className="mt-3 flex items-center gap-1.5 text-sm text-[#7A5AF8] font-syne font-medium hover:underline"
>
<PartyPopper className="w-4 h-4" /> Celebrate again!
</button>
</div>
<button
onClick={handleGoToDashboard}
className="absolute uppercase bottom-20 text-[#7A5AF8] flex items-center gap-2 right-10 text-xs font-normal font-syne"
>
Go to your dashboard <ArrowRight className="w-4 h-4 text-[#7A5AF8]" />
</button>
</div>
);
};
export default FinalStep;
export default FinalStep

View file

@ -1,59 +1,64 @@
import { ChevronRight } from 'lucide-react'
import React from 'react'
const ModeSelectStep = ({ setStep, setSelectedMode }: { setStep: (step: number) => void, setSelectedMode: (mode: string) => void }) => {
const ModeSelectStep = ({ selectedMode, setStep, setSelectedMode }: { selectedMode: string, setStep: (step: number) => void, setSelectedMode: (mode: string) => void }) => {
return (
<div className='max-w-[650px]'>
<div className='mb-[70px]'>
<h2 className='mb-4 text-black text-[26px] font-normal font-unbounded '>Lets set up your AI workspace</h2>
<p className='text-[#000000CC] text-xl font-normal font-syne'>First, choose the intelligence behind your presentation generation.</p>
<h2 className='mb-4 text-black text-[26px] font-normal font-unbounded '>Choose how you want to generate presentations</h2>
<p className='text-[#000000CC] text-xl font-normal font-syne'>Pick a generation mode first. Youll connect your model providers in the next step.</p>
</div>
<div className='space-y-5'>
<div onClick={() => {
setSelectedMode("presenton")
setStep(2)
}} className='border font-syne border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'>
}} className={`border font-syne rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer ${selectedMode === "presenton" ? "border-[#a49cfc]" : "border-[#EDEEEF]"}`}>
<div className='flex items-center gap-6'>
<div className='rounded-[4px] bg-[#F4F3FF] p-[12px] w-[58px] h-[58px] flex items-center justify-center'>
<img src='/logo-with-bg.png' alt='presenton' className='w-full h-full object-contain' />
<div className='rounded-[4px] bg-[#F4F3FF] pt-[16.8px] pl-[16.8px] pb-[15.8px] pr-[17.1px] w-[74px] h-[74px] flex items-center justify-center'>
<img src='/logo-with-bg.png' alt='presenton' className='w-[40px] h-[41.4px] object-contain' />
</div>
<div className=''>
<div className='flex items-start gap-2 relative '>
<h3 className='text-black text-[18px] font-medium font-syne'>Presenton</h3>
<p className='bg-[#F4F3FF] px-3 py-1.5 rounded-[30px] text-[#7A5AF8] text-[9px] absolute left-[95px] top-[-10px]'>PPTX</p>
<h3 className='text-black text-[18px] font-medium font-syne'>Template Presentation Mode</h3>
<p className='bg-[#F4F3FF] px-3 py-1.5 rounded-[30px] text-[#7A5AF8] text-[9px] absolute left-[260px] top-[-10px]'>PPTX Export </p>
</div>
<p className='text-[#999999] text-[14px] font-normal font-syne'>Optimized for fast, structured slide generation.</p>
<p className='text-[#999999] text-[14px] font-normal font-syne'>Best for structured decks, editing, and PPTX export. Requires text and image providers.</p>
</div>
</div>
<ChevronRight className='w-6 h-6 text-[#B3B3B3]' />
</div>
<div
// onClick={() => {
// setSelectedMode("image")
// setStep(2)
// }}
className='border font-syne border-[#EDEEEF] cursor-not-allowed rounded-[11px] p-3 flex items-center justify-between gap-6 relative'>
<p className='text-black absolute top-1/2 -translate-y-1/2 right-14 flex items-center justify-center text-[14px] font-normal bg-[#F4F3FF] px-3 py-1.5 rounded-[30px]'>Coming soon</p>
<p className='text-black absolute top-[20px] right-14 flex items-center justify-center text-[14px] font-normal bg-[#F4F3FF] px-3 py-1.5 rounded-[30px]'>Coming soon</p>
<div className='flex items-center gap-6'>
<div className='rounded-[4px] bg-[#FFF6ED] p-[12px] w-[58px] h-[58px] flex items-center justify-center'>
<div className='rounded-[4px] bg-[#FFF6ED] p-[12px] w-[74px] h-[74px] flex items-center justify-center'>
<img src='/image_mode.png' alt='presenton' className='w-full h-full object-contain' />
</div>
<div className=''>
<div className='flex items-start gap-2 relative '>
<h3 className='text-black text-[18px] font-medium font-syne'>Generate with Image Model</h3>
<h3 className='text-black text-[18px] font-medium font-syne'>Image Slides Mode</h3>
<p className='bg-[#F4F3FF] px-3 py-1.5 rounded-[30px] text-[#7A5AF8] text-[9px] absolute left-[180px] top-[-10px]'>No PPTX Export </p>
</div>
<p className='text-[#999999] text-[14px] font-normal font-syne'>Generate presentations with visual layouts and elements.</p>
<p className='text-[#999999] text-[14px] font-normal font-syne'> Best for visual slide generation from image models. No PPTX export.</p>
</div>
</div>
<ChevronRight className='w-6 h-6 text-[#B3B3B3]' />
</div>
</div>
<div className='fixed bottom-16 mr-8 max-w-[1440px] right-16 flex justify-end items-center gap-2.5 '>
<button
onClick={() => {
setStep(2);
}}
className='border font-syne border-[#EDEEEF] bg-[#7C51F8] rounded-[58px] px-5 py-2.5 text-white text-xs font-semibold'>
Continue to providers
</button>
</div>
</div>
)
}

View file

@ -1,12 +1,18 @@
import React from 'react'
const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
const OnBoardingHeader = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => {
return (
<div className='relative z-20 flex items-center font-syne justify-end gap-1 mt-7 mb-[52px]'>
<div className='sticky top-8 z-20 flex items-center font-syne justify-end gap-1 mt-7 mb-[52px]'>
<div className='flex items-center gap-1'>
<div className={`${currentStep === 1 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
<div className='flex items-center gap-1 cursor-pointer'
onClick={() => {
if (currentStep > 1) {
setStep(1);
}
}}
>
<div className={`${currentStep === 1 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center `}>
1
</div>
<p className='text-[#010000] text-xs '>Select Mode</p>
@ -14,8 +20,14 @@ const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="1" viewBox="0 0 22 1" fill="none">
<path d="M0 0.5H21.5" stroke="#ECECEF" />
</svg>
<div className='flex items-center gap-1'>
<div className={`${currentStep === 2 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
<div className='flex items-center gap-1 cursor-pointer'
onClick={() => {
if (currentStep > 2) {
setStep(2);
}
}}
>
<div className={`${currentStep === 2 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center `}>
2
</div>
<p className='text-[#010000] text-xs '>Choose Providers</p>
@ -24,7 +36,7 @@ const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
<path d="M0 0.5H21.5" stroke="#ECECEF" />
</svg>
<div className='flex items-center gap-1'>
<div className={`${currentStep === 3 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
<div className={`${currentStep === 3 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center `}>
3
</div>
<p className='text-[#010000] text-xs '>Finish Setup</p>

View file

@ -1,16 +1,16 @@
import React from 'react'
const OnBoardingSlidebar = () => {
const OnBoardingSlidebar = ({ step }: { step: number }) => {
return (
<div className='bg-[#F6F6F9] w-[300px] relative'>
<img src="/Logo.png" alt="Presenton logo" className="absolute top-0 left-0 w-[128px] m-6" />
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="591" viewBox="0 0 296 591" fill="none">
<div className={`${step === 3 ? "bg-white" : "bg-[#F6F6F9]"} w-[300px] relative`}>
<img src="/Logo.png" alt="Presenton logo" className="sticky top-8 left-0 w-[128px] m-6" />
{step !== 3 && <svg xmlns="http://www.w3.org/2000/svg" width="296" height="591" viewBox="0 0 296 591" fill="none">
<path d="M291.5 183.5C311.916 183.5 328.5 200.271 328.5 221C328.5 241.729 311.916 258.5 291.5 258.5C271.084 258.5 254.5 241.729 254.5 221C254.5 200.271 271.084 183.5 291.5 183.5Z" stroke="#EDEEEF" strokeWidth="3" />
<path d="M291.5 131.238C340.408 131.238 380.089 171.407 380.09 220.998C380.09 270.589 340.408 310.758 291.5 310.758C242.591 310.758 202.91 270.589 202.91 220.998C202.91 171.407 242.591 131.238 291.5 131.238Z" stroke="#EDEEEF" strokeWidth="3" />
<path d="M291.5 43.6289C388.173 43.6289 466.576 123.021 466.576 220.998C466.576 318.975 388.174 398.368 291.5 398.368C194.826 398.368 116.424 318.975 116.424 220.998C116.424 123.022 194.826 43.629 291.5 43.6289Z" stroke="#EDEEEF" strokeWidth="3" />
<path d="M287.5 -62.5C434.322 -62.5 553.5 64.115 553.5 220.5C553.5 376.885 434.322 503.5 287.5 503.5C140.678 503.5 21.5 376.885 21.5 220.5C21.5 64.115 140.678 -62.5 287.5 -62.5Z" stroke="#EDEEEF" strokeWidth="3" />
<path d="M291 -176.5C495.019 -176.5 660.5 -5.07604 660.5 206.5C660.5 418.076 495.019 589.5 291 589.5C86.9809 589.5 -78.5 418.076 -78.5 206.5C-78.5 -5.07604 86.9809 -176.5 291 -176.5Z" stroke="#EDEEEF" strokeWidth="3" />
</svg>
</svg>}
</div>
)

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Button } from '../ui/button';
import { Check, CheckCircle, ChevronLeft, ChevronUp, Download, Eye, EyeOff, Loader2 } from 'lucide-react';
import { ArrowUpRight, Check, CheckCircle, ChevronLeft, ChevronUp, Download, Eye, EyeOff, Info, Loader2 } from 'lucide-react';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
import { DALLE_3_QUALITY_OPTIONS, GPT_IMAGE_1_5_QUALITY_OPTIONS, IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants';
import { cn } from '@/lib/utils';
@ -114,7 +114,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
return config.OLLAMA_MODEL || '';
case 'custom':
return config.CUSTOM_MODEL || '';
case 'codex':
case 'chatgpt':
return config.CODEX_MODEL || '';
default:
return '';
@ -231,7 +231,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
DALL·E 3 Image Quality
</label>
<div className="">
<Select value={llmConfig.DALL_E_3_QUALITY} onValueChange={(value) => setLlmConfig((prev) => ({
<Select value={llmConfig.DALL_E_3_QUALITY || 'standard'} onValueChange={(value) => setLlmConfig((prev) => ({
...prev,
DALL_E_3_QUALITY: value
}))}>
@ -258,7 +258,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</label>
<div className="">
<Select
value={llmConfig.GPT_IMAGE_1_5_QUALITY}
value={llmConfig.GPT_IMAGE_1_5_QUALITY || 'low'}
onValueChange={(value) => setLlmConfig((prev) => ({
...prev,
GPT_IMAGE_1_5_QUALITY: value
@ -315,11 +315,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
text_provider: textProvider,
text_provider_label: LLM_PROVIDERS[textProvider]?.label || textProvider || '',
text_model: textModel,
uses_chatgpt_login: textProvider === 'codex',
codex_model: llmConfig.CODEX_MODEL || '',
ollama_model: llmConfig.OLLAMA_MODEL || '',
ollama_uses_custom_url: !!llmConfig.USE_CUSTOM_URL,
custom_llm_url_set: Boolean((llmConfig.CUSTOM_LLM_URL || '').trim()),
uses_chatgpt_login: textProvider === 'chatgpt',
image_generation_enabled: imageGenerationEnabled,
image_provider: imageProvider,
image_provider_label: imageGenerationEnabled
@ -327,13 +323,6 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
: 'Image generation disabled',
image_quality: imageGenerationEnabled ? getSelectedImageQuality(llmConfig) : ''
});
trackEvent(MixpanelEvent.Onboarding_Configuration_Saved, {
pathname,
text_provider: textProvider,
text_model: textModel,
image_generation_enabled: imageGenerationEnabled,
image_provider: imageProvider,
});
toast.info("Configuration saved successfully");
setStep(3)
@ -361,25 +350,20 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
}, [llmConfig.LLM, modelsChecked, modelsLoading]);
return (
<div className='w-full max-w-[640px] font-syne'>
<div className='w-full max-w-[660px] font-syne pb-10'>
<p className='px-2.5 py-0.5 w-fit text-[#7A5AF8] rounded-[50px] border border-[#EDEEEF] text-[10px] font-medium mb-5 font-syne'>PRESENTON</p>
<div className='mb-[54px]'>
<div className=''>
<h2 className='mb-4 text-black text-[26px] font-normal font-unbounded '>Choose your content providers</h2>
<p className='text-[#000000CC] text-xl font-normal font-syne'>Select the AI engines that will generate your slide text and visuals.</p>
</div>
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig(prev => ({
...prev,
[normalizedField]: value
}));
}}
/>
<div className='flex items-center gap-2 bg-[#F0F3F9B2] rounded-[8px] px-6 py-2.5 my-[54px]'>
<Info className='w-4 h-4 fill-[#003399] stroke-white' />
<p className='text-sm text-[#5F6062] font-medium'>Runs locally on your device. Your API keys and generation setup stay on your machine.</p>
</div>
{/* Text Provider */}
<div className='p-3 border border-[#EDEEEF] rounded-[11px] '>
<div className='p-3 border border-[#EDEEEF] rounded-[11px] bg-white '>
<div className="flex items-center gap-[24.3px] mb-[42px]">
<div className='w-[74px] h-[74px] rounded-[4px] pt-[16.8px] pr-[17.15px] pb-[17.2px] pl-[16.85px] flex items-center justify-center'
style={{ backgroundColor: '#4C55541A' }}
@ -398,7 +382,22 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</p>
</div>
</div>
<div className='flex items-start gap-4 '>
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig(prev => ({
...prev,
[normalizedField]: value
}));
}}
/>
<div className='flex items-center gap-2.5 my-[30px]'>
<div className='w-full h-[1px] bg-[#E1E1E5]' />
<p className='text-xs font-normal text-[#999999]'>OR</p>
<div className='w-full h-[1px] bg-[#E1E1E5]' />
</div>
<div className='flex flex-col items-start gap-4 '>
<div className="flex flex-col justify-start w-full ">
<label className="block text-sm font-medium text-gray-700 mb-2">
@ -427,8 +426,8 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[215px] "
align="start"
className="p-0 w-full "
align="end"
>
<Command>
@ -484,7 +483,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
USE_CUSTOM_URL: true,
OLLAMA_URL: prev.OLLAMA_URL || 'http://localhost:11434'
}))}
className="mt-8 py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border border-[#EDEEEF] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
className="py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border border-[#EDEEEF] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
>
Use Ollama URL
</button>
@ -519,7 +518,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</>
)}
</>
) : llmConfig.LLM === 'codex' ? (
) : llmConfig.LLM === 'chatgpt' ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select GPT Model
@ -581,9 +580,14 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
) : (
<>
<label className="block text-sm font-medium capitalize text-gray-700 mb-2">
{llmConfig.LLM === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
</label>
<div className='flex items-center justify-between mb-2'>
<label className="block text-sm font-medium capitalize text-gray-700 ">
{llmConfig.LLM === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
</label>
{llmConfig.LLM && LLM_PROVIDERS[llmConfig.LLM!]?.getApiKeyUrl && <a href={LLM_PROVIDERS[llmConfig.LLM!]?.getApiKeyUrl || ""} target='_blank' className='text-[#666666] text-xs font-normal flex items-center gap-1'>Get API Key <ArrowUpRight className='w-3.5 h-3.5' /></a>}
</div>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
@ -622,7 +626,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
{llmConfig.LLM !== 'ollama' && llmConfig.LLM !== 'codex' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
{llmConfig.LLM !== 'ollama' && llmConfig.LLM !== 'chatgpt' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
onClick={fetchAvailableModels}
@ -633,9 +637,9 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
(llmConfig.LLM === 'anthropic' && !currentApiKey) ||
(llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL)
}
className={`mt-4 py-2.5 bg-[#EDEEEF] disabled:opacity-50 disabled:cursor-not-allowed px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
className={`mt-4 py-2.5 bg-[#EDEEEF] disabled:opacity-50 disabled:cursor-not-allowed px-3.5 w-full rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-[#101323] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
: " border-[#EDEEEF] text-[#101323] hover:bg-[#EDEEEF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
>
{modelsLoading ? (
@ -644,7 +648,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
Checking for models...
</span>
) : (
"Check models"
"Validate & Load Models"
)}
</button>
)}
@ -652,10 +656,10 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
<div className='flex items-start gap-4 mt-4'>
<p className='text-sm font-medium text-gray-700 mb-2 w-full'></p>
{/* Model Selection - only show if models are available */}
{llmConfig.LLM !== 'codex' && modelsChecked && availableModels.length > 0 && (
{llmConfig.LLM !== 'chatgpt' && modelsChecked && availableModels.length > 0 && (
<div className="w-full">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
@ -740,7 +744,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</div>
{/* Image Provider */}
<div className={`p-3 border border-[#EDEEEF] rounded-[11px] relative mt-5 ${llmConfig.DISABLE_IMAGE_GENERATION ? "bg-[#F9FAFB]" : ""}`}>
<div className={`p-3 border border-[#EDEEEF] rounded-[11px] relative mt-5 bg-white ${llmConfig.DISABLE_IMAGE_GENERATION ? "bg-[#F9FAFB]" : ""}`}>
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center absolute top-3 right-3'>
<div className='flex justify-end items-center'>
<Switch
@ -769,7 +773,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</div>
{!llmConfig.DISABLE_IMAGE_GENERATION && (
<div className='flex gap-4'>
<div className='flex flex-col gap-4'>
{/* Image Provider Selection */}
<div className="w-full">
<label className="block text-sm font-medium text-gray-700 mb-2">
@ -895,9 +899,13 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
// Show API key input for other providers
return (
<div className="w-full ">
<label className="block text-sm font-medium text-gray-700 mb-2">
{provider.apiKeyFieldLabel}
</label>
<div className='flex items-center justify-between mb-2'>
<label className="block text-sm font-medium text-gray-700">
{provider.apiKeyFieldLabel}
</label>
{provider.getApiKeyUrl && <a href={provider.getApiKeyUrl || ""} target='_blank' className='text-[#666666] text-xs font-normal flex items-center gap-1'>Get API Key <ArrowUpRight className='w-3.5 h-3.5' /></a>}
</div>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
@ -928,9 +936,9 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
)}
{!llmConfig.DISABLE_IMAGE_GENERATION && <div className='flex justify-end items-center mt-[18px]'>
{!llmConfig.DISABLE_IMAGE_GENERATION && <div className='flex flex-col justify-end items-center mt-[18px]'>
<div className='w-full flex items-center gap-4'>
<p className='w-full'></p>
{renderQualitySelector(llmConfig)}
</div>
{llmConfig.IMAGE_PROVIDER === "comfyui" && <div className='w-full'>

View file

@ -1,10 +1,26 @@
"use client"
import type React from "react"
import { useTheme } from "next-themes"
import { BadgeCheck, Info, Loader2, ShieldAlert } from "lucide-react"
import { Toaster as Sonner, toast as sonnerToast } from "sonner"
/** Toasts with both title and description. */
/** Blue circle for neutral / informational toasts (matches web `servers/nextjs` Toaster). */
function NeutralToastIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19" fill="none">
<path d="M9.12333 17.4567C13.7257 17.4567 17.4567 13.7257 17.4567 9.12337C17.4567 4.521 13.7257 0.790039 9.12333 0.790039C4.52096 0.790039 0.790001 4.521 0.790001 9.12337C0.790001 13.7257 4.52096 17.4567 9.12333 17.4567Z" fill="url(#paint0_linear_4686_451)" stroke="#2863A3" strokeWidth="1.58" strokeLinecap="round" strokeLinejoin="round" />
<defs>
<linearGradient id="paint0_linear_4686_451" x1="9.12333" y1="0.790039" x2="9.12333" y2="17.4567" gradientUnits="userSpaceOnUse">
<stop stopColor="#1880F6" />
<stop offset="1" stopColor="#75B5FF" />
</linearGradient>
</defs>
</svg>
)
}
/** Toasts with both title and description (matches styled [data-title] / [data-description]). */
export const notify = {
error: (title: string, description: string) =>
sonnerToast.error(title, { description }),
@ -16,158 +32,220 @@ export const notify = {
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const Toaster = ({ icons, ...props }: ToasterProps) => {
const defaultIcons: NonNullable<ToasterProps["icons"]> = {
success: <BadgeCheck aria-hidden="true" />,
error: <ShieldAlert aria-hidden="true" />,
info: <Info className="fill-[#1880F6] stroke-white" />,
warning: <ShieldAlert aria-hidden="true" />,
loading: <Loader2 aria-hidden="true" className="animate-spin" />,
close: <span aria-hidden="true">Got it!</span>,
}
return (
<>
<style jsx global>{`
/* Base toast styling */
[data-sonner-toast] {
border-radius: 12px !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
backdrop-filter: blur(8px) !important;
}
/* Success Toast */
[data-sonner-toast][data-type="success"] {
background: rgb(248 250 252) !important; /* slate-50 */
border: 1px solid rgb(28, 138, 68) !important; /* green-500 border */
border-left: 4px solid rgb(28, 138, 68) !important; /* green-500 left accent */
}
[data-sonner-toast][data-type="success"] [data-title] {
color: rgb(15 23 42) !important; /* slate-900 */
font-weight: 500 !important;
}
[data-sonner-toast][data-type="success"] [data-description] {
color: rgb(71 85 105) !important; /* slate-600 */
/* Near Sonner default width on desktop; nearly full width on narrow screens */
[data-sonner-toaster] {
--width: min(100dvw - 1.5rem, 22.5rem) !important;
box-sizing: border-box !important;
}
/* Error Toast */
[data-sonner-toast][data-type="error"] {
background: rgb(248 250 252) !important; /* slate-50 */
border: 1px solid rgb(186, 48, 48) !important; /* red-500 border */
border-left: 4px solid rgb(186, 48, 48) !important; /* red-500 left accent */
}
[data-sonner-toast][data-type="error"] [data-title] {
color: rgb(15 23 42) !important; /* slate-900 */
font-weight: 500 !important;
}
[data-sonner-toast][data-type="error"] [data-description] {
color: rgb(71 85 105) !important; /* slate-600 */
@media (min-width: 640px) {
[data-sonner-toaster] {
--width: min(100dvw - 2rem, 24rem) !important;
}
}
/* Info Toast */
[data-sonner-toast][data-type="info"] {
background: rgb(248 250 252) !important; /* slate-50 */
border: 1px solid rgb(59 130 246) !important; /* blue-500 border */
border-left: 4px solid rgb(59 130 246) !important; /* blue-500 left accent */
}
[data-sonner-toast][data-type="info"] [data-title] {
color: rgb(15 23 42) !important; /* slate-900 */
font-weight: 500 !important;
}
[data-sonner-toast][data-type="info"] [data-description] {
color: rgb(71 85 105) !important; /* slate-600 */
/* Neutral "card" toast container — design tokens */
[data-sonner-toast][data-styled="true"] {
border-radius: 10px !important;
border: 1px solid var(--Base-Gray-700, #e1e1e5) !important;
background: rgba(255, 255, 255, 0.6) !important;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.06) !important;
padding: clamp(9px, 0.5rem + 0.35vw, 12px) clamp(11px, 0.65rem + 0.5vw, 14px) !important;
gap: clamp(8px, 0.5rem + 0.35vw, 11px) !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
width: 100% !important;
max-width: 100% !important;
}
/* Warning Toast */
[data-sonner-toast][data-type="warning"] {
background: rgb(248 250 252) !important; /* slate-50 */
border: 1px solid rgb(245 158 11) !important; /* amber-500 border */
border-left: 4px solid rgb(245 158 11) !important; /* amber-500 left accent */
}
[data-sonner-toast][data-type="warning"] [data-title] {
color: rgb(15 23 42) !important; /* slate-900 */
/* Typography — slight scale-up from original 12px, capped modestly */
[data-sonner-toast][data-styled="true"] [data-title] {
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif !important;
font-size: clamp(0.8125rem, 0.8rem + 0.12vw, 0.9375rem) !important;
font-weight: 500 !important;
}
[data-sonner-toast][data-type="warning"] [data-description] {
color: rgb(71 85 105) !important; /* slate-600 */
line-height: 1.35 !important;
letter-spacing: 0.03em !important;
color: rgb(15 23 42) !important; /* slate-900 */
text-transform: none !important;
}
/* Loading Toast */
[data-sonner-toast][data-type="loading"] {
background: rgb(248 250 252) !important; /* slate-50 */
border: 1px solid rgb(139 92 246) !important; /* violet-500 border */
border-left: 4px solid rgb(139 92 246) !important; /* violet-500 left accent */
}
[data-sonner-toast][data-type="loading"] [data-title] {
color: rgb(15 23 42) !important; /* slate-900 */
font-weight: 500 !important;
}
[data-sonner-toast][data-type="loading"] [data-description] {
color: rgb(71 85 105) !important; /* slate-600 */
[data-sonner-toast][data-styled="true"] [data-description] {
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif !important;
font-size: clamp(0.6875rem, 0.67rem + 0.1vw, 0.8125rem) !important;
font-weight: 400 !important;
line-height: 1.4 !important;
letter-spacing: 0.03em !important;
color: rgb(100 116 139) !important; /* slate-500 */
}
/* Dark mode */
.dark [data-sonner-toast][data-type="success"] {
background: rgb(15 23 42) !important; /* slate-900 */
border: 1px solid rgb(34 197 94) !important;
border-left: 4px solid rgb(34 197 94) !important;
[data-sonner-toast][data-styled="true"] [data-content] {
gap: clamp(2px, 0.15vw, 5px) !important;
flex: 1 1 auto !important;
min-width: 0 !important;
}
.dark [data-sonner-toast][data-type="success"] [data-title] {
/* Left icon badge */
[data-sonner-toast][data-styled="true"] [data-icon] {
width: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
height: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
flex-shrink: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 !important;
color: rgb(51 65 85) !important; /* slate-700 */
}
[data-sonner-toast][data-styled="true"] [data-icon] svg {
width: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
height: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
}
/* Per-type icon colors */
[data-sonner-toast][data-type="success"] [data-icon] {
color: rgb(22, 163, 74) !important;
}
[data-sonner-toast][data-type="error"] [data-icon] {
color: rgb(220, 38, 38) !important;
}
[data-sonner-toast][data-type="info"] [data-icon] {
color: rgb(37, 99, 235) !important;
}
[data-sonner-toast][data-type="warning"] [data-icon] {
color: rgb(217, 119, 6) !important;
}
[data-sonner-toast][data-type="loading"] [data-icon] {
color: rgb(124, 58, 237) !important;
}
/* Outline buttons like the mock ("Got it!") */
[data-sonner-toast][data-styled="true"] [data-button] {
height: auto !important;
padding: clamp(4px, 0.3rem + 0.2vw, 7px)
clamp(7px, 0.5rem + 0.25vw, 10px) !important;
border-radius: 6px !important;
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif !important;
font-size: clamp(0.625rem, 0.62rem + 0.08vw, 0.75rem) !important;
font-weight: 400 !important;
background: rgb(255 255 255) !important;
color: #3F3F3F !important;
border: 1px solid #EDEEEF !important;
box-shadow: none !important;
}
/* Always-present "Got it!" button (styled close button) */
[data-sonner-toast][data-styled="true"] [data-close-button] {
position: static !important;
inset: auto !important;
transform: none !important;
order: 9999 !important;
flex: 0 0 auto !important;
flex-shrink: 0 !important;
white-space: nowrap !important;
width: auto !important;
height: auto !important;
padding: clamp(4px, 0.3rem + 0.2vw, 7px)
clamp(7px, 0.5rem + 0.25vw, 10px) !important;
border-radius: 6px !important;
margin-left: auto !important;
margin-right: 0 !important;
align-self: center !important;
background: rgb(255 255 255) !important;
color: #3f3f3f !important;
border: 1px solid #edeeef !important;
box-shadow: none !important;
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif !important;
font-size: clamp(0.625rem, 0.62rem + 0.08vw, 0.75rem) !important;
font-weight: 400 !important;
line-height: 1.3 !important;
letter-spacing: 0.02em !important;
}
[data-sonner-toast][data-styled="true"] [data-close-button]:hover {
background: rgb(248 250 252) !important; /* slate-50 */
}
[data-sonner-toast][data-styled="true"] [data-button]:hover {
background: rgb(248 250 252) !important; /* slate-50 */
}
/* Dark mode — same radius, border weight, shadow; frosted dark surface */
.dark [data-sonner-toast][data-styled="true"] {
border-radius: 10px !important;
border: 1px solid rgba(148, 163, 184, 0.22) !important;
background: rgba(2, 6, 23, 0.6) !important;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.06) !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
}
.dark [data-sonner-toast][data-styled="true"] [data-title] {
color: rgb(248 250 252) !important; /* slate-50 */
}
.dark [data-sonner-toast][data-type="success"] [data-description] {
.dark [data-sonner-toast][data-styled="true"] [data-description] {
color: rgb(148 163 184) !important; /* slate-400 */
}
.dark [data-sonner-toast][data-type="error"] {
background: rgb(15 23 42) !important; /* slate-900 */
border: 1px solid rgb(239 68 68) !important;
border-left: 4px solid rgb(239 68 68) !important;
}
.dark [data-sonner-toast][data-type="error"] [data-title] {
color: rgb(248 250 252) !important; /* slate-50 */
}
.dark [data-sonner-toast][data-type="error"] [data-description] {
color: rgb(148 163 184) !important; /* slate-400 */
.dark [data-sonner-toast][data-styled="true"] [data-button] {
background: rgb(2 6 23) !important;
color: rgb(248 250 252) !important;
border: 1px solid rgba(148, 163, 184, 0.26) !important;
}
.dark [data-sonner-toast][data-type="info"] {
background: rgb(15 23 42) !important; /* slate-900 */
border: 1px solid rgb(59 130 246) !important;
border-left: 4px solid rgb(59 130 246) !important;
}
.dark [data-sonner-toast][data-type="info"] [data-title] {
color: rgb(248 250 252) !important; /* slate-50 */
}
.dark [data-sonner-toast][data-type="info"] [data-description] {
color: rgb(148 163 184) !important; /* slate-400 */
.dark [data-sonner-toast][data-styled="true"] [data-close-button] {
background: rgb(2 6 23) !important;
color: rgb(248 250 252) !important;
border: 1px solid rgba(148, 163, 184, 0.26) !important;
}
.dark [data-sonner-toast][data-type="warning"] {
.dark [data-sonner-toast][data-styled="true"] [data-button]:hover {
background: rgb(15 23 42) !important; /* slate-900 */
border: 1px solid rgb(245 158 11) !important;
border-left: 4px solid rgb(245 158 11) !important;
}
.dark [data-sonner-toast][data-type="warning"] [data-title] {
color: rgb(248 250 252) !important; /* slate-50 */
}
.dark [data-sonner-toast][data-type="warning"] [data-description] {
color: rgb(148 163 184) !important; /* slate-400 */
}
.dark [data-sonner-toast][data-type="loading"] {
.dark [data-sonner-toast][data-styled="true"] [data-close-button]:hover {
background: rgb(15 23 42) !important; /* slate-900 */
border: 1px solid rgb(139 92 246) !important;
border-left: 4px solid rgb(139 92 246) !important;
}
.dark [data-sonner-toast][data-type="loading"] [data-title] {
color: rgb(248 250 252) !important; /* slate-50 */
}
.dark [data-sonner-toast][data-type="loading"] [data-description] {
color: rgb(148 163 184) !important; /* slate-400 */
}
`}</style>
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={{ zIndex: 999999999 }}
className="toaster group z-50 bg-transparent"
icons={{ ...defaultIcons, ...(icons ?? {}) }}
toastOptions={{
closeButtonAriaLabel: "Dismiss notification",
classNames: {
toast: "group toast",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-slate-900 group-[.toast]:text-white hover:group-[.toast]:bg-slate-800 group-[.toast]:rounded-lg group-[.toast]:px-3 group-[.toast]:py-1.5",
cancelButton: "group-[.toast]:bg-slate-200 group-[.toast]:text-slate-700 hover:group-[.toast]:bg-slate-300 group-[.toast]:rounded-lg group-[.toast]:px-3 group-[.toast]:py-1.5",
actionButton:
"group-[.toast]:rounded-2xl group-[.toast]:border group-[.toast]:border-slate-200 group-[.toast]:bg-white group-[.toast]:text-slate-900 hover:group-[.toast]:bg-slate-50",
cancelButton:
"group-[.toast]:rounded-2xl group-[.toast]:border group-[.toast]:border-slate-200 group-[.toast]:bg-white group-[.toast]:text-slate-700 hover:group-[.toast]:bg-slate-50",
},
}}
{...props}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View file

@ -14,6 +14,7 @@ export interface ImageProviderOption {
requiresApiKey?: boolean;
apiKeyField?: string;
apiKeyFieldLabel?: string;
getApiKeyUrl?: string;
}
export interface LLMProviderOption {
@ -24,6 +25,7 @@ export interface LLMProviderOption {
model_label?: string;
url?: string;
icon?: string;
getApiKeyUrl?: string;
}
export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
@ -31,61 +33,67 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
value: "pexels",
label: "Pexels",
description: "Free stock photo and video platform",
icon: "/icons/pexels.png",
icon: "/providers/pexel.png",
requiresApiKey: true,
apiKeyField: "PEXELS_API_KEY",
apiKeyFieldLabel: "Pexels API Key",
getApiKeyUrl: "https://docs.presenton.ai/help/get-api-keys/get-pexels-api-key",
},
pixabay: {
value: "pixabay",
label: "Pixabay",
description: "Free images and videos",
icon: "/icons/pixabay.png",
icon: "/providers/pixabay.png",
requiresApiKey: true,
apiKeyField: "PIXABAY_API_KEY",
apiKeyFieldLabel: "Pixabay API Key",
getApiKeyUrl: "https://docs.presenton.ai/help/get-api-keys/get-pixabay-api-keyhttps://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
"dall-e-3": {
value: "dall-e-3",
label: "DALL-E 3",
description: "OpenAI's image generation model",
icon: "/icons/dall-e.png",
icon: "/providers/openai.png",
requiresApiKey: true,
apiKeyField: "OPENAI_API_KEY",
apiKeyFieldLabel: "OpenAI API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
"gpt-image-1.5": {
value: "gpt-image-1.5",
label: "GPT Image 1.5",
description: "OpenAI's image generation model",
icon: "/icons/gpt.png",
icon: "/providers/openai.png",
requiresApiKey: true,
apiKeyField: "OPENAI_API_KEY",
apiKeyFieldLabel: "OpenAI API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
gemini_flash: {
value: "gemini_flash",
label: "Gemini Flash",
description: "Google's fast image generation model",
icon: "/icons/google.png",
icon: "/providers/gemini-color.svg",
requiresApiKey: true,
apiKeyField: "GOOGLE_API_KEY",
apiKeyFieldLabel: "Google API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
nanobanana_pro: {
value: "nanobanana_pro",
label: "NanoBanana Pro",
description: "Google's advanced image generation model",
icon: "/icons/google.png",
icon: "/providers/gemini-color.svg",
requiresApiKey: true,
apiKeyField: "GOOGLE_API_KEY",
apiKeyFieldLabel: "Google API Key",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
comfyui: {
value: "comfyui",
label: "ComfyUI",
description: "Use your local ComfyUI server with custom workflows",
icon: "/icons/comfyui.png",
icon: "/providers/comfyui-color.svg",
requiresApiKey: false,
apiKeyField: "COMFYUI_URL",
apiKeyFieldLabel: "ComfyUI Server URL",
@ -93,45 +101,49 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
};
export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
// codex: {
// value: "codex",
// label: "ChatGPT",
// description: "ChatGPT Plus/Pro via OAuth",
// icon: "/providers/openai.png",
// },
openai: {
value: "openai",
label: "OpenAI",
description: "OpenAI's latest text generation model",
url: "https://api.openai.com/v1",
icon: "/icons/openai.png",
icon: "/providers/openai.png",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+openai+api+key&ie=UTF-8",
},
google: {
value: "google",
label: "Google",
description: "Google's primary text generation model",
url: "https://api.google.com/v1",
icon: "/icons/google.png",
icon: "/providers/gemini-color.svg",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+google+AI+studio+api+key&sxsrf=ANbL-n5_hUGaEiG9v6k9VxZWyv0mqO0Jew%3A1776339625724",
},
anthropic: {
value: "anthropic",
label: "Anthropic",
description: "Anthropic's Claude models",
url: "https://api.anthropic.com/v1",
icon: "/icons/anthropic.png",
icon: "/providers/claude-color.svg",
getApiKeyUrl: "https://www.google.com/search?q=how+to+get+anthropic+api+key&sxsrf=ANbL-n7lsueZQ88L56HhqC1ch2PGD0rbNQ%3A1776339632265",
},
ollama: {
value: "ollama",
label: "Ollama",
description: "Ollama's primary text generation model",
icon: "/icons/ollama.png",
icon: "/providers/ollama.svg",
},
custom: {
value: "custom",
label: "Custom",
description: "Custom LLM",
icon: "/icons/custom.png",
},
codex: {
value: "codex",
label: "ChatGPT",
description: "ChatGPT Plus/Pro via OAuth",
icon: "/icons/chatgpt.png",
description: "OpenAI-compatible LLM",
icon: "/providers/custom.svg",
},
};
export const DALLE_3_QUALITY_OPTIONS = [

View file

@ -57,7 +57,7 @@ export const getLLMConfigValidationError = (
if (!isProvided(llmConfig.CUSTOM_MODEL)) {
return 'No model selected for your custom endpoint. Use "Check models" after entering the URL, then choose a model.';
}
} else if (llm === "codex") {
} else if (llm === "codex" || llm === "chatgpt") {
if (!isProvided(llmConfig.CODEX_MODEL)) {
return "Select a Codex model.";
}