feat: add new onboarding animation, adds onboarding into layout wrapper (#267)
* modularized header * removed onboarding redirection * added animations * Made animations slide instead of grow * change name of grow to slide * Added constants * Fixed animations, added onboarding content to card, switch display depending on current state of onboarding * fixed initial animation * fixed animation glitch * removed debug settimeout * fixed width * Format
This commit is contained in:
parent
c71cfedbe6
commit
6a971d45d6
13 changed files with 639 additions and 435 deletions
|
|
@ -1,147 +1,148 @@
|
|||
"use client";
|
||||
|
||||
import { AlertTriangle, ExternalLink, Copy } from "lucide-react";
|
||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
||||
import { Banner, BannerIcon, BannerTitle, BannerAction } from "@/components/ui/banner";
|
||||
import { AlertTriangle, Copy, ExternalLink } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
BannerAction,
|
||||
BannerIcon,
|
||||
BannerTitle,
|
||||
} from "@/components/ui/banner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { HEADER_HEIGHT } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
||||
|
||||
interface DoclingHealthBannerProps {
|
||||
className?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// DoclingSetupDialog component
|
||||
interface DoclingSetupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
className?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DoclingSetupDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
className
|
||||
open,
|
||||
onOpenChange,
|
||||
className,
|
||||
}: DoclingSetupDialogProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText("uv run openrag");
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText("uv run openrag");
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={cn("max-w-lg", className)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
docling-serve is stopped. Knowledge ingest is unavailable.
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Start docling-serve by running:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={cn("max-w-lg", className)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
docling-serve is stopped. Knowledge ingest is unavailable.
|
||||
</DialogTitle>
|
||||
<DialogDescription>Start docling-serve by running:</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted px-3 py-2.5 rounded-md text-sm font-mono">
|
||||
uv run openrag
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
className="shrink-0"
|
||||
title={copied ? "Copied!" : "Copy to clipboard"}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted px-3 py-2.5 rounded-md text-sm font-mono">
|
||||
uv run openrag
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
className="shrink-0"
|
||||
title={copied ? "Copied!" : "Copy to clipboard"}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DialogDescription>
|
||||
Then, select <span className="font-semibold text-foreground">Start All Services</span> in the TUI. Once docling-serve is running, refresh OpenRAG.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogDescription>
|
||||
Then, select{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
Start All Services
|
||||
</span>{" "}
|
||||
in the TUI. Once docling-serve is running, refresh OpenRAG.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogFooter>
|
||||
<Button variant="default" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook to check docling health status
|
||||
export function useDoclingHealth() {
|
||||
const { data: health, isLoading, isError } = useDoclingHealthQuery();
|
||||
const { data: health, isLoading, isError } = useDoclingHealthQuery();
|
||||
|
||||
const isHealthy = health?.status === "healthy" && !isError;
|
||||
const isUnhealthy = health?.status === "unhealthy" || isError;
|
||||
const isHealthy = health?.status === "healthy" && !isError;
|
||||
const isUnhealthy = health?.status === "unhealthy" || isError;
|
||||
|
||||
return {
|
||||
health,
|
||||
isLoading,
|
||||
isError,
|
||||
isHealthy,
|
||||
isUnhealthy,
|
||||
};
|
||||
return {
|
||||
health,
|
||||
isLoading,
|
||||
isError,
|
||||
isHealthy,
|
||||
isUnhealthy,
|
||||
};
|
||||
}
|
||||
|
||||
export function DoclingHealthBanner({ className }: DoclingHealthBannerProps) {
|
||||
const { isLoading, isHealthy, isUnhealthy } = useDoclingHealth();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { isLoading, isHealthy, isUnhealthy } = useDoclingHealth();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
// Only show banner when service is unhealthy
|
||||
if (isLoading || isHealthy) {
|
||||
return null;
|
||||
}
|
||||
// Only show banner when service is unhealthy
|
||||
if (isLoading || isHealthy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isUnhealthy) {
|
||||
return (
|
||||
<>
|
||||
<Banner
|
||||
className={cn(
|
||||
"bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200 border-amber-200 dark:border-amber-800",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<BannerIcon
|
||||
icon={AlertTriangle}
|
||||
/>
|
||||
<BannerTitle className="font-medium">
|
||||
docling-serve native service is stopped. Knowledge ingest is unavailable.
|
||||
</BannerTitle>
|
||||
<BannerAction
|
||||
onClick={() => setShowDialog(true)}
|
||||
className="bg-foreground text-background hover:bg-primary/90"
|
||||
>
|
||||
Setup Docling Serve
|
||||
<ExternalLink className="h-3 w-3 ml-1" />
|
||||
</BannerAction>
|
||||
</Banner>
|
||||
if (isUnhealthy) {
|
||||
return (
|
||||
<>
|
||||
<Banner
|
||||
className={cn(
|
||||
`bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200 border-amber-200 dark:border-amber-800`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<BannerIcon icon={AlertTriangle} />
|
||||
<BannerTitle className="font-medium">
|
||||
docling-serve native service is stopped. Knowledge ingest is
|
||||
unavailable.
|
||||
</BannerTitle>
|
||||
<BannerAction
|
||||
onClick={() => setShowDialog(true)}
|
||||
className="bg-foreground text-background hover:bg-primary/90"
|
||||
>
|
||||
Setup Docling Serve
|
||||
<ExternalLink className="h-3 w-3 ml-1" />
|
||||
</BannerAction>
|
||||
</Banner>
|
||||
|
||||
<DoclingSetupDialog
|
||||
open={showDialog}
|
||||
onOpenChange={setShowDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<DoclingSetupDialog open={showDialog} onOpenChange={setShowDialog} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,15 +109,12 @@
|
|||
|
||||
@layer components {
|
||||
.app-grid-arrangement {
|
||||
--sidebar-width: 0px;
|
||||
--notifications-width: 0px;
|
||||
--filters-width: 0px;
|
||||
--app-header-height: 53px;
|
||||
--top-banner-height: 0px;
|
||||
--header-height: 54px;
|
||||
--sidebar-width: 280px;
|
||||
|
||||
@media (width >= 48rem) {
|
||||
--sidebar-width: 288px;
|
||||
}
|
||||
&.notifications-open {
|
||||
--notifications-width: 320px;
|
||||
}
|
||||
|
|
@ -132,7 +129,7 @@
|
|||
width: 100%;
|
||||
grid-template-rows:
|
||||
var(--top-banner-height)
|
||||
var(--app-header-height)
|
||||
var(--header-height)
|
||||
1fr;
|
||||
grid-template-columns:
|
||||
var(--sidebar-width)
|
||||
|
|
@ -147,10 +144,6 @@
|
|||
grid-template-rows 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.header-arrangement {
|
||||
@apply flex w-full items-center justify-between border-b border-border;
|
||||
}
|
||||
|
||||
.header-start-display {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,37 +9,28 @@ import { Button } from "@/components/ui/button";
|
|||
import { DotPattern } from "@/components/ui/dot-pattern";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery";
|
||||
|
||||
function LoginPageContent() {
|
||||
const { isLoading, isAuthenticated, isNoAuthMode, login } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
||||
enabled: isAuthenticated || isNoAuthMode,
|
||||
});
|
||||
|
||||
const redirect =
|
||||
settings && !settings.edited
|
||||
? "/onboarding"
|
||||
: searchParams.get("redirect") || "/chat";
|
||||
const redirect = searchParams.get("redirect") || "/chat";
|
||||
|
||||
// Redirect if already authenticated or in no-auth mode
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isSettingsLoading && (isAuthenticated || isNoAuthMode)) {
|
||||
if (!isLoading && (isAuthenticated || isNoAuthMode)) {
|
||||
router.push(redirect);
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
isSettingsLoading,
|
||||
isAuthenticated,
|
||||
isNoAuthMode,
|
||||
router,
|
||||
redirect,
|
||||
]);
|
||||
|
||||
if (isLoading || isSettingsLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
"use client";
|
||||
|
||||
import OnboardingCard from "@/app/onboarding/components/onboarding-card";
|
||||
import { OnboardingStep } from "./onboarding-step";
|
||||
|
||||
export function OnboardingContent({
|
||||
handleStepComplete,
|
||||
currentStep,
|
||||
}: {
|
||||
handleStepComplete: () => void;
|
||||
currentStep: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 0}
|
||||
isCompleted={currentStep > 0}
|
||||
text="Let's get started by setting up your model provider."
|
||||
>
|
||||
<OnboardingCard onComplete={handleStepComplete} />
|
||||
</OnboardingStep>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 1}
|
||||
isCompleted={currentStep > 1}
|
||||
text="Step 1: Configure your settings"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Let's configure some basic settings for your account.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStepComplete}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 2}
|
||||
isCompleted={currentStep > 2}
|
||||
text="Step 2: Connect your model"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Choose and connect your preferred AI model provider.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStepComplete}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 3}
|
||||
isCompleted={currentStep > 3}
|
||||
text="Step 3: You're all set!"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Your account is ready to use. Let's start chatting!
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStepComplete}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Go to Chat
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,78 +1,90 @@
|
|||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { type ReactNode, useEffect, useState } from "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;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setDisplayedText("");
|
||||
setShowChildren(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = 0;
|
||||
setDisplayedText("");
|
||||
setShowChildren(false);
|
||||
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
|
||||
const interval = setInterval(() => {
|
||||
if (currentIndex < text.length) {
|
||||
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
setShowChildren(true);
|
||||
}
|
||||
}, 20); // 20ms per character
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [text, isVisible]);
|
||||
return () => clearInterval(interval);
|
||||
}, [text, isVisible]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
className={isCompleted ? "opacity-50" : ""}
|
||||
>
|
||||
<Message
|
||||
icon={
|
||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
||||
<DogIcon className="h-6 w-6 text-accent-foreground" disabled={isCompleted} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className={`text-foreground text-sm py-1.5 ${isCompleted ? "text-placeholder-foreground" : ""}`}>
|
||||
{displayedText}
|
||||
{!showChildren && !isCompleted && <span className="inline-block w-1 h-4 bg-primary ml-1 animate-pulse" />}
|
||||
</p>
|
||||
<AnimatePresence>
|
||||
{showChildren && !isCompleted && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Message>
|
||||
</motion.div>
|
||||
);
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.4, ease: "easeOut" }}
|
||||
className={isCompleted ? "opacity-50" : ""}
|
||||
>
|
||||
<Message
|
||||
icon={
|
||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
||||
<DogIcon
|
||||
className="h-6 w-6 text-accent-foreground"
|
||||
disabled={isCompleted}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p
|
||||
className={`text-foreground text-sm py-1.5 ${isCompleted ? "text-placeholder-foreground" : ""}`}
|
||||
>
|
||||
{displayedText}
|
||||
{!showChildren && !isCompleted && (
|
||||
<span className="inline-block w-1 h-4 bg-primary ml-1 animate-pulse" />
|
||||
)}
|
||||
</p>
|
||||
<AnimatePresence>
|
||||
{showChildren && !isCompleted && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Message>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) {
|
|||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center max-w-md mx-auto gap-3">
|
||||
<div className="flex-1 h-2 bg-background rounded-full overflow-hidden">
|
||||
<div className="flex items-center max-w-48 mx-auto gap-3">
|
||||
<div className="flex-1 h-1 bg-background rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
|
|
@ -18,7 +18,7 @@ export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{currentStep + 1}/{totalSteps}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,109 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { Suspense, useState } from "react";
|
||||
import { ProtectedRoute } from "@/components/protected-route";
|
||||
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 OnboardingCard from "../onboarding/components/onboarding-card";
|
||||
import { OnboardingContent } from "./components/onboarding-content";
|
||||
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 [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const handleStepComplete = () => {
|
||||
if (currentStep < TOTAL_STEPS - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
const handleStepComplete = () => {
|
||||
if (currentStep < TOTAL_STEPS - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh w-full flex gap-5 flex-col items-center justify-center bg-primary-foreground relative p-4">
|
||||
<DoclingHealthBanner className="absolute top-0 left-0 right-0 w-full z-20" />
|
||||
return (
|
||||
<div className="min-h-dvh w-full flex gap-5 flex-col items-center justify-center bg-primary-foreground relative p-4">
|
||||
<DoclingHealthBanner className="absolute top-0 left-0 right-0 w-full z-20" />
|
||||
|
||||
{/* Chat-like content area */}
|
||||
<div className="flex flex-col items-center gap-5 w-full max-w-3xl z-10">
|
||||
<div className="w-full h-[872px] bg-background border rounded-lg p-4 shadow-sm overflow-y-auto">
|
||||
<div className="space-y-6">
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 0}
|
||||
isCompleted={currentStep > 0}
|
||||
text="Let's get started by setting up your model provider."
|
||||
>
|
||||
<OnboardingCard onComplete={handleStepComplete} />
|
||||
</OnboardingStep>
|
||||
{/* Chat-like content area */}
|
||||
<div className="flex flex-col items-center gap-5 w-full max-w-3xl z-10">
|
||||
<div className="w-full h-[872px] bg-background border rounded-lg p-4 shadow-sm overflow-y-auto">
|
||||
<OnboardingContent />
|
||||
</div>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 1}
|
||||
isCompleted={currentStep > 1}
|
||||
text="Step 1: Configure your settings"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Let's configure some basic settings for your account.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStepComplete}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 2}
|
||||
isCompleted={currentStep > 2}
|
||||
text="Step 2: Connect your model"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Choose and connect your preferred AI model provider.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStepComplete}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
|
||||
<OnboardingStep
|
||||
isVisible={currentStep >= 3}
|
||||
isCompleted={currentStep > 3}
|
||||
text="Step 3: You're all set!"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Your account is ready to use. Let's start chatting!
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = "/chat"}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Go to Chat
|
||||
</button>
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressBar currentStep={currentStep} totalSteps={TOTAL_STEPS} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<ProgressBar currentStep={currentStep} totalSteps={TOTAL_STEPS} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProtectedNewOnboardingPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<NewOnboardingPage />
|
||||
</Suspense>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<NewOnboardingPage />
|
||||
</Suspense>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
53
frontend/src/components/animated-conditional.tsx
Normal file
53
frontend/src/components/animated-conditional.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { ANIMATION_DURATION } from "@/lib/constants";
|
||||
|
||||
export const AnimatedConditional = ({
|
||||
children,
|
||||
isOpen,
|
||||
className,
|
||||
slide = false,
|
||||
delay,
|
||||
vertical = false,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
vertical?: boolean;
|
||||
slide?: boolean;
|
||||
}) => {
|
||||
const animationProperty = slide
|
||||
? vertical
|
||||
? "translateY"
|
||||
: "translateX"
|
||||
: vertical
|
||||
? "height"
|
||||
: "width";
|
||||
const animationValue = isOpen
|
||||
? slide
|
||||
? "0px"
|
||||
: "auto"
|
||||
: slide
|
||||
? "-100%"
|
||||
: "0px";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ [animationProperty]: animationValue }}
|
||||
animate={{ [animationProperty]: animationValue }}
|
||||
exit={{ [animationProperty]: 0 }}
|
||||
transition={{
|
||||
duration: ANIMATION_DURATION,
|
||||
ease: "easeOut",
|
||||
delay: delay,
|
||||
}}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
whiteSpace: vertical ? "normal" : "nowrap",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
180
frontend/src/components/chat-renderer.tsx
Normal file
180
frontend/src/components/chat-renderer.tsx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
type ChatConversation,
|
||||
useGetConversationsQuery,
|
||||
} from "@/app/api/queries/useGetConversationsQuery";
|
||||
import type { Settings } from "@/app/api/queries/useGetSettingsQuery";
|
||||
import { OnboardingContent } from "@/app/new-onboarding/components/onboarding-content";
|
||||
import { ProgressBar } from "@/app/new-onboarding/components/progress-bar";
|
||||
import { AnimatedConditional } from "@/components/animated-conditional";
|
||||
import { Header } from "@/components/header";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
import {
|
||||
ANIMATION_DURATION,
|
||||
HEADER_HEIGHT,
|
||||
SIDEBAR_WIDTH,
|
||||
TOTAL_ONBOARDING_STEPS,
|
||||
} from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ChatRenderer({
|
||||
settings,
|
||||
children,
|
||||
}: {
|
||||
settings: Settings;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { isAuthenticated, isNoAuthMode } = useAuth();
|
||||
const {
|
||||
endpoint,
|
||||
refreshTrigger,
|
||||
refreshConversations,
|
||||
startNewConversation,
|
||||
} = useChat();
|
||||
|
||||
// Onboarding animation state
|
||||
const [showLayout, setShowLayout] = useState(!!settings?.edited);
|
||||
// Only fetch conversations on chat page
|
||||
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
||||
const { data: conversations = [], isLoading: isConversationsLoading } =
|
||||
useGetConversationsQuery(endpoint, refreshTrigger, {
|
||||
enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
|
||||
}) as { data: ChatConversation[]; isLoading: boolean };
|
||||
|
||||
const handleNewConversation = () => {
|
||||
refreshConversations();
|
||||
startNewConversation();
|
||||
};
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const handleStepComplete = () => {
|
||||
if (currentStep < TOTAL_ONBOARDING_STEPS - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
setShowLayout(true);
|
||||
}
|
||||
};
|
||||
|
||||
// List of paths with smaller max-width
|
||||
const smallWidthPaths = ["/settings/connector/new"];
|
||||
const isSmallWidthPath = smallWidthPaths.includes(pathname);
|
||||
|
||||
const x = showLayout ? "0px" : `calc(-${SIDEBAR_WIDTH / 2}px + 50vw)`;
|
||||
const y = showLayout ? "0px" : `calc(-${HEADER_HEIGHT / 2}px + 50vh)`;
|
||||
const translateY = showLayout ? "0px" : `-50vh`;
|
||||
const translateX = showLayout ? "0px" : `-50vw`;
|
||||
|
||||
// For all other pages, render with Langflow-styled navigation and task menu
|
||||
return (
|
||||
<>
|
||||
<AnimatedConditional
|
||||
className="[grid-area:header] bg-background border-b"
|
||||
vertical
|
||||
slide
|
||||
isOpen={showLayout}
|
||||
delay={ANIMATION_DURATION / 2}
|
||||
>
|
||||
<Header />
|
||||
</AnimatedConditional>
|
||||
|
||||
{/* Sidebar Navigation */}
|
||||
<AnimatedConditional
|
||||
isOpen={showLayout}
|
||||
slide
|
||||
className={`border-r bg-background overflow-hidden [grid-area:nav] w-[${SIDEBAR_WIDTH}px]`}
|
||||
>
|
||||
<Navigation
|
||||
conversations={conversations}
|
||||
isConversationsLoading={isConversationsLoading}
|
||||
onNewConversation={handleNewConversation}
|
||||
/>
|
||||
</AnimatedConditional>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="overflow-visible w-full flex items-center justify-center [grid-area:main]">
|
||||
<motion.div
|
||||
initial={{
|
||||
width: showLayout ? "100%" : "100vw",
|
||||
height: showLayout ? "100%" : "100vh",
|
||||
x: x,
|
||||
y: y,
|
||||
translateX: translateX,
|
||||
translateY: translateY,
|
||||
}}
|
||||
animate={{
|
||||
width: showLayout ? "100%" : "850px",
|
||||
borderRadius: showLayout ? "0" : "16px",
|
||||
border: showLayout ? "0" : "1px solid #27272A",
|
||||
height: showLayout ? "100%" : "800px",
|
||||
x: x,
|
||||
y: y,
|
||||
translateX: translateX,
|
||||
translateY: translateY,
|
||||
}}
|
||||
transition={{
|
||||
duration: ANIMATION_DURATION,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-full w-full max-w-full max-h-full items-center justify-center overflow-hidden",
|
||||
!showLayout && "absolute",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full bg-background",
|
||||
showLayout && "p-6 container",
|
||||
showLayout && isSmallWidthPath && "max-w-[850px] ml-0",
|
||||
!showLayout &&
|
||||
"w-full bg-card rounded-lg shadow-2xl p-8 overflow-y-auto",
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: showLayout ? 1 : 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: "100%",
|
||||
}}
|
||||
transition={{
|
||||
duration: ANIMATION_DURATION,
|
||||
ease: "easeOut",
|
||||
delay: ANIMATION_DURATION,
|
||||
}}
|
||||
className={cn("w-full h-full 0v")}
|
||||
>
|
||||
<div className={cn("w-full h-full", !showLayout && "hidden")}>
|
||||
{children}
|
||||
</div>
|
||||
{!showLayout && (
|
||||
<OnboardingContent
|
||||
handleStepComplete={handleStepComplete}
|
||||
currentStep={currentStep}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: showLayout ? 0 : 1, y: showLayout ? 20 : 0 }}
|
||||
transition={{ duration: ANIMATION_DURATION, ease: "easeOut" }}
|
||||
className={cn("absolute bottom-10 left-0 right-0")}
|
||||
>
|
||||
<ProgressBar
|
||||
currentStep={currentStep}
|
||||
totalSteps={TOTAL_ONBOARDING_STEPS}
|
||||
/>
|
||||
</motion.div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
60
frontend/src/components/header.tsx
Normal file
60
frontend/src/components/header.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"use client";
|
||||
|
||||
import { Bell } from "lucide-react";
|
||||
import Logo from "@/components/logo/logo";
|
||||
import { UserNav } from "@/components/user-nav";
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Header() {
|
||||
const { tasks, toggleMenu } = useTask();
|
||||
|
||||
// Calculate active tasks for the bell icon
|
||||
const activeTasks = tasks.filter(
|
||||
(task) =>
|
||||
task.status === "pending" ||
|
||||
task.status === "running" ||
|
||||
task.status === "processing",
|
||||
);
|
||||
|
||||
return (
|
||||
<header className={cn(`flex w-full h-full items-center justify-between`)}>
|
||||
<div className="header-start-display px-[16px]">
|
||||
{/* Logo/Title */}
|
||||
<div className="flex items-center">
|
||||
<Logo className="fill-primary" width={24} height={22} />
|
||||
<span className="text-lg font-semibold pl-2.5">OpenRAG</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="justify-end flex items-center">
|
||||
{/* Knowledge Filter Dropdown */}
|
||||
{/* <KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/> */}
|
||||
|
||||
{/* GitHub Star Button */}
|
||||
{/* <GitHubStarButton repo="phact/openrag" /> */}
|
||||
|
||||
{/* Discord Link */}
|
||||
{/* <DiscordLink inviteCode="EqksyE2EX9" /> */}
|
||||
|
||||
{/* Task Notification Bell */}
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="relative h-8 w-8 hover:bg-muted rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Bell size={16} className="text-muted-foreground" />
|
||||
{activeTasks.length > 0 && <div className="header-notifications" />}
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-6 bg-border mx-3" />
|
||||
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,39 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { Bell, Loader2 } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
useGetConversationsQuery,
|
||||
type ChatConversation,
|
||||
} from "@/app/api/queries/useGetConversationsQuery";
|
||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||
import { DoclingHealthBanner } from "@/components/docling-health-banner";
|
||||
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel";
|
||||
import Logo from "@/components/logo/logo";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import { TaskNotificationMenu } from "@/components/task-notification-menu";
|
||||
import { UserNav } from "@/components/user-nav";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useChat } from "@/contexts/chat-context";
|
||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||
// import { GitHubStarButton } from "@/components/github-star-button"
|
||||
// import { DiscordLink } from "@/components/discord-link"
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
||||
import { ChatRenderer } from "./chat-renderer";
|
||||
|
||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const { tasks, isMenuOpen, toggleMenu } = useTask();
|
||||
const { isMenuOpen } = useTask();
|
||||
const { isPanelOpen } = useKnowledgeFilter();
|
||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
||||
const {
|
||||
endpoint,
|
||||
refreshTrigger,
|
||||
refreshConversations,
|
||||
startNewConversation,
|
||||
} = useChat();
|
||||
const { isLoading: isSettingsLoading } = useGetSettingsQuery({
|
||||
|
||||
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
||||
enabled: isAuthenticated || isNoAuthMode,
|
||||
});
|
||||
const {
|
||||
|
|
@ -42,40 +28,16 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
|||
isError,
|
||||
} = useDoclingHealthQuery();
|
||||
|
||||
// Only fetch conversations on chat page
|
||||
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
||||
const { data: conversations = [], isLoading: isConversationsLoading } =
|
||||
useGetConversationsQuery(endpoint, refreshTrigger, {
|
||||
enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
|
||||
}) as { data: ChatConversation[]; isLoading: boolean };
|
||||
|
||||
const handleNewConversation = () => {
|
||||
refreshConversations();
|
||||
startNewConversation();
|
||||
};
|
||||
|
||||
// List of paths that should not show navigation
|
||||
const authPaths = ["/login", "/auth/callback", "/onboarding", "/new-onboarding"];
|
||||
const authPaths = ["/login", "/auth/callback"];
|
||||
const isAuthPage = authPaths.includes(pathname);
|
||||
const isOnKnowledgePage = pathname.startsWith("/knowledge");
|
||||
|
||||
// List of paths with smaller max-width
|
||||
const smallWidthPaths = ["/settings/connector/new"];
|
||||
const isSmallWidthPath = smallWidthPaths.includes(pathname);
|
||||
|
||||
// Calculate active tasks for the bell icon
|
||||
const activeTasks = tasks.filter(
|
||||
task =>
|
||||
task.status === "pending" ||
|
||||
task.status === "running" ||
|
||||
task.status === "processing"
|
||||
);
|
||||
|
||||
const isUnhealthy = health?.status === "unhealthy" || isError;
|
||||
const isBannerVisible = !isHealthLoading && isUnhealthy;
|
||||
|
||||
// Show loading state when backend isn't ready
|
||||
if (isLoading || isSettingsLoading) {
|
||||
if (isLoading || isSettingsLoading || !settings) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
|
|
@ -93,88 +55,31 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
|||
|
||||
// For all other pages, render with Langflow-styled navigation and task menu
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"app-grid-arrangement",
|
||||
isBannerVisible && "banner-visible",
|
||||
isPanelOpen && isOnKnowledgePage && !isMenuOpen && "filters-open",
|
||||
isMenuOpen && "notifications-open"
|
||||
)}
|
||||
>
|
||||
<div className="w-full [grid-area:banner]">
|
||||
<DoclingHealthBanner className="w-full" />
|
||||
<div className=" h-screen w-screen flex items-center justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
"app-grid-arrangement bg-black relative",
|
||||
isBannerVisible && "banner-visible",
|
||||
isPanelOpen && isOnKnowledgePage && !isMenuOpen && "filters-open",
|
||||
isMenuOpen && "notifications-open",
|
||||
)}
|
||||
>
|
||||
<div className={`w-full z-10 bg-background [grid-area:banner]`}>
|
||||
<DoclingHealthBanner className="w-full" />
|
||||
</div>
|
||||
|
||||
<ChatRenderer settings={settings}>{children}</ChatRenderer>
|
||||
|
||||
{/* Task Notifications Panel */}
|
||||
<aside className="overflow-y-auto overflow-x-hidden [grid-area:notifications]">
|
||||
{isMenuOpen && <TaskNotificationMenu />}
|
||||
</aside>
|
||||
|
||||
{/* Knowledge Filter Panel */}
|
||||
<aside className="overflow-y-auto overflow-x-hidden [grid-area:filters]">
|
||||
{isPanelOpen && <KnowledgeFilterPanel />}
|
||||
</aside>
|
||||
</div>
|
||||
<header className="header-arrangement bg-background [grid-area:header]">
|
||||
<div className="header-start-display px-[16px]">
|
||||
{/* Logo/Title */}
|
||||
<div className="flex items-center">
|
||||
<Logo className="fill-primary" width={24} height={22} />
|
||||
<span className="text-lg font-semibold pl-2.5">OpenRAG</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="justify-end flex items-center">
|
||||
{/* Knowledge Filter Dropdown */}
|
||||
{/* <KnowledgeFilterDropdown
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterSelect={setSelectedFilter}
|
||||
/> */}
|
||||
|
||||
{/* GitHub Star Button */}
|
||||
{/* <GitHubStarButton repo="phact/openrag" /> */}
|
||||
|
||||
{/* Discord Link */}
|
||||
{/* <DiscordLink inviteCode="EqksyE2EX9" /> */}
|
||||
|
||||
{/* Task Notification Bell */}
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="relative h-8 w-8 hover:bg-muted rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Bell size={16} className="text-muted-foreground" />
|
||||
{activeTasks.length > 0 && (
|
||||
<div className="header-notifications" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-6 bg-border mx-3" />
|
||||
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className="bg-background border-r overflow-hidden [grid-area:nav]">
|
||||
<Navigation
|
||||
conversations={conversations}
|
||||
isConversationsLoading={isConversationsLoading}
|
||||
onNewConversation={handleNewConversation}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="overflow-y-auto [grid-area:main]">
|
||||
<div
|
||||
className={cn(
|
||||
"p-6 h-full container",
|
||||
isSmallWidthPath && "max-w-[850px] ml-0"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Task Notifications Panel */}
|
||||
<aside className="overflow-y-auto overflow-x-hidden [grid-area:notifications]">
|
||||
{isMenuOpen && <TaskNotificationMenu />}
|
||||
</aside>
|
||||
|
||||
{/* Knowledge Filter Panel */}
|
||||
<aside className="overflow-y-auto overflow-x-hidden [grid-area:filters]">
|
||||
{isPanelOpen && <KnowledgeFilterPanel />}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { Loader2 } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
|
|
@ -12,10 +11,6 @@ interface ProtectedRouteProps {
|
|||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
||||
const { data: settings = {}, isLoading: isSettingsLoading } =
|
||||
useGetSettingsQuery({
|
||||
enabled: isAuthenticated || isNoAuthMode,
|
||||
});
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
|
|
@ -31,30 +26,22 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isSettingsLoading && !isAuthenticated && !isNoAuthMode) {
|
||||
if (!isLoading && !isAuthenticated && !isNoAuthMode) {
|
||||
// Redirect to login with current path as redirect parameter
|
||||
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`;
|
||||
router.push(redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoading && !isSettingsLoading && !settings.edited) {
|
||||
const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true";
|
||||
router.push(updatedOnboarding ? "/new-onboarding" : "/onboarding");
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
isSettingsLoading,
|
||||
isAuthenticated,
|
||||
isNoAuthMode,
|
||||
router,
|
||||
pathname,
|
||||
isSettingsLoading,
|
||||
settings.edited,
|
||||
]);
|
||||
|
||||
// Show loading state while checking authentication
|
||||
if (isLoading || isSettingsLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -22,4 +22,9 @@ export const DEFAULT_KNOWLEDGE_SETTINGS = {
|
|||
*/
|
||||
export const UI_CONSTANTS = {
|
||||
MAX_SYSTEM_PROMPT_CHARS: 2000,
|
||||
} as const;
|
||||
} as const;
|
||||
|
||||
export const ANIMATION_DURATION = 0.4;
|
||||
export const SIDEBAR_WIDTH = 280;
|
||||
export const HEADER_HEIGHT = 54;
|
||||
export const TOTAL_ONBOARDING_STEPS = 4;
|
||||
Loading…
Add table
Reference in a new issue