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";
|
"use client";
|
||||||
|
|
||||||
import { AlertTriangle, ExternalLink, Copy } from "lucide-react";
|
import { AlertTriangle, Copy, ExternalLink } from "lucide-react";
|
||||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
import { useState } from "react";
|
||||||
import { Banner, BannerIcon, BannerTitle, BannerAction } from "@/components/ui/banner";
|
import {
|
||||||
|
Banner,
|
||||||
|
BannerAction,
|
||||||
|
BannerIcon,
|
||||||
|
BannerTitle,
|
||||||
|
} from "@/components/ui/banner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogDescription,
|
||||||
DialogTitle,
|
DialogFooter,
|
||||||
DialogDescription,
|
DialogHeader,
|
||||||
DialogFooter
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { HEADER_HEIGHT } from "@/lib/constants";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useState } from "react";
|
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
||||||
|
|
||||||
interface DoclingHealthBannerProps {
|
interface DoclingHealthBannerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoclingSetupDialog component
|
// DoclingSetupDialog component
|
||||||
interface DoclingSetupDialogProps {
|
interface DoclingSetupDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DoclingSetupDialog({
|
function DoclingSetupDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
className
|
className,
|
||||||
}: DoclingSetupDialogProps) {
|
}: DoclingSetupDialogProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
await navigator.clipboard.writeText("uv run openrag");
|
await navigator.clipboard.writeText("uv run openrag");
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className={cn("max-w-lg", className)}>
|
<DialogContent className={cn("max-w-lg", className)}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-base">
|
<DialogTitle className="flex items-center gap-2 text-base">
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||||
docling-serve is stopped. Knowledge ingest is unavailable.
|
docling-serve is stopped. Knowledge ingest is unavailable.
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Start docling-serve by running:</DialogDescription>
|
||||||
Start docling-serve by running:
|
</DialogHeader>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code className="flex-1 bg-muted px-3 py-2.5 rounded-md text-sm font-mono">
|
<code className="flex-1 bg-muted px-3 py-2.5 rounded-md text-sm font-mono">
|
||||||
uv run openrag
|
uv run openrag
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
title={copied ? "Copied!" : "Copy to clipboard"}
|
title={copied ? "Copied!" : "Copy to clipboard"}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Then, select <span className="font-semibold text-foreground">Start All Services</span> in the TUI. Once docling-serve is running, refresh OpenRAG.
|
Then, select{" "}
|
||||||
</DialogDescription>
|
<span className="font-semibold text-foreground">
|
||||||
</div>
|
Start All Services
|
||||||
|
</span>{" "}
|
||||||
|
in the TUI. Once docling-serve is running, refresh OpenRAG.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button variant="default" onClick={() => onOpenChange(false)}>
|
||||||
variant="default"
|
Close
|
||||||
onClick={() => onOpenChange(false)}
|
</Button>
|
||||||
>
|
</DialogFooter>
|
||||||
Close
|
</DialogContent>
|
||||||
</Button>
|
</Dialog>
|
||||||
</DialogFooter>
|
);
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom hook to check docling health status
|
// Custom hook to check docling health status
|
||||||
export function useDoclingHealth() {
|
export function useDoclingHealth() {
|
||||||
const { data: health, isLoading, isError } = useDoclingHealthQuery();
|
const { data: health, isLoading, isError } = useDoclingHealthQuery();
|
||||||
|
|
||||||
const isHealthy = health?.status === "healthy" && !isError;
|
const isHealthy = health?.status === "healthy" && !isError;
|
||||||
const isUnhealthy = health?.status === "unhealthy" || isError;
|
const isUnhealthy = health?.status === "unhealthy" || isError;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
health,
|
health,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
isHealthy,
|
isHealthy,
|
||||||
isUnhealthy,
|
isUnhealthy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DoclingHealthBanner({ className }: DoclingHealthBannerProps) {
|
export function DoclingHealthBanner({ className }: DoclingHealthBannerProps) {
|
||||||
const { isLoading, isHealthy, isUnhealthy } = useDoclingHealth();
|
const { isLoading, isHealthy, isUnhealthy } = useDoclingHealth();
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
// Only show banner when service is unhealthy
|
// Only show banner when service is unhealthy
|
||||||
if (isLoading || isHealthy) {
|
if (isLoading || isHealthy) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUnhealthy) {
|
if (isUnhealthy) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Banner
|
<Banner
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200 border-amber-200 dark:border-amber-800",
|
`bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200 border-amber-200 dark:border-amber-800`,
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BannerIcon
|
<BannerIcon icon={AlertTriangle} />
|
||||||
icon={AlertTriangle}
|
<BannerTitle className="font-medium">
|
||||||
/>
|
docling-serve native service is stopped. Knowledge ingest is
|
||||||
<BannerTitle className="font-medium">
|
unavailable.
|
||||||
docling-serve native service is stopped. Knowledge ingest is unavailable.
|
</BannerTitle>
|
||||||
</BannerTitle>
|
<BannerAction
|
||||||
<BannerAction
|
onClick={() => setShowDialog(true)}
|
||||||
onClick={() => setShowDialog(true)}
|
className="bg-foreground text-background hover:bg-primary/90"
|
||||||
className="bg-foreground text-background hover:bg-primary/90"
|
>
|
||||||
>
|
Setup Docling Serve
|
||||||
Setup Docling Serve
|
<ExternalLink className="h-3 w-3 ml-1" />
|
||||||
<ExternalLink className="h-3 w-3 ml-1" />
|
</BannerAction>
|
||||||
</BannerAction>
|
</Banner>
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<DoclingSetupDialog
|
<DoclingSetupDialog open={showDialog} onOpenChange={setShowDialog} />
|
||||||
open={showDialog}
|
</>
|
||||||
onOpenChange={setShowDialog}
|
);
|
||||||
/>
|
}
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,15 +109,12 @@
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.app-grid-arrangement {
|
.app-grid-arrangement {
|
||||||
--sidebar-width: 0px;
|
|
||||||
--notifications-width: 0px;
|
--notifications-width: 0px;
|
||||||
--filters-width: 0px;
|
--filters-width: 0px;
|
||||||
--app-header-height: 53px;
|
|
||||||
--top-banner-height: 0px;
|
--top-banner-height: 0px;
|
||||||
|
--header-height: 54px;
|
||||||
|
--sidebar-width: 280px;
|
||||||
|
|
||||||
@media (width >= 48rem) {
|
|
||||||
--sidebar-width: 288px;
|
|
||||||
}
|
|
||||||
&.notifications-open {
|
&.notifications-open {
|
||||||
--notifications-width: 320px;
|
--notifications-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +129,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-rows:
|
grid-template-rows:
|
||||||
var(--top-banner-height)
|
var(--top-banner-height)
|
||||||
var(--app-header-height)
|
var(--header-height)
|
||||||
1fr;
|
1fr;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
var(--sidebar-width)
|
var(--sidebar-width)
|
||||||
|
|
@ -147,10 +144,6 @@
|
||||||
grid-template-rows 0.25s ease-in-out;
|
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 {
|
.header-start-display {
|
||||||
@apply flex items-center gap-2;
|
@apply flex items-center gap-2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,37 +9,28 @@ import { Button } from "@/components/ui/button";
|
||||||
import { DotPattern } from "@/components/ui/dot-pattern";
|
import { DotPattern } from "@/components/ui/dot-pattern";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery";
|
|
||||||
|
|
||||||
function LoginPageContent() {
|
function LoginPageContent() {
|
||||||
const { isLoading, isAuthenticated, isNoAuthMode, login } = useAuth();
|
const { isLoading, isAuthenticated, isNoAuthMode, login } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
const redirect = searchParams.get("redirect") || "/chat";
|
||||||
enabled: isAuthenticated || isNoAuthMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirect =
|
|
||||||
settings && !settings.edited
|
|
||||||
? "/onboarding"
|
|
||||||
: searchParams.get("redirect") || "/chat";
|
|
||||||
|
|
||||||
// Redirect if already authenticated or in no-auth mode
|
// Redirect if already authenticated or in no-auth mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isSettingsLoading && (isAuthenticated || isNoAuthMode)) {
|
if (!isLoading && (isAuthenticated || isNoAuthMode)) {
|
||||||
router.push(redirect);
|
router.push(redirect);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isLoading,
|
isLoading,
|
||||||
isSettingsLoading,
|
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isNoAuthMode,
|
isNoAuthMode,
|
||||||
router,
|
router,
|
||||||
redirect,
|
redirect,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (isLoading || isSettingsLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<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 { AnimatePresence, motion } from "motion/react";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { Message } from "@/app/chat/components/message";
|
import { Message } from "@/app/chat/components/message";
|
||||||
import DogIcon from "@/components/logo/dog-icon";
|
import DogIcon from "@/components/logo/dog-icon";
|
||||||
|
|
||||||
interface OnboardingStepProps {
|
interface OnboardingStepProps {
|
||||||
text: string;
|
text: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingStep({ text, children, isVisible, isCompleted = false }: OnboardingStepProps) {
|
export function OnboardingStep({
|
||||||
const [displayedText, setDisplayedText] = useState("");
|
text,
|
||||||
const [showChildren, setShowChildren] = useState(false);
|
children,
|
||||||
|
isVisible,
|
||||||
|
isCompleted = false,
|
||||||
|
}: OnboardingStepProps) {
|
||||||
|
const [displayedText, setDisplayedText] = useState("");
|
||||||
|
const [showChildren, setShowChildren] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
setDisplayedText("");
|
setDisplayedText("");
|
||||||
setShowChildren(false);
|
setShowChildren(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
setDisplayedText("");
|
setDisplayedText("");
|
||||||
setShowChildren(false);
|
setShowChildren(false);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (currentIndex < text.length) {
|
if (currentIndex < text.length) {
|
||||||
setDisplayedText(text.slice(0, currentIndex + 1));
|
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||||
currentIndex++;
|
currentIndex++;
|
||||||
} else {
|
} else {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
setShowChildren(true);
|
setShowChildren(true);
|
||||||
}
|
}
|
||||||
}, 10); // 10ms per character
|
}, 20); // 20ms per character
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [text, isVisible]);
|
}, [text, isVisible]);
|
||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
transition={{ duration: 0.4, delay: 0.4, ease: "easeOut" }}
|
||||||
className={isCompleted ? "opacity-50" : ""}
|
className={isCompleted ? "opacity-50" : ""}
|
||||||
>
|
>
|
||||||
<Message
|
<Message
|
||||||
icon={
|
icon={
|
||||||
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
|
<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} />
|
<DogIcon
|
||||||
</div>
|
className="h-6 w-6 text-accent-foreground"
|
||||||
}
|
disabled={isCompleted}
|
||||||
>
|
/>
|
||||||
<div className="space-y-4">
|
</div>
|
||||||
<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" />}
|
<div className="space-y-4">
|
||||||
</p>
|
<p
|
||||||
<AnimatePresence>
|
className={`text-foreground text-sm py-1.5 ${isCompleted ? "text-placeholder-foreground" : ""}`}
|
||||||
{showChildren && !isCompleted && (
|
>
|
||||||
<motion.div
|
{displayedText}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
{!showChildren && !isCompleted && (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<span className="inline-block w-1 h-4 bg-primary ml-1 animate-pulse" />
|
||||||
exit={{ opacity: 0, height: 0 }}
|
)}
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
</p>
|
||||||
>
|
<AnimatePresence>
|
||||||
{children}
|
{showChildren && !isCompleted && (
|
||||||
</motion.div>
|
<motion.div
|
||||||
)}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
</AnimatePresence>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</div>
|
exit={{ opacity: 0, height: 0 }}
|
||||||
</Message>
|
transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
|
||||||
</motion.div>
|
>
|
||||||
);
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center max-w-md mx-auto gap-3">
|
<div className="flex items-center max-w-48 mx-auto gap-3">
|
||||||
<div className="flex-1 h-2 bg-background rounded-full overflow-hidden">
|
<div className="flex-1 h-1 bg-background rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full transition-all duration-300 ease-in-out"
|
className="h-full transition-all duration-300 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -18,7 +18,7 @@ export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{currentStep + 1}/{totalSteps}
|
{currentStep + 1}/{totalSteps}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,48 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { ProtectedRoute } from "@/components/protected-route";
|
|
||||||
import { DoclingHealthBanner } from "@/components/docling-health-banner";
|
import { DoclingHealthBanner } from "@/components/docling-health-banner";
|
||||||
|
import { ProtectedRoute } from "@/components/protected-route";
|
||||||
import { DotPattern } from "@/components/ui/dot-pattern";
|
import { DotPattern } from "@/components/ui/dot-pattern";
|
||||||
import { cn } from "@/lib/utils";
|
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 { OnboardingStep } from "./components/onboarding-step";
|
||||||
import { ProgressBar } from "./components/progress-bar";
|
import { ProgressBar } from "./components/progress-bar";
|
||||||
import OnboardingCard from "../onboarding/components/onboarding-card";
|
|
||||||
|
|
||||||
const TOTAL_STEPS = 4;
|
const TOTAL_STEPS = 4;
|
||||||
|
|
||||||
function NewOnboardingPage() {
|
function NewOnboardingPage() {
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
|
||||||
const handleStepComplete = () => {
|
const handleStepComplete = () => {
|
||||||
if (currentStep < TOTAL_STEPS - 1) {
|
if (currentStep < TOTAL_STEPS - 1) {
|
||||||
setCurrentStep(currentStep + 1);
|
setCurrentStep(currentStep + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh w-full flex gap-5 flex-col items-center justify-center bg-primary-foreground relative p-4">
|
<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" />
|
<DoclingHealthBanner className="absolute top-0 left-0 right-0 w-full z-20" />
|
||||||
|
|
||||||
{/* Chat-like content area */}
|
{/* Chat-like content area */}
|
||||||
<div className="flex flex-col items-center gap-5 w-full max-w-3xl z-10">
|
<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="w-full h-[872px] bg-background border rounded-lg p-4 shadow-sm overflow-y-auto">
|
||||||
<div className="space-y-6">
|
<OnboardingContent />
|
||||||
<OnboardingStep
|
</div>
|
||||||
isVisible={currentStep >= 0}
|
|
||||||
isCompleted={currentStep > 0}
|
|
||||||
text="Let's get started by setting up your model provider."
|
|
||||||
>
|
|
||||||
<OnboardingCard onComplete={handleStepComplete} />
|
|
||||||
</OnboardingStep>
|
|
||||||
|
|
||||||
<OnboardingStep
|
<ProgressBar currentStep={currentStep} totalSteps={TOTAL_STEPS} />
|
||||||
isVisible={currentStep >= 1}
|
</div>
|
||||||
isCompleted={currentStep > 1}
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProtectedNewOnboardingPage() {
|
export default function ProtectedNewOnboardingPage() {
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<NewOnboardingPage />
|
<NewOnboardingPage />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ProtectedRoute>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { Bell, Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import {
|
|
||||||
useGetConversationsQuery,
|
|
||||||
type ChatConversation,
|
|
||||||
} from "@/app/api/queries/useGetConversationsQuery";
|
|
||||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||||
import { DoclingHealthBanner } from "@/components/docling-health-banner";
|
import { DoclingHealthBanner } from "@/components/docling-health-banner";
|
||||||
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel";
|
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 { TaskNotificationMenu } from "@/components/task-notification-menu";
|
||||||
import { UserNav } from "@/components/user-nav";
|
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useChat } from "@/contexts/chat-context";
|
|
||||||
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-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 { useTask } from "@/contexts/task-context";
|
||||||
import { useDoclingHealthQuery } from "@/src/app/api/queries/useDoclingHealthQuery";
|
|
||||||
import { cn } from "@/lib/utils";
|
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 }) {
|
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { tasks, isMenuOpen, toggleMenu } = useTask();
|
const { isMenuOpen } = useTask();
|
||||||
const { isPanelOpen } = useKnowledgeFilter();
|
const { isPanelOpen } = useKnowledgeFilter();
|
||||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
const {
|
|
||||||
endpoint,
|
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
||||||
refreshTrigger,
|
|
||||||
refreshConversations,
|
|
||||||
startNewConversation,
|
|
||||||
} = useChat();
|
|
||||||
const { isLoading: isSettingsLoading } = useGetSettingsQuery({
|
|
||||||
enabled: isAuthenticated || isNoAuthMode,
|
enabled: isAuthenticated || isNoAuthMode,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
|
|
@ -42,40 +28,16 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
isError,
|
isError,
|
||||||
} = useDoclingHealthQuery();
|
} = 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
|
// 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 isAuthPage = authPaths.includes(pathname);
|
||||||
const isOnKnowledgePage = pathname.startsWith("/knowledge");
|
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 isUnhealthy = health?.status === "unhealthy" || isError;
|
||||||
const isBannerVisible = !isHealthLoading && isUnhealthy;
|
const isBannerVisible = !isHealthLoading && isUnhealthy;
|
||||||
|
|
||||||
// Show loading state when backend isn't ready
|
// Show loading state when backend isn't ready
|
||||||
if (isLoading || isSettingsLoading) {
|
if (isLoading || isSettingsLoading || !settings) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<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
|
// For all other pages, render with Langflow-styled navigation and task menu
|
||||||
return (
|
return (
|
||||||
<div
|
<div className=" h-screen w-screen flex items-center justify-center">
|
||||||
className={cn(
|
<div
|
||||||
"app-grid-arrangement",
|
className={cn(
|
||||||
isBannerVisible && "banner-visible",
|
"app-grid-arrangement bg-black relative",
|
||||||
isPanelOpen && isOnKnowledgePage && !isMenuOpen && "filters-open",
|
isBannerVisible && "banner-visible",
|
||||||
isMenuOpen && "notifications-open"
|
isPanelOpen && isOnKnowledgePage && !isMenuOpen && "filters-open",
|
||||||
)}
|
isMenuOpen && "notifications-open",
|
||||||
>
|
)}
|
||||||
<div className="w-full [grid-area:banner]">
|
>
|
||||||
<DoclingHealthBanner className="w-full" />
|
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
|
|
@ -12,10 +11,6 @@ interface ProtectedRouteProps {
|
||||||
|
|
||||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
const { data: settings = {}, isLoading: isSettingsLoading } =
|
|
||||||
useGetSettingsQuery({
|
|
||||||
enabled: isAuthenticated || isNoAuthMode,
|
|
||||||
});
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
|
@ -31,30 +26,22 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isSettingsLoading && !isAuthenticated && !isNoAuthMode) {
|
if (!isLoading && !isAuthenticated && !isNoAuthMode) {
|
||||||
// Redirect to login with current path as redirect parameter
|
// Redirect to login with current path as redirect parameter
|
||||||
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`;
|
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading && !isSettingsLoading && !settings.edited) {
|
|
||||||
const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true";
|
|
||||||
router.push(updatedOnboarding ? "/new-onboarding" : "/onboarding");
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
isLoading,
|
isLoading,
|
||||||
isSettingsLoading,
|
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isNoAuthMode,
|
isNoAuthMode,
|
||||||
router,
|
router,
|
||||||
pathname,
|
pathname,
|
||||||
isSettingsLoading,
|
|
||||||
settings.edited,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Show loading state while checking authentication
|
// Show loading state while checking authentication
|
||||||
if (isLoading || isSettingsLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,9 @@ export const DEFAULT_KNOWLEDGE_SETTINGS = {
|
||||||
*/
|
*/
|
||||||
export const UI_CONSTANTS = {
|
export const UI_CONSTANTS = {
|
||||||
MAX_SYSTEM_PROMPT_CHARS: 2000,
|
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