diff --git a/frontend/components/logo/dog-icon.tsx b/frontend/components/logo/dog-icon.tsx index d9b5774d..ce0b4104 100644 --- a/frontend/components/logo/dog-icon.tsx +++ b/frontend/components/logo/dog-icon.tsx @@ -1,6 +1,12 @@ -const DogIcon = (props: React.SVGProps) => { +interface DogIconProps extends React.SVGProps { + disabled?: boolean; +} + +const DogIcon = ({ disabled = false, stroke, ...props }: DogIconProps) => { + const strokeColor = disabled ? "#71717A" : (stroke || "#0F62FE"); + return ( - ) => { > -
- -
-
- - - {isStreaming && ( - - )} -
- {showForkButton && onFork && ( -
+ + +
+ } + actions={ + showForkButton && onFork ? ( - + ) : undefined + } + > + + + {isStreaming && ( + )} - + ); } diff --git a/frontend/src/app/chat/components/message.tsx b/frontend/src/app/chat/components/message.tsx new file mode 100644 index 00000000..9e0022c2 --- /dev/null +++ b/frontend/src/app/chat/components/message.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react"; + +interface MessageProps { + icon: ReactNode; + children: ReactNode; + actions?: ReactNode; +} + +export function Message({ icon, children, actions }: MessageProps) { + return ( +
+ {icon} +
{children}
+ {actions &&
{actions}
} +
+ ); +} diff --git a/frontend/src/app/chat/components/user-message.tsx b/frontend/src/app/chat/components/user-message.tsx index 5c01b7e8..882b3416 100644 --- a/frontend/src/app/chat/components/user-message.tsx +++ b/frontend/src/app/chat/components/user-message.tsx @@ -1,6 +1,7 @@ import { User } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useAuth } from "@/contexts/auth-context"; +import { Message } from "./message"; interface UserMessageProps { content: string; @@ -10,22 +11,23 @@ export function UserMessage({ content }: UserMessageProps) { const { user } = useAuth(); return ( -
- - - - {user?.name ? ( - user.name.charAt(0).toUpperCase() - ) : ( - - )} - - -
-

- {content} -

-
-
+ + + + {user?.name ? ( + user.name.charAt(0).toUpperCase() + ) : ( + + )} + + + } + > +

+ {content} +

+
); } diff --git a/frontend/src/app/new-onboarding/components/onboarding-step.tsx b/frontend/src/app/new-onboarding/components/onboarding-step.tsx new file mode 100644 index 00000000..bda65101 --- /dev/null +++ b/frontend/src/app/new-onboarding/components/onboarding-step.tsx @@ -0,0 +1,78 @@ +import { ReactNode, useEffect, useState } from "react"; +import { motion, AnimatePresence } from "motion/react"; +import { Message } from "@/app/chat/components/message"; +import DogIcon from "@/components/logo/dog-icon"; + +interface OnboardingStepProps { + text: string; + children: ReactNode; + isVisible: boolean; + isCompleted?: boolean; +} + +export function OnboardingStep({ text, children, isVisible, isCompleted = false }: OnboardingStepProps) { + const [displayedText, setDisplayedText] = useState(""); + const [showChildren, setShowChildren] = useState(false); + + useEffect(() => { + if (!isVisible) { + setDisplayedText(""); + setShowChildren(false); + return; + } + + let currentIndex = 0; + setDisplayedText(""); + setShowChildren(false); + + const interval = setInterval(() => { + if (currentIndex < text.length) { + setDisplayedText(text.slice(0, currentIndex + 1)); + currentIndex++; + } else { + clearInterval(interval); + setShowChildren(true); + } + }, 10); // 10ms per character + + return () => clearInterval(interval); + }, [text, isVisible]); + + if (!isVisible) return null; + + return ( + + + + + } + > +
+

+ {displayedText} + {!showChildren && !isCompleted && } +

+ + {showChildren && !isCompleted && ( + + {children} + + )} + +
+
+
+ ); +} diff --git a/frontend/src/app/new-onboarding/components/progress-bar.tsx b/frontend/src/app/new-onboarding/components/progress-bar.tsx new file mode 100644 index 00000000..e0e12e45 --- /dev/null +++ b/frontend/src/app/new-onboarding/components/progress-bar.tsx @@ -0,0 +1,27 @@ +interface ProgressBarProps { + currentStep: number; + totalSteps: number; +} + +export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) { + const progressPercentage = ((currentStep + 1) / totalSteps) * 100; + + return ( +
+
+
+
+
+ + {currentStep + 1}/{totalSteps} + +
+
+ ); +} diff --git a/frontend/src/app/new-onboarding/page.tsx b/frontend/src/app/new-onboarding/page.tsx new file mode 100644 index 00000000..efc50bf0 --- /dev/null +++ b/frontend/src/app/new-onboarding/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { Suspense, useState } from "react"; +import { ProtectedRoute } from "@/components/protected-route"; +import { DoclingHealthBanner } from "@/components/docling-health-banner"; +import { DotPattern } from "@/components/ui/dot-pattern"; +import { cn } from "@/lib/utils"; +import { OnboardingStep } from "./components/onboarding-step"; +import { ProgressBar } from "./components/progress-bar"; +import OnboardingCard from "../onboarding/components/onboarding-card"; + +const TOTAL_STEPS = 4; + +function NewOnboardingPage() { + const [currentStep, setCurrentStep] = useState(0); + + const handleStepComplete = () => { + if (currentStep < TOTAL_STEPS - 1) { + setCurrentStep(currentStep + 1); + } + }; + + return ( +
+ + + {/* Chat-like content area */} +
+
+
+ = 0} + isCompleted={currentStep > 0} + text="Let's get started by setting up your model provider." + > + + + + = 1} + isCompleted={currentStep > 1} + text="Step 1: Configure your settings" + > +
+

+ Let's configure some basic settings for your account. +

+ +
+
+ + = 2} + isCompleted={currentStep > 2} + text="Step 2: Connect your model" + > +
+

+ Choose and connect your preferred AI model provider. +

+ +
+
+ + = 3} + isCompleted={currentStep > 3} + text="Step 3: You're all set!" + > +
+

+ Your account is ready to use. Let's start chatting! +

+ +
+
+
+
+ + +
+
+ ); +} + +export default function ProtectedNewOnboardingPage() { + return ( + + Loading...
}> + + + + ); +} diff --git a/frontend/src/app/onboarding/components/advanced.tsx b/frontend/src/app/onboarding/components/advanced.tsx index 5872cadf..565b5af9 100644 --- a/frontend/src/app/onboarding/components/advanced.tsx +++ b/frontend/src/app/onboarding/components/advanced.tsx @@ -1,5 +1,4 @@ import { LabelWrapper } from "@/components/label-wrapper"; -import OllamaLogo from "@/components/logo/ollama-logo"; import { Accordion, AccordionContent, @@ -39,6 +38,8 @@ export function AdvancedOnboarding({ languageModels !== undefined && languageModel !== undefined && setLanguageModel !== undefined; + + const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true"; return ( @@ -74,18 +75,20 @@ export function AdvancedOnboarding({ /> )} - {(hasLanguageModels || hasEmbeddingModels) && } - - - + {(hasLanguageModels || hasEmbeddingModels) && !updatedOnboarding && } + {!updatedOnboarding && ( + + + + )} diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx index 9ca70ab8..58f629f7 100644 --- a/frontend/src/app/onboarding/components/onboarding-card.tsx +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -1,13 +1,12 @@ "use client"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; import { type OnboardingVariables, useOnboardingMutation, } from "@/app/api/mutations/useOnboardingMutation"; -import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; +import { useDoclingHealth } from "@/components/docling-health-banner"; import IBMLogo from "@/components/logo/ibm-logo"; import OllamaLogo from "@/components/logo/ollama-logo"; import OpenAILogo from "@/components/logo/openai-logo"; @@ -28,24 +27,13 @@ import { IBMOnboarding } from "./ibm-onboarding"; import { OllamaOnboarding } from "./ollama-onboarding"; import { OpenAIOnboarding } from "./openai-onboarding"; -const OnboardingCard = ({ - isDoclingHealthy, -}: { - isDoclingHealthy: boolean; -}) => { +interface OnboardingCardProps { + onComplete: () => void; +} + +const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true"; - const { data: settingsDb, isLoading: isSettingsLoading } = - useGetSettingsQuery(); - - const redirect = "/"; - const router = useRouter(); - - // Redirect if already authenticated or in no-auth mode - useEffect(() => { - if (!isSettingsLoading && settingsDb && settingsDb.edited) { - router.push(redirect); - } - }, [isSettingsLoading, settingsDb, router]); + const { isHealthy: isDoclingHealthy } = useDoclingHealth(); const [modelProvider, setModelProvider] = useState("openai"); @@ -71,7 +59,7 @@ const OnboardingCard = ({ const onboardingMutation = useOnboardingMutation({ onSuccess: (data) => { console.log("Onboarding completed successfully", data); - router.push(redirect); + onComplete(); }, onError: (error) => { toast.error("Failed to complete onboarding", { @@ -124,7 +112,7 @@ const OnboardingCard = ({ defaultValue={modelProvider} onValueChange={handleSetModelProvider} > - + @@ -140,7 +128,7 @@ const OnboardingCard = ({ - + - +
diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index e48ce7a1..5c2788f1 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -1,14 +1,28 @@ "use client"; -import { Suspense } from "react"; -import { DoclingHealthBanner, useDoclingHealth } from "@/components/docling-health-banner"; +import { Suspense, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { DoclingHealthBanner } from "@/components/docling-health-banner"; import { ProtectedRoute } from "@/components/protected-route"; import { DotPattern } from "@/components/ui/dot-pattern"; import { cn } from "@/lib/utils"; +import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import OnboardingCard from "./components/onboarding-card"; -function OnboardingPage() { - const { isHealthy: isDoclingHealthy } = useDoclingHealth(); +function LegacyOnboardingPage() { + const router = useRouter(); + const { data: settingsDb, isLoading: isSettingsLoading } = useGetSettingsQuery(); + + // Redirect if already completed onboarding + useEffect(() => { + if (!isSettingsLoading && settingsDb && settingsDb.edited) { + router.push("/"); + } + }, [isSettingsLoading, settingsDb, router]); + + const handleComplete = () => { + router.push("/"); + }; return (
@@ -32,17 +46,34 @@ function OnboardingPage() { Connect a model provider
- +
); } +function OnboardingRouter() { + const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true"; + const router = useRouter(); + + useEffect(() => { + if (updatedOnboarding) { + router.push("/new-onboarding"); + } + }, [updatedOnboarding, router]); + + if (updatedOnboarding) { + return null; + } + + return ; +} + export default function ProtectedOnboardingPage() { return ( Loading onboarding...}> - + ); diff --git a/frontend/src/components/layout-wrapper.tsx b/frontend/src/components/layout-wrapper.tsx index d6061384..130ad3f0 100644 --- a/frontend/src/components/layout-wrapper.tsx +++ b/frontend/src/components/layout-wrapper.tsx @@ -55,7 +55,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { }; // List of paths that should not show navigation - const authPaths = ["/login", "/auth/callback", "/onboarding"]; + const authPaths = ["/login", "/auth/callback", "/onboarding", "/new-onboarding"]; const isAuthPage = authPaths.includes(pathname); const isOnKnowledgePage = pathname.startsWith("/knowledge"); diff --git a/frontend/src/components/protected-route.tsx b/frontend/src/components/protected-route.tsx index ba195874..a5403e1a 100644 --- a/frontend/src/components/protected-route.tsx +++ b/frontend/src/components/protected-route.tsx @@ -39,7 +39,8 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) { } if (!isLoading && !isSettingsLoading && !settings.edited) { - router.push("/onboarding"); + const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true"; + router.push(updatedOnboarding ? "/new-onboarding" : "/onboarding"); } }, [ isLoading,