feat(app): add new setup wizard

This commit is contained in:
ItzCrazyKns
2025-10-19 13:54:35 +05:30
parent df1ed5f0f9
commit 9d0e2e7f7c
4 changed files with 321 additions and 10 deletions

View File

@@ -0,0 +1,23 @@
import configManager from '@/lib/config';
import { NextRequest } from 'next/server';
export const POST = async (req: NextRequest) => {
try {
configManager.markSetupComplete();
return Response.json(
{
message: 'Setup marked as complete.',
},
{
status: 200,
},
);
} catch (err) {
console.error('Error marking setup as complete: ', err);
return Response.json(
{ message: 'An error has occurred.' },
{ status: 500 },
);
}
};

View File

@@ -1,3 +1,5 @@
export const dynamic = 'force-dynamic';
import type { Metadata } from 'next';
import { Montserrat } from 'next/font/google';
import './globals.css';
@@ -5,6 +7,8 @@ import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider';
import configManager from '@/lib/config';
import SetupWizard from '@/components/Setup/SetupWizard';
const montserrat = Montserrat({
weight: ['300', '400', '500', '700'],
@@ -24,20 +28,29 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const setupComplete = configManager.isSetupComplete();
const configSections = configManager.getUIConfigSections();
return (
<html className="h-full" lang="en" suppressHydrationWarning>
<body className={cn('h-full', montserrat.className)}>
<ThemeProvider>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
unstyled: true,
classNames: {
toast:
'bg-light-primary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
},
}}
/>
{setupComplete ? (
<>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
unstyled: true,
classNames: {
toast:
'bg-light-secondary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
},
}}
/>
</>
) : (
<SetupWizard configSections={configSections} />
)}
</ThemeProvider>
</body>
</html>

View File

@@ -0,0 +1,149 @@
import {
ConfigModelProvider,
UIConfigField,
UIConfigSections,
} from '@/lib/config/types';
import { motion } from 'framer-motion';
import { ArrowLeft, ArrowRight, Check } from 'lucide-react';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import AddProvider from '../Settings/Sections/Models/AddProviderDialog';
import ModelProvider from '../Settings/Sections/Models/ModelProvider';
const SetupConfig = ({
configSections,
setupState,
setSetupState,
}: {
configSections: UIConfigSections;
setupState: number;
setSetupState: (state: number) => void;
}) => {
const [providers, setProviders] = useState<ConfigModelProvider[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isFinishing, setIsFinishing] = useState(false);
useEffect(() => {
const fetchProviders = async () => {
try {
setIsLoading(true);
const res = await fetch('/api/providers');
if (!res.ok) throw new Error('Failed to fetch providers');
const data = await res.json();
setProviders(data.providers || []);
} catch (error) {
console.error('Error fetching providers:', error);
toast.error('Failed to load providers');
} finally {
setIsLoading(false);
}
};
if (setupState === 2) {
fetchProviders();
}
}, [setupState]);
const handleFinish = async () => {
try {
setIsFinishing(true);
const res = await fetch('/api/config/setup-complete', {
method: 'POST',
});
if (!res.ok) throw new Error('Failed to complete setup');
window.location.reload();
} catch (error) {
console.error('Error completing setup:', error);
toast.error('Failed to complete setup');
setIsFinishing(false);
}
};
const hasProviders = providers.length > 0;
return (
<div className="w-[95vw] md:w-[80vw] lg:w-[65vw] mx-auto px-2 sm:px-4 md:px-6 flex flex-col space-y-6">
{setupState === 2 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: { duration: 0.5, delay: 0.1 },
}}
className="w-full h-[calc(95vh-80px)] bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 rounded-xl shadow-sm flex flex-col overflow-hidden"
>
<div className="flex-1 overflow-y-auto px-3 sm:px-4 md:px-6 py-4 md:py-6">
<div className="flex flex-row justify-between items-center mb-4 md:mb-6 pb-3 md:pb-4 border-b border-light-200 dark:border-dark-200">
<div>
<p className="text-xs sm:text-sm font-medium text-black dark:text-white">
Manage Providers
</p>
<p className="text-[10px] sm:text-xs text-black/50 dark:text-white/50 mt-0.5">
Add and configure your model providers
</p>
</div>
<AddProvider
modelProviders={configSections.modelProviders}
setProviders={setProviders}
/>
</div>
<div className="space-y-3 md:space-y-4">
{isLoading ? (
<div className="flex items-center justify-center py-8 md:py-12">
<p className="text-xs sm:text-sm text-black/50 dark:text-white/50">
Loading providers...
</p>
</div>
) : providers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 md:py-12 text-center">
<p className="text-xs sm:text-sm font-medium text-black/70 dark:text-white/70">
No providers configured
</p>
</div>
) : (
providers.map((provider) => (
<ModelProvider
key={`provider-${provider.id}`}
fields={
(configSections.modelProviders.find(
(f) => f.key === provider.type,
)?.fields ?? []) as UIConfigField[]
}
modelProvider={provider}
setProviders={setProviders}
/>
))
)}
</div>
</div>
</motion.div>
)}
<div className="flex flex-row items-center justify-between pt-2">
{setupState === 2 && (
<motion.button
initial={{ opacity: 0, x: 10 }}
animate={{
opacity: 1,
x: 0,
transition: { duration: 0.5 },
}}
onClick={handleFinish}
disabled={!hasProviders || isLoading || isFinishing}
className="flex flex-row items-center gap-1.5 md:gap-2 px-3 md:px-5 py-2 md:py-2.5 rounded-lg bg-[#24A0ED] text-white hover:bg-[#1e8fd1] active:scale-95 transition-all duration-200 font-medium text-xs sm:text-sm disabled:bg-light-200 dark:disabled:bg-dark-200 disabled:text-black/40 dark:disabled:text-white/40 disabled:cursor-not-allowed disabled:active:scale-100"
>
<span>{isFinishing ? 'Finishing...' : 'Finish'}</span>
<Check className="w-4 h-4 md:w-[18px] md:h-[18px]" />
</motion.button>
)}
</div>
</div>
);
};
export default SetupConfig;

View File

@@ -0,0 +1,126 @@
'use client';
import { useEffect, useState } from 'react';
import { UIConfigSections } from '@/lib/config/types';
import { AnimatePresence, motion } from 'framer-motion';
import SetupConfig from './SetupConfig';
const SetupWizard = ({
configSections,
}: {
configSections: UIConfigSections;
}) => {
const [showWelcome, setShowWelcome] = useState(true);
const [showSetup, setShowSetup] = useState(false);
const [setupState, setSetupState] = useState(1);
const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
useEffect(() => {
(async () => {
await delay(2500);
setShowWelcome(false);
await delay(600);
setShowSetup(true);
setSetupState(1);
await delay(1500);
setSetupState(2);
})();
}, []);
return (
<div className="bg-light-primary dark:bg-dark-primary h-screen w-screen fixed inset-0 overflow-hidden">
<AnimatePresence>
{showWelcome && (
<div className="absolute inset-0 flex items-center justify-center overflow-hidden">
<motion.div
className="absolute flex flex-col items-center justify-center h-full"
initial={{ opacity: 1 }}
exit={{ opacity: 0, scale: 1.1 }}
transition={{ duration: 0.6 }}
>
<motion.h2
transition={{ duration: 0.6 }}
initial={{ opacity: 0, translateY: '30px' }}
animate={{ opacity: 1, translateY: '0px' }}
className="text-4xl md:text-6xl xl:text-8xl font-normal font-['Instrument_Serif'] tracking-tight"
>
Welcome to{' '}
<span className="text-[#24A0ED] italic font-['PP_Editorial']">
Perplexica
</span>
</motion.h2>
<motion.p
transition={{ delay: 0.8, duration: 0.7 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-black/70 dark:text-white/70 text-sm md:text-lg xl:text-2xl mt-2"
>
<span className="font-light">Web search,</span>{' '}
<span className="font-light font-['PP_Editorial'] italic">
reimagined
</span>
</motion.p>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{
opacity: 0.2,
scale: 1,
transition: { delay: 0.8, duration: 0.7 },
}}
exit={{ opacity: 0, scale: 1.1, transition: { duration: 0.6 } }}
className="bg-[#24A0ED] left-50 translate-x-[-50%] h-[250px] w-[250px] rounded-full relative z-40 blur-[100px]"
/>
</div>
)}
{showSetup && (
<div className="absolute inset-0 flex items-center justify-center overflow-hidden">
<AnimatePresence mode="wait">
{setupState === 1 && (
<motion.p
key="setup-text"
transition={{ duration: 0.6 }}
initial={{ opacity: 0, translateY: '30px' }}
animate={{ opacity: 1, translateY: '0px' }}
exit={{
opacity: 0,
translateY: '-30px',
transition: { duration: 0.6 },
}}
className="text-2xl md:text-4xl xl:text-6xl font-normal font-['Instrument_Serif'] tracking-tight"
>
Let us get{' '}
<span className="text-[#24A0ED] italic font-['PP_Editorial']">
Perplexica
</span>{' '}
set up for you
</motion.p>
)}
{setupState > 1 && (
<motion.div
key="setup-config"
initial={{ opacity: 0, translateY: '30px' }}
animate={{
opacity: 1,
translateY: '0px',
transition: { duration: 0.6 },
}}
>
<SetupConfig
configSections={configSections}
setupState={setupState}
setSetupState={setSetupState}
/>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</AnimatePresence>
</div>
);
};
export default SetupWizard;