mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
Merge pull request #515 from MODSetter/dev
refactor: enhance onboarding experience
This commit is contained in:
commit
6cc9e38e1d
11 changed files with 715 additions and 428 deletions
|
|
@ -1,21 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowLeft, ArrowRight, Bot, CheckCircle, MessageSquare, Sparkles } from "lucide-react";
|
import { FileText, MessageSquare, UserPlus, Users } from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Logo } from "@/components/Logo";
|
import { toast } from "sonner";
|
||||||
import { CompletionStep } from "@/components/onboard/completion-step";
|
import { OnboardActionCard } from "@/components/onboard/onboard-action-card";
|
||||||
import { SetupLLMStep } from "@/components/onboard/setup-llm-step";
|
import { OnboardAdvancedSettings } from "@/components/onboard/onboard-advanced-settings";
|
||||||
import { SetupPromptStep } from "@/components/onboard/setup-prompt-step";
|
import { OnboardHeader } from "@/components/onboard/onboard-header";
|
||||||
import { Button } from "@/components/ui/button";
|
import { OnboardLLMSetup } from "@/components/onboard/onboard-llm-setup";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { OnboardLoading } from "@/components/onboard/onboard-loading";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { OnboardStats } from "@/components/onboard/onboard-stats";
|
||||||
import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
|
import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
|
||||||
|
|
||||||
const TOTAL_STEPS = 3;
|
|
||||||
|
|
||||||
const OnboardPage = () => {
|
const OnboardPage = () => {
|
||||||
const t = useTranslations("onboard");
|
const t = useTranslations("onboard");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -28,10 +26,17 @@ const OnboardPage = () => {
|
||||||
preferences,
|
preferences,
|
||||||
loading: preferencesLoading,
|
loading: preferencesLoading,
|
||||||
isOnboardingComplete,
|
isOnboardingComplete,
|
||||||
|
updatePreferences,
|
||||||
refreshPreferences,
|
refreshPreferences,
|
||||||
} = useLLMPreferences(searchSpaceId);
|
} = 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
|
// Track if onboarding was complete on initial mount
|
||||||
const wasCompleteOnMount = useRef<boolean | null>(null);
|
const wasCompleteOnMount = useRef<boolean | null>(null);
|
||||||
|
|
@ -59,231 +64,215 @@ const OnboardPage = () => {
|
||||||
}
|
}
|
||||||
}, [preferencesLoading, configsLoading, globalConfigsLoading, isOnboardingComplete]);
|
}, [preferencesLoading, configsLoading, globalConfigsLoading, isOnboardingComplete]);
|
||||||
|
|
||||||
// Track if user has progressed beyond step 1
|
// Redirect to dashboard if onboarding was already complete
|
||||||
useEffect(() => {
|
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 (
|
if (
|
||||||
wasCompleteOnMount.current === true &&
|
wasCompleteOnMount.current === true &&
|
||||||
!hasUserProgressed &&
|
|
||||||
!preferencesLoading &&
|
!preferencesLoading &&
|
||||||
!configsLoading &&
|
!configsLoading &&
|
||||||
!globalConfigsLoading
|
!globalConfigsLoading
|
||||||
) {
|
) {
|
||||||
// Small delay to ensure the check is stable on initial load
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
router.push(`/dashboard/${searchSpaceId}`);
|
router.push(`/dashboard/${searchSpaceId}`);
|
||||||
}, 300);
|
}, 300);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [
|
}, [preferencesLoading, configsLoading, globalConfigsLoading, router, searchSpaceId]);
|
||||||
hasUserProgressed,
|
|
||||||
preferencesLoading,
|
|
||||||
configsLoading,
|
|
||||||
globalConfigsLoading,
|
|
||||||
router,
|
|
||||||
searchSpaceId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const progress = (currentStep / TOTAL_STEPS) * 100;
|
// Auto-configure LLM roles if global configs are available
|
||||||
|
const autoConfigureLLMs = useCallback(async () => {
|
||||||
const stepTitles = [t("setup_llm_configuration"), "Configure AI Responses", t("setup_complete")];
|
if (hasAttemptedAutoConfig.current) return;
|
||||||
|
if (globalConfigs.length === 0) return;
|
||||||
const stepDescriptions = [
|
if (isOnboardingComplete()) {
|
||||||
t("configure_providers_and_assign_roles"),
|
setAutoConfigComplete(true);
|
||||||
"Customize how the AI responds to your queries (Optional)",
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 handlePrevious = () => {
|
const success = await updatePreferences(newPreferences);
|
||||||
if (currentStep > 1) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (configsLoading || preferencesLoading || globalConfigsLoading) {
|
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]);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
<OnboardLoading
|
||||||
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
|
title={isAutoConfiguring ? "Setting up your AI assistant..." : t("loading_config")}
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
subtitle={
|
||||||
<Bot className="h-12 w-12 text-primary animate-pulse mb-4" />
|
isAutoConfiguring
|
||||||
<p className="text-sm text-muted-foreground">{t("loading_config")}</p>
|
? "Auto-configuring optimal settings for you"
|
||||||
</CardContent>
|
: "Please wait while we load your configuration"
|
||||||
</Card>
|
}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No configs available - show LLM setup
|
||||||
|
if (allConfigs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20 flex items-center justify-center p-4">
|
<OnboardLLMSetup
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="w-full max-w-4xl"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="flex items-center justify-center mb-4">
|
|
||||||
<Logo className="w-12 h-12 mr-3 rounded-full" />
|
|
||||||
<h1 className="text-3xl font-bold">{t("welcome_title")}</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground text-lg">{t("welcome_subtitle")}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress */}
|
|
||||||
<Card className="mb-8 bg-background/60 backdrop-blur-sm">
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="text-sm font-medium">
|
|
||||||
{t("step_of", { current: currentStep, total: TOTAL_STEPS })}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t("percent_complete", { percent: Math.round(progress) })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress value={progress} className="mb-4" />
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
{Array.from({ length: TOTAL_STEPS }, (_, i) => {
|
|
||||||
const stepNum = i + 1;
|
|
||||||
const isCompleted = stepNum < currentStep;
|
|
||||||
const isCurrent = stepNum === currentStep;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={stepNum} className="flex items-center space-x-2">
|
|
||||||
<div
|
|
||||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
|
||||||
isCompleted
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: isCurrent
|
|
||||||
? "bg-primary/20 text-primary border-2 border-primary"
|
|
||||||
: "bg-muted text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isCompleted ? <CheckCircle className="w-4 h-4" /> : stepNum}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p
|
|
||||||
className={`text-sm font-medium truncate ${
|
|
||||||
isCurrent ? "text-foreground" : "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{stepTitles[i]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Step Content */}
|
|
||||||
<Card className="min-h-[500px] bg-background/60 backdrop-blur-sm">
|
|
||||||
<CardHeader className="text-center">
|
|
||||||
<CardTitle className="text-2xl flex items-center justify-center gap-2">
|
|
||||||
{currentStep === 1 && <Sparkles className="w-6 h-6" />}
|
|
||||||
{currentStep === 2 && <MessageSquare className="w-6 h-6" />}
|
|
||||||
{currentStep === 3 && <CheckCircle className="w-6 h-6" />}
|
|
||||||
{stepTitles[currentStep - 1]}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
{stepDescriptions[currentStep - 1]}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
key={currentStep}
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && (
|
|
||||||
<SetupLLMStep
|
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
|
title={t("welcome_title")}
|
||||||
|
configTitle={t("setup_llm_configuration")}
|
||||||
|
configDescription={t("configure_providers_and_assign_roles")}
|
||||||
onConfigCreated={refreshConfigs}
|
onConfigCreated={refreshConfigs}
|
||||||
onConfigDeleted={refreshConfigs}
|
onConfigDeleted={refreshConfigs}
|
||||||
onPreferencesUpdated={refreshPreferences}
|
onPreferencesUpdated={refreshPreferences}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
{currentStep === 2 && (
|
}
|
||||||
<SetupPromptStep searchSpaceId={searchSpaceId} onComplete={handleNext} />
|
|
||||||
)}
|
|
||||||
{currentStep === 3 && <CompletionStep searchSpaceId={searchSpaceId} />}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
// Main onboarding view
|
||||||
<div className="flex justify-between mt-8">
|
return (
|
||||||
{currentStep === 1 ? (
|
<div className="min-h-screen bg-background">
|
||||||
<>
|
<div className="flex items-center justify-center min-h-screen p-4 md:p-8">
|
||||||
<div />
|
<motion.div
|
||||||
<Button
|
initial={{ opacity: 0 }}
|
||||||
onClick={handleNext}
|
animate={{ opacity: 1 }}
|
||||||
disabled={!canProceedToStep2}
|
transition={{ duration: 0.6 }}
|
||||||
className="flex items-center gap-2"
|
className="w-full max-w-5xl"
|
||||||
>
|
>
|
||||||
{t("next")}
|
{/* Header */}
|
||||||
<ArrowRight className="w-4 h-4" />
|
<OnboardHeader
|
||||||
</Button>
|
title={t("welcome_title")}
|
||||||
</>
|
subtitle={
|
||||||
) : currentStep === 2 ? (
|
isReady ? "You're all set! Choose what you'd like to do next." : t("welcome_subtitle")
|
||||||
<>
|
}
|
||||||
<Button
|
isReady={isReady}
|
||||||
variant="outline"
|
/>
|
||||||
onClick={handlePrevious}
|
|
||||||
className="flex items-center gap-2"
|
{/* Quick Stats */}
|
||||||
|
<OnboardStats
|
||||||
|
globalConfigsCount={globalConfigs.length}
|
||||||
|
userConfigsCount={llmConfigs.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Action Cards */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<OnboardActionCard
|
||||||
{t("previous")}
|
title="Manage Team"
|
||||||
</Button>
|
description="Invite team members and collaborate on your search space"
|
||||||
{/* Next button is handled by SetupPromptStep component */}
|
icon={Users}
|
||||||
<div />
|
features={[
|
||||||
</>
|
"Invite team members",
|
||||||
) : (
|
"Assign roles & permissions",
|
||||||
<>
|
"Collaborate together",
|
||||||
<Button
|
]}
|
||||||
variant="outline"
|
buttonText="Manage Team"
|
||||||
onClick={handlePrevious}
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/team`)}
|
||||||
className="flex items-center gap-2"
|
colorScheme="emerald"
|
||||||
>
|
delay={0.7}
|
||||||
<ArrowLeft className="w-4 h-4" />
|
/>
|
||||||
{t("previous")}
|
|
||||||
</Button>
|
<OnboardActionCard
|
||||||
<div />
|
title="Add Sources"
|
||||||
</>
|
description="Connect your data sources to start building your knowledge base"
|
||||||
)}
|
icon={FileText}
|
||||||
</div>
|
features={[
|
||||||
|
"Connect documents and files",
|
||||||
|
"Import from various sources",
|
||||||
|
"Build your knowledge base",
|
||||||
|
]}
|
||||||
|
buttonText="Add Sources"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/sources/add`)}
|
||||||
|
colorScheme="blue"
|
||||||
|
delay={0.8}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OnboardActionCard
|
||||||
|
title="Start Chatting"
|
||||||
|
description="Jump right into the AI researcher and start asking questions"
|
||||||
|
icon={MessageSquare}
|
||||||
|
features={[
|
||||||
|
"AI-powered conversations",
|
||||||
|
"Research and explore topics",
|
||||||
|
"Get instant insights",
|
||||||
|
]}
|
||||||
|
buttonText="Start Chatting"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)}
|
||||||
|
colorScheme="violet"
|
||||||
|
delay={0.9}
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Advanced Settings */}
|
||||||
|
<OnboardAdvancedSettings
|
||||||
|
searchSpaceId={searchSpaceId}
|
||||||
|
showLLMSettings={showAdvancedSettings}
|
||||||
|
setShowLLMSettings={setShowAdvancedSettings}
|
||||||
|
showPromptSettings={showPromptSettings}
|
||||||
|
setShowPromptSettings={setShowPromptSettings}
|
||||||
|
onConfigCreated={refreshConfigs}
|
||||||
|
onConfigDeleted={refreshConfigs}
|
||||||
|
onPreferencesUpdated={refreshPreferences}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.1 }}
|
||||||
|
className="text-center mt-10 text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
You can always adjust these settings later in{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings`)}
|
||||||
|
className="text-primary hover:underline underline-offset-2 transition-colors"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { toast } from "sonner";
|
||||||
import { SearchSpaceForm } from "@/components/search-space-form";
|
import { SearchSpaceForm } from "@/components/search-space-form";
|
||||||
export default function SearchSpacesPage() {
|
export default function SearchSpacesPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const handleCreateSearchSpace = async (data: { name: string; description: string }) => {
|
const handleCreateSearchSpace = async (data: { name: string; description?: string }) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`,
|
||||||
|
|
@ -16,7 +16,10 @@ export default function SearchSpacesPage() {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
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.`,
|
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;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Next Steps - What would you like to do? */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.6 }}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-xl font-semibold mb-2">What would you like to do next?</h3>
|
|
||||||
<p className="text-muted-foreground">Choose an option to continue</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
{/* Manage Team Card */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.9, type: "spring", stiffness: 300, damping: 25 }}
|
|
||||||
>
|
|
||||||
<Card className="h-full border-2 hover:border-emerald-500/50 transition-all duration-300 hover:shadow-xl hover:shadow-emerald-500/10 cursor-pointer group relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-emerald-500/10 to-transparent rounded-full blur-2xl -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-500" />
|
|
||||||
<CardHeader className="relative">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 ring-1 ring-emerald-500/20">
|
|
||||||
<Users className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-lg">Manage Team</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Invite team members and collaborate on your search space
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4 relative">
|
|
||||||
<div className="space-y-2 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserPlus className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Invite team members</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Assign roles & permissions</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Collaborate together</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white group-hover:shadow-lg group-hover:shadow-emerald-500/25 transition-all duration-300"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/team`)}
|
|
||||||
>
|
|
||||||
Manage Team
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Add Sources Card */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.7, type: "spring", stiffness: 300, damping: 25 }}
|
|
||||||
>
|
|
||||||
<Card className="h-full border-2 hover:border-blue-500/50 transition-all duration-300 hover:shadow-xl hover:shadow-blue-500/10 cursor-pointer group relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-blue-500/10 to-transparent rounded-full blur-2xl -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-500" />
|
|
||||||
<CardHeader className="relative">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500/20 to-blue-600/10 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 ring-1 ring-blue-500/20">
|
|
||||||
<FileText className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-lg">Add Sources</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect your data sources to start building your knowledge base
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4 relative">
|
|
||||||
<div className="space-y-2 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Connect documents and files</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Import from various sources</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Build your knowledge base</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white group-hover:shadow-lg group-hover:shadow-blue-500/25 transition-all duration-300"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/sources/add`)}
|
|
||||||
>
|
|
||||||
Add Sources
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Start Chatting Card */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.8, type: "spring", stiffness: 300, damping: 25 }}
|
|
||||||
>
|
|
||||||
<Card className="h-full border-2 hover:border-purple-500/50 transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10 cursor-pointer group relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-purple-500/10 to-transparent rounded-full blur-2xl -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-500" />
|
|
||||||
<CardHeader className="relative">
|
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500/20 to-purple-600/10 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 ring-1 ring-purple-500/20">
|
|
||||||
<MessageSquare className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-lg">Start Chatting</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Jump right into the AI researcher and start asking questions
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4 relative">
|
|
||||||
<div className="space-y-2 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>AI-powered conversations</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Research and explore topics</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span>Get instant insights</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white group-hover:shadow-lg group-hover:shadow-purple-500/25 transition-all duration-300"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)}
|
|
||||||
>
|
|
||||||
Start Chatting
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Stats */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.9 }}
|
|
||||||
className="flex flex-wrap justify-center gap-2 pt-4"
|
|
||||||
>
|
|
||||||
<Badge variant="secondary">
|
|
||||||
✓ {allConfigs.length} LLM provider{allConfigs.length > 1 ? "s" : ""} available
|
|
||||||
</Badge>
|
|
||||||
{globalConfigs.length > 0 && (
|
|
||||||
<Badge variant="secondary">✓ {globalConfigs.length} Global config(s)</Badge>
|
|
||||||
)}
|
|
||||||
{llmConfigs.length > 0 && (
|
|
||||||
<Badge variant="secondary">✓ {llmConfigs.length} Custom config(s)</Badge>
|
|
||||||
)}
|
|
||||||
<Badge variant="secondary">✓ All roles assigned</Badge>
|
|
||||||
<Badge variant="secondary">✓ Ready to use</Badge>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
8
surfsense_web/components/onboard/index.ts
Normal file
8
surfsense_web/components/onboard/index.ts
Normal file
|
|
@ -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";
|
||||||
114
surfsense_web/components/onboard/onboard-action-card.tsx
Normal file
114
surfsense_web/components/onboard/onboard-action-card.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay, type: "spring", stiffness: 200 }}
|
||||||
|
whileHover={{ y: -6, transition: { duration: 0.2 } }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"h-full cursor-pointer group relative overflow-hidden transition-all duration-300",
|
||||||
|
"border bg-card hover:shadow-lg",
|
||||||
|
colors.hoverBorder
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<CardHeader className="relative pb-4">
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"w-14 h-14 rounded-2xl flex items-center justify-center mb-4 ring-1 transition-all duration-300",
|
||||||
|
colors.iconBg,
|
||||||
|
colors.iconRing,
|
||||||
|
"group-hover:scale-110"
|
||||||
|
)}
|
||||||
|
whileHover={{ rotate: [0, -5, 5, 0] }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Icon className={cn("w-7 h-7", colors.iconColor)} />
|
||||||
|
</motion.div>
|
||||||
|
<CardTitle className="text-xl">{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="relative space-y-4">
|
||||||
|
<div className="space-y-2.5 text-sm text-muted-foreground">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2.5">
|
||||||
|
<CheckCircle className={cn("w-4 h-4", colors.checkColor)} />
|
||||||
|
<span>{feature}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"w-full text-white border-0 transition-all duration-300",
|
||||||
|
colors.buttonBg
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
surfsense_web/components/onboard/onboard-advanced-settings.tsx
Normal file
144
surfsense_web/components/onboard/onboard-advanced-settings.tsx
Normal file
|
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OnboardAdvancedSettings({
|
||||||
|
searchSpaceId,
|
||||||
|
showLLMSettings,
|
||||||
|
setShowLLMSettings,
|
||||||
|
showPromptSettings,
|
||||||
|
setShowPromptSettings,
|
||||||
|
onConfigCreated,
|
||||||
|
onConfigDeleted,
|
||||||
|
onPreferencesUpdated,
|
||||||
|
}: OnboardAdvancedSettingsProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 1 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{/* LLM Configuration */}
|
||||||
|
<Collapsible open={showLLMSettings} onOpenChange={setShowLLMSettings}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
|
||||||
|
<CardContent className="py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl bg-fuchsia-500/10 dark:bg-fuchsia-500/20 border border-fuchsia-500/20">
|
||||||
|
<Settings2 className="w-5 h-5 text-fuchsia-600 dark:text-fuchsia-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">LLM Configuration</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Customize AI models and role assignments
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: showLLMSettings ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showLLMSettings && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Card className="mt-2">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<SetupLLMStep
|
||||||
|
searchSpaceId={searchSpaceId}
|
||||||
|
onConfigCreated={onConfigCreated}
|
||||||
|
onConfigDeleted={onConfigDeleted}
|
||||||
|
onPreferencesUpdated={onPreferencesUpdated}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
{/* Prompt Configuration */}
|
||||||
|
<Collapsible open={showPromptSettings} onOpenChange={setShowPromptSettings}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
|
||||||
|
<CardContent className="py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20 border border-cyan-500/20">
|
||||||
|
<MessageSquare className="w-5 h-5 text-cyan-600 dark:text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">AI Response Settings</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Configure citations and custom instructions (Optional)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: showPromptSettings ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showPromptSettings && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Card className="mt-2">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<SetupPromptStep
|
||||||
|
searchSpaceId={searchSpaceId}
|
||||||
|
onComplete={() => setShowPromptSettings(false)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
surfsense_web/components/onboard/onboard-header.tsx
Normal file
56
surfsense_web/components/onboard/onboard-header.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
className="text-center mb-10"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, delay: 0.2 }}
|
||||||
|
className="inline-flex items-center justify-center mb-6"
|
||||||
|
>
|
||||||
|
<Logo className="w-20 h-20 rounded-2xl shadow-lg" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="space-y-2"
|
||||||
|
>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-foreground">{title}</h1>
|
||||||
|
<p className="text-muted-foreground text-lg md:text-xl max-w-2xl mx-auto">{subtitle}</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{isReady && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.4, type: "spring" }}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Badge className="px-4 py-2 text-sm bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400">
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
AI Configuration Complete
|
||||||
|
</Badge>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
surfsense_web/components/onboard/onboard-llm-setup.tsx
Normal file
93
surfsense_web/components/onboard/onboard-llm-setup.tsx
Normal file
|
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OnboardLLMSetup({
|
||||||
|
searchSpaceId,
|
||||||
|
title,
|
||||||
|
configTitle,
|
||||||
|
configDescription,
|
||||||
|
onConfigCreated,
|
||||||
|
onConfigDeleted,
|
||||||
|
onPreferencesUpdated,
|
||||||
|
}: OnboardLLMSetupProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, delay: 0.1 }}
|
||||||
|
className="inline-flex items-center justify-center mb-6"
|
||||||
|
>
|
||||||
|
<Logo className="w-16 h-16 rounded-2xl shadow-lg" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="text-4xl font-bold text-foreground mb-3"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="text-muted-foreground text-lg"
|
||||||
|
>
|
||||||
|
Configure your AI model to get started
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LLM Setup Card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="text-center border-b pb-6">
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-2">
|
||||||
|
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20">
|
||||||
|
<Bot className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl">{configTitle}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>{configDescription}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<SetupLLMStep
|
||||||
|
searchSpaceId={searchSpaceId}
|
||||||
|
onConfigCreated={onConfigCreated}
|
||||||
|
onConfigDeleted={onConfigDeleted}
|
||||||
|
onPreferencesUpdated={onPreferencesUpdated}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
surfsense_web/components/onboard/onboard-loading.tsx
Normal file
47
surfsense_web/components/onboard/onboard-loading.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<div className="relative mb-8 flex justify-center">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-16 h-16 text-primary" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
|
||||||
|
<p className="text-muted-foreground">{subtitle}</p>
|
||||||
|
<div className="mt-6 flex justify-center gap-1.5">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="w-2 h-2 rounded-full bg-primary"
|
||||||
|
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
|
||||||
|
transition={{
|
||||||
|
duration: 1,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: i * 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
surfsense_web/components/onboard/onboard-stats.tsx
Normal file
38
surfsense_web/components/onboard/onboard-stats.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="flex flex-wrap justify-center gap-3 mb-10"
|
||||||
|
>
|
||||||
|
{globalConfigsCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="px-3 py-1.5">
|
||||||
|
<Sparkles className="w-3 h-3 mr-1.5 text-violet-500" />
|
||||||
|
{globalConfigsCount} Global Model{globalConfigsCount > 1 ? "s" : ""}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{userConfigsCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="px-3 py-1.5">
|
||||||
|
<Bot className="w-3 h-3 mr-1.5 text-blue-500" />
|
||||||
|
{userConfigsCount} Custom Config{userConfigsCount > 1 ? "s" : ""}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Badge variant="secondary" className="px-3 py-1.5">
|
||||||
|
<Brain className="w-3 h-3 mr-1.5 text-fuchsia-500" />
|
||||||
|
All Roles Assigned
|
||||||
|
</Badge>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -36,19 +36,19 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// Define the form schema with Zod
|
// Define the form schema with Zod
|
||||||
const searchSpaceFormSchema = z.object({
|
const searchSpaceFormSchema = z.object({
|
||||||
name: z.string().min(3, "Name is required"),
|
name: z.string().min(3, "Name must be at least 3 characters"),
|
||||||
description: z.string().min(10, "Description is required"),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the type for the form values
|
// Define the type for the form values
|
||||||
type SearchSpaceFormValues = z.infer<typeof searchSpaceFormSchema>;
|
type SearchSpaceFormValues = z.infer<typeof searchSpaceFormSchema>;
|
||||||
|
|
||||||
interface SearchSpaceFormProps {
|
interface SearchSpaceFormProps {
|
||||||
onSubmit?: (data: { name: string; description: string }) => void;
|
onSubmit?: (data: { name: string; description?: string }) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
initialData?: { name: string; description: string };
|
initialData?: { name: string; description?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSpaceForm({
|
export function SearchSpaceForm({
|
||||||
|
|
@ -229,7 +229,9 @@ export function SearchSpaceForm({
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>
|
||||||
|
Description <span className="text-muted-foreground font-normal">(optional)</span>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Enter search space description" {...field} />
|
<Input placeholder="Enter search space description" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue