From 9d0e2e7f7cb1789f300169c7ec68eaa3f1c335f1 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:54:35 +0530 Subject: [PATCH] feat(app): add new setup wizard --- src/app/api/config/setup-complete/route.ts | 23 ++++ src/app/layout.tsx | 33 +++-- src/components/Setup/SetupConfig.tsx | 149 +++++++++++++++++++++ src/components/Setup/SetupWizard.tsx | 126 +++++++++++++++++ 4 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 src/app/api/config/setup-complete/route.ts create mode 100644 src/components/Setup/SetupConfig.tsx create mode 100644 src/components/Setup/SetupWizard.tsx diff --git a/src/app/api/config/setup-complete/route.ts b/src/app/api/config/setup-complete/route.ts new file mode 100644 index 0000000..0055fd3 --- /dev/null +++ b/src/app/api/config/setup-complete/route.ts @@ -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 }, + ); + } +}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 684a99c..830d842 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - {children} - + {setupComplete ? ( + <> + {children} + + + ) : ( + + )} diff --git a/src/components/Setup/SetupConfig.tsx b/src/components/Setup/SetupConfig.tsx new file mode 100644 index 0000000..f8c047a --- /dev/null +++ b/src/components/Setup/SetupConfig.tsx @@ -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([]); + 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 ( +
+ {setupState === 2 && ( + +
+
+
+

+ Manage Providers +

+

+ Add and configure your model providers +

+
+ +
+ +
+ {isLoading ? ( +
+

+ Loading providers... +

+
+ ) : providers.length === 0 ? ( +
+

+ No providers configured +

+
+ ) : ( + providers.map((provider) => ( + f.key === provider.type, + )?.fields ?? []) as UIConfigField[] + } + modelProvider={provider} + setProviders={setProviders} + /> + )) + )} +
+
+
+ )} + +
+ {setupState === 2 && ( + + {isFinishing ? 'Finishing...' : 'Finish'} + + + )} +
+
+ ); +}; + +export default SetupConfig; diff --git a/src/components/Setup/SetupWizard.tsx b/src/components/Setup/SetupWizard.tsx new file mode 100644 index 0000000..d919d96 --- /dev/null +++ b/src/components/Setup/SetupWizard.tsx @@ -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 ( +
+ + {showWelcome && ( +
+ + + Welcome to{' '} + + Perplexica + + + + Web search,{' '} + + reimagined + + + + +
+ )} + {showSetup && ( +
+ + {setupState === 1 && ( + + Let us get{' '} + + Perplexica + {' '} + set up for you + + )} + {setupState > 1 && ( + + + + )} + +
+ )} +
+
+ ); +}; + +export default SetupWizard;