diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 1588743e8..150cd772c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -1,21 +1,19 @@ "use client"; -import { ArrowLeft, ArrowRight, Bot, CheckCircle, MessageSquare, Sparkles } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; +import { FileText, MessageSquare, UserPlus, Users } from "lucide-react"; +import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useEffect, useRef, useState } from "react"; -import { Logo } from "@/components/Logo"; -import { CompletionStep } from "@/components/onboard/completion-step"; -import { SetupLLMStep } from "@/components/onboard/setup-llm-step"; -import { SetupPromptStep } from "@/components/onboard/setup-prompt-step"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { OnboardActionCard } from "@/components/onboard/onboard-action-card"; +import { OnboardAdvancedSettings } from "@/components/onboard/onboard-advanced-settings"; +import { OnboardHeader } from "@/components/onboard/onboard-header"; +import { OnboardLLMSetup } from "@/components/onboard/onboard-llm-setup"; +import { OnboardLoading } from "@/components/onboard/onboard-loading"; +import { OnboardStats } from "@/components/onboard/onboard-stats"; import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; -const TOTAL_STEPS = 3; - const OnboardPage = () => { const t = useTranslations("onboard"); const router = useRouter(); @@ -28,10 +26,17 @@ const OnboardPage = () => { preferences, loading: preferencesLoading, isOnboardingComplete, + updatePreferences, refreshPreferences, } = useLLMPreferences(searchSpaceId); - const [currentStep, setCurrentStep] = useState(1); - const [hasUserProgressed, setHasUserProgressed] = useState(false); + + const [isAutoConfiguring, setIsAutoConfiguring] = useState(false); + const [autoConfigComplete, setAutoConfigComplete] = useState(false); + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const [showPromptSettings, setShowPromptSettings] = useState(false); + + // Track if we've already attempted auto-configuration + const hasAttemptedAutoConfig = useRef(false); // Track if onboarding was complete on initial mount const wasCompleteOnMount = useRef(null); @@ -59,231 +64,215 @@ const OnboardPage = () => { } }, [preferencesLoading, configsLoading, globalConfigsLoading, isOnboardingComplete]); - // Track if user has progressed beyond step 1 + // Redirect to dashboard if onboarding was already complete useEffect(() => { - if (currentStep > 1) { - setHasUserProgressed(true); - } - }, [currentStep]); - - // Redirect to dashboard if onboarding was already complete on mount (not during this session) - useEffect(() => { - // Only redirect if: - // 1. Onboarding was complete when page loaded - // 2. User hasn't progressed past step 1 - // 3. All data is loaded if ( wasCompleteOnMount.current === true && - !hasUserProgressed && !preferencesLoading && !configsLoading && !globalConfigsLoading ) { - // Small delay to ensure the check is stable on initial load const timer = setTimeout(() => { router.push(`/dashboard/${searchSpaceId}`); }, 300); return () => clearTimeout(timer); } - }, [ - hasUserProgressed, - preferencesLoading, - configsLoading, - globalConfigsLoading, - router, - searchSpaceId, - ]); + }, [preferencesLoading, configsLoading, globalConfigsLoading, router, searchSpaceId]); - const progress = (currentStep / TOTAL_STEPS) * 100; - - const stepTitles = [t("setup_llm_configuration"), "Configure AI Responses", t("setup_complete")]; - - const stepDescriptions = [ - t("configure_providers_and_assign_roles"), - "Customize how the AI responds to your queries (Optional)", - t("all_set"), - ]; - - // User can proceed to step 2 if all roles are assigned - const canProceedToStep2 = - !preferencesLoading && - preferences.long_context_llm_id && - preferences.fast_llm_id && - preferences.strategic_llm_id; - - // User can always proceed from step 2 to step 3 (prompt config is optional) - const canProceedToStep3 = true; - - const handleNext = () => { - if (currentStep < TOTAL_STEPS) { - setCurrentStep(currentStep + 1); + // Auto-configure LLM roles if global configs are available + const autoConfigureLLMs = useCallback(async () => { + if (hasAttemptedAutoConfig.current) return; + if (globalConfigs.length === 0) return; + if (isOnboardingComplete()) { + setAutoConfigComplete(true); + return; } - }; - const handlePrevious = () => { - if (currentStep > 1) { - setCurrentStep(currentStep - 1); + hasAttemptedAutoConfig.current = true; + setIsAutoConfiguring(true); + + try { + const allConfigs = [...globalConfigs, ...llmConfigs]; + + if (allConfigs.length === 0) { + setIsAutoConfiguring(false); + return; + } + + // Use first available config for all roles + const defaultConfigId = allConfigs[0].id; + + const newPreferences = { + long_context_llm_id: defaultConfigId, + fast_llm_id: defaultConfigId, + strategic_llm_id: defaultConfigId, + }; + + const success = await updatePreferences(newPreferences); + + if (success) { + await refreshPreferences(); + setAutoConfigComplete(true); + toast.success("AI models configured automatically!", { + description: "You can customize these in advanced settings.", + }); + } + } catch (error) { + console.error("Auto-configuration failed:", error); + } finally { + setIsAutoConfiguring(false); } - }; + }, [globalConfigs, llmConfigs, isOnboardingComplete, updatePreferences, refreshPreferences]); - if (configsLoading || preferencesLoading || globalConfigsLoading) { + // Trigger auto-configuration once data is loaded + useEffect(() => { + if (!configsLoading && !globalConfigsLoading && !preferencesLoading) { + autoConfigureLLMs(); + } + }, [configsLoading, globalConfigsLoading, preferencesLoading, autoConfigureLLMs]); + + const allConfigs = [...globalConfigs, ...llmConfigs]; + const isReady = autoConfigComplete || isOnboardingComplete(); + + // Loading state + if (configsLoading || preferencesLoading || globalConfigsLoading || isAutoConfiguring) { return ( -
- - - -

{t("loading_config")}

-
-
-
+ ); } + // No configs available - show LLM setup + if (allConfigs.length === 0) { + return ( + + ); + } + + // Main onboarding view return ( -
- - {/* Header */} -
-
- -

{t("welcome_title")}

-
-

{t("welcome_subtitle")}

-
+
+
+ + {/* Header */} + - {/* Progress */} - - -
-
- {t("step_of", { current: currentStep, total: TOTAL_STEPS })} -
-
- {t("percent_complete", { percent: Math.round(progress) })} -
-
- -
- {Array.from({ length: TOTAL_STEPS }, (_, i) => { - const stepNum = i + 1; - const isCompleted = stepNum < currentStep; - const isCurrent = stepNum === currentStep; + {/* Quick Stats */} + - return ( -
-
- {isCompleted ? : stepNum} -
-
-

- {stepTitles[i]} -

-
-
- ); - })} -
-
-
+ {/* Action Cards */} + + router.push(`/dashboard/${searchSpaceId}/team`)} + colorScheme="emerald" + delay={0.7} + /> - {/* Step Content */} - - - - {currentStep === 1 && } - {currentStep === 2 && } - {currentStep === 3 && } - {stepTitles[currentStep - 1]} - - - {stepDescriptions[currentStep - 1]} - - - - - - {currentStep === 1 && ( - - )} - {currentStep === 2 && ( - - )} - {currentStep === 3 && } - - - - + router.push(`/dashboard/${searchSpaceId}/sources/add`)} + colorScheme="blue" + delay={0.8} + /> - {/* Navigation */} -
- {currentStep === 1 ? ( - <> -
- - - ) : currentStep === 2 ? ( - <> - - {/* Next button is handled by SetupPromptStep component */} -
- - ) : ( - <> - -
- - )} -
- + Settings + +

+ + +
); }; diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx index 598536c1b..5d18195af 100644 --- a/surfsense_web/app/dashboard/searchspaces/page.tsx +++ b/surfsense_web/app/dashboard/searchspaces/page.tsx @@ -6,7 +6,7 @@ import { toast } from "sonner"; import { SearchSpaceForm } from "@/components/search-space-form"; export default function SearchSpacesPage() { const router = useRouter(); - const handleCreateSearchSpace = async (data: { name: string; description: string }) => { + const handleCreateSearchSpace = async (data: { name: string; description?: string }) => { try { const response = await fetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, @@ -16,7 +16,10 @@ export default function SearchSpacesPage() { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, }, - body: JSON.stringify(data), + body: JSON.stringify({ + name: data.name, + description: data.description || "", + }), } ); @@ -31,7 +34,8 @@ export default function SearchSpacesPage() { description: `"${data.name}" has been created.`, }); - router.push(`/dashboard`); + // Redirect to the newly created search space's onboarding + router.push(`/dashboard/${result.id}/onboard`); return result; } catch (error) { diff --git a/surfsense_web/components/onboard/completion-step.tsx b/surfsense_web/components/onboard/completion-step.tsx deleted file mode 100644 index a8dbbd76c..000000000 --- a/surfsense_web/components/onboard/completion-step.tsx +++ /dev/null @@ -1,208 +0,0 @@ -"use client"; - -import { - ArrowRight, - Bot, - Brain, - CheckCircle, - FileText, - MessageSquare, - Sparkles, - UserPlus, - Users, - Zap, -} from "lucide-react"; -import { motion } from "motion/react"; -import { useRouter } from "next/navigation"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; - -interface CompletionStepProps { - searchSpaceId: number; -} - -export function CompletionStep({ searchSpaceId }: CompletionStepProps) { - const router = useRouter(); - const { llmConfigs } = useLLMConfigs(searchSpaceId); - const { globalConfigs } = useGlobalLLMConfigs(); - const { preferences } = useLLMPreferences(searchSpaceId); - - // Combine global and user-specific configs - const allConfigs = [...globalConfigs, ...llmConfigs]; - - const assignedConfigs = { - long_context: allConfigs.find((c) => c.id === preferences.long_context_llm_id), - fast: allConfigs.find((c) => c.id === preferences.fast_llm_id), - strategic: allConfigs.find((c) => c.id === preferences.strategic_llm_id), - }; - - return ( -
- {/* Next Steps - What would you like to do? */} - -
-

What would you like to do next?

-

Choose an option to continue

-
- -
- {/* Manage Team Card */} - - -
- -
- -
- Manage Team - - Invite team members and collaborate on your search space - -
- -
-
- - Invite team members -
-
- - Assign roles & permissions -
-
- - Collaborate together -
-
- -
- - - - {/* Add Sources Card */} - - -
- -
- -
- Add Sources - - Connect your data sources to start building your knowledge base - -
- -
-
- - Connect documents and files -
-
- - Import from various sources -
-
- - Build your knowledge base -
-
- -
- - - - {/* Start Chatting Card */} - - -
- -
- -
- Start Chatting - - Jump right into the AI researcher and start asking questions - -
- -
-
- - AI-powered conversations -
-
- - Research and explore topics -
-
- - Get instant insights -
-
- -
- - -
- - {/* Quick Stats */} - - - ✓ {allConfigs.length} LLM provider{allConfigs.length > 1 ? "s" : ""} available - - {globalConfigs.length > 0 && ( - ✓ {globalConfigs.length} Global config(s) - )} - {llmConfigs.length > 0 && ( - ✓ {llmConfigs.length} Custom config(s) - )} - ✓ All roles assigned - ✓ Ready to use - -
-
- ); -} diff --git a/surfsense_web/components/onboard/index.ts b/surfsense_web/components/onboard/index.ts new file mode 100644 index 000000000..607ba4e7d --- /dev/null +++ b/surfsense_web/components/onboard/index.ts @@ -0,0 +1,8 @@ +export { OnboardActionCard } from "./onboard-action-card"; +export { OnboardAdvancedSettings } from "./onboard-advanced-settings"; +export { OnboardHeader } from "./onboard-header"; +export { OnboardLLMSetup } from "./onboard-llm-setup"; +export { OnboardLoading } from "./onboard-loading"; +export { OnboardStats } from "./onboard-stats"; +export { SetupLLMStep } from "./setup-llm-step"; +export { SetupPromptStep } from "./setup-prompt-step"; diff --git a/surfsense_web/components/onboard/onboard-action-card.tsx b/surfsense_web/components/onboard/onboard-action-card.tsx new file mode 100644 index 000000000..c6bb41dbf --- /dev/null +++ b/surfsense_web/components/onboard/onboard-action-card.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { ArrowRight, CheckCircle, type LucideIcon } from "lucide-react"; +import { motion } from "motion/react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +interface OnboardActionCardProps { + title: string; + description: string; + icon: LucideIcon; + features: string[]; + buttonText: string; + onClick: () => void; + colorScheme: "emerald" | "blue" | "violet"; + delay?: number; +} + +const colorSchemes = { + emerald: { + iconBg: "bg-emerald-500/10 dark:bg-emerald-500/20", + iconRing: "ring-emerald-500/20 dark:ring-emerald-500/30", + iconColor: "text-emerald-600 dark:text-emerald-400", + checkColor: "text-emerald-500", + buttonBg: "bg-emerald-600 hover:bg-emerald-500", + hoverBorder: "hover:border-emerald-500/50", + }, + blue: { + iconBg: "bg-blue-500/10 dark:bg-blue-500/20", + iconRing: "ring-blue-500/20 dark:ring-blue-500/30", + iconColor: "text-blue-600 dark:text-blue-400", + checkColor: "text-blue-500", + buttonBg: "bg-blue-600 hover:bg-blue-500", + hoverBorder: "hover:border-blue-500/50", + }, + violet: { + iconBg: "bg-violet-500/10 dark:bg-violet-500/20", + iconRing: "ring-violet-500/20 dark:ring-violet-500/30", + iconColor: "text-violet-600 dark:text-violet-400", + checkColor: "text-violet-500", + buttonBg: "bg-violet-600 hover:bg-violet-500", + hoverBorder: "hover:border-violet-500/50", + }, +}; + +export function OnboardActionCard({ + title, + description, + icon: Icon, + features, + buttonText, + onClick, + colorScheme, + delay = 0, +}: OnboardActionCardProps) { + const colors = colorSchemes[colorScheme]; + + return ( + + + + + + + {title} + {description} + + + +
+ {features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ + +
+
+
+ ); +} diff --git a/surfsense_web/components/onboard/onboard-advanced-settings.tsx b/surfsense_web/components/onboard/onboard-advanced-settings.tsx new file mode 100644 index 000000000..b2b9c5080 --- /dev/null +++ b/surfsense_web/components/onboard/onboard-advanced-settings.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { ChevronDown, MessageSquare, Settings2 } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { SetupLLMStep } from "@/components/onboard/setup-llm-step"; +import { SetupPromptStep } from "@/components/onboard/setup-prompt-step"; +import { Card, CardContent } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; + +interface OnboardAdvancedSettingsProps { + searchSpaceId: number; + showLLMSettings: boolean; + setShowLLMSettings: (show: boolean) => void; + showPromptSettings: boolean; + setShowPromptSettings: (show: boolean) => void; + onConfigCreated: () => void; + onConfigDeleted: () => void; + onPreferencesUpdated: () => Promise; +} + +export function OnboardAdvancedSettings({ + searchSpaceId, + showLLMSettings, + setShowLLMSettings, + showPromptSettings, + setShowPromptSettings, + onConfigCreated, + onConfigDeleted, + onPreferencesUpdated, +}: OnboardAdvancedSettingsProps) { + return ( + + {/* LLM Configuration */} + + + + +
+
+
+ +
+
+

LLM Configuration

+

+ Customize AI models and role assignments +

+
+
+ + + +
+
+
+
+ + + + {showLLMSettings && ( + + + + + + + + )} + + +
+ + {/* Prompt Configuration */} + + + + +
+
+
+ +
+
+

AI Response Settings

+

+ Configure citations and custom instructions (Optional) +

+
+
+ + + +
+
+
+
+ + + + {showPromptSettings && ( + + + + setShowPromptSettings(false)} + /> + + + + )} + + +
+
+ ); +} diff --git a/surfsense_web/components/onboard/onboard-header.tsx b/surfsense_web/components/onboard/onboard-header.tsx new file mode 100644 index 000000000..d84bb5adc --- /dev/null +++ b/surfsense_web/components/onboard/onboard-header.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { CheckCircle } from "lucide-react"; +import { motion } from "motion/react"; +import { Logo } from "@/components/Logo"; +import { Badge } from "@/components/ui/badge"; + +interface OnboardHeaderProps { + title: string; + subtitle: string; + isReady?: boolean; +} + +export function OnboardHeader({ title, subtitle, isReady }: OnboardHeaderProps) { + return ( + + + + + + +

{title}

+

{subtitle}

+
+ + {isReady && ( + + + + AI Configuration Complete + + + )} +
+ ); +} diff --git a/surfsense_web/components/onboard/onboard-llm-setup.tsx b/surfsense_web/components/onboard/onboard-llm-setup.tsx new file mode 100644 index 000000000..b0b2d3fac --- /dev/null +++ b/surfsense_web/components/onboard/onboard-llm-setup.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { Bot } from "lucide-react"; +import { motion } from "motion/react"; +import { Logo } from "@/components/Logo"; +import { SetupLLMStep } from "@/components/onboard/setup-llm-step"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +interface OnboardLLMSetupProps { + searchSpaceId: number; + title: string; + configTitle: string; + configDescription: string; + onConfigCreated: () => void; + onConfigDeleted: () => void; + onPreferencesUpdated: () => Promise; +} + +export function OnboardLLMSetup({ + searchSpaceId, + title, + configTitle, + configDescription, + onConfigCreated, + onConfigDeleted, + onPreferencesUpdated, +}: OnboardLLMSetupProps) { + return ( +
+ + {/* Header */} +
+ + + + + {title} + + + Configure your AI model to get started + +
+ + {/* LLM Setup Card */} + + + +
+
+ +
+ {configTitle} +
+ {configDescription} +
+ + + +
+
+
+
+ ); +} diff --git a/surfsense_web/components/onboard/onboard-loading.tsx b/surfsense_web/components/onboard/onboard-loading.tsx new file mode 100644 index 000000000..4a85736d2 --- /dev/null +++ b/surfsense_web/components/onboard/onboard-loading.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Wand2 } from "lucide-react"; +import { motion } from "motion/react"; + +interface OnboardLoadingProps { + title: string; + subtitle: string; +} + +export function OnboardLoading({ title, subtitle }: OnboardLoadingProps) { + return ( +
+ +
+ + + +
+

{title}

+

{subtitle}

+
+ {[0, 1, 2].map((i) => ( + + ))} +
+
+
+ ); +} diff --git a/surfsense_web/components/onboard/onboard-stats.tsx b/surfsense_web/components/onboard/onboard-stats.tsx new file mode 100644 index 000000000..0918c74e2 --- /dev/null +++ b/surfsense_web/components/onboard/onboard-stats.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Bot, Brain, Sparkles } from "lucide-react"; +import { motion } from "motion/react"; +import { Badge } from "@/components/ui/badge"; + +interface OnboardStatsProps { + globalConfigsCount: number; + userConfigsCount: number; +} + +export function OnboardStats({ globalConfigsCount, userConfigsCount }: OnboardStatsProps) { + return ( + + {globalConfigsCount > 0 && ( + + + {globalConfigsCount} Global Model{globalConfigsCount > 1 ? "s" : ""} + + )} + {userConfigsCount > 0 && ( + + + {userConfigsCount} Custom Config{userConfigsCount > 1 ? "s" : ""} + + )} + + + All Roles Assigned + + + ); +} diff --git a/surfsense_web/components/search-space-form.tsx b/surfsense_web/components/search-space-form.tsx index 79102dbcf..ccb290dc8 100644 --- a/surfsense_web/components/search-space-form.tsx +++ b/surfsense_web/components/search-space-form.tsx @@ -36,19 +36,19 @@ import { cn } from "@/lib/utils"; // Define the form schema with Zod const searchSpaceFormSchema = z.object({ - name: z.string().min(3, "Name is required"), - description: z.string().min(10, "Description is required"), + name: z.string().min(3, "Name must be at least 3 characters"), + description: z.string().optional(), }); // Define the type for the form values type SearchSpaceFormValues = z.infer; interface SearchSpaceFormProps { - onSubmit?: (data: { name: string; description: string }) => void; + onSubmit?: (data: { name: string; description?: string }) => void; onDelete?: () => void; className?: string; isEditing?: boolean; - initialData?: { name: string; description: string }; + initialData?: { name: string; description?: string }; } export function SearchSpaceForm({ @@ -229,7 +229,9 @@ export function SearchSpaceForm({ name="description" render={({ field }) => ( - Description + + Description (optional) +