refactor: Update Onboarding UI
This commit is contained in:
parent
cfc7233447
commit
41f9eae61d
25 changed files with 997 additions and 845 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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..." />
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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'>You’re all set. Let’s 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">
|
||||
You’re all set. Let’s 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
|
||||
|
|
|
|||
|
|
@ -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 '>Let’s 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. You’ll 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue