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;