From 985f249fa1142b1c6de5b336465bfda0519b03a7 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Wed, 23 Oct 2024 10:53:41 -0700 Subject: [PATCH 01/25] Start on new sidebar --- app/(chat)/layout.tsx | 21 + app/globals.css | 16 + app/layout.tsx | 1 - components/app-sidebar.tsx | 234 +++++++++++ components/custom/chat.tsx | 27 +- components/custom/history.tsx | 241 ----------- components/sign-out-form.tsx | 25 ++ components/ui/button.tsx | 48 +-- components/ui/separator.tsx | 31 ++ components/ui/sidebar.tsx | 764 ++++++++++++++++++++++++++++++++++ components/ui/skeleton.tsx | 15 + components/ui/tooltip.tsx | 30 ++ hooks/use-mobile.tsx | 19 + package.json | 1 + pnpm-lock.yaml | 25 ++ tailwind.config.ts | 116 +++--- 16 files changed, 1285 insertions(+), 329 deletions(-) create mode 100644 app/(chat)/layout.tsx create mode 100644 components/app-sidebar.tsx delete mode 100644 components/custom/history.tsx create mode 100644 components/sign-out-form.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 hooks/use-mobile.tsx diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx new file mode 100644 index 000000000..21bde8e73 --- /dev/null +++ b/app/(chat)/layout.tsx @@ -0,0 +1,21 @@ +import { AppSidebar } from '@/components/app-sidebar'; +import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; + +import { auth } from '../(auth)/auth'; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + let session = await auth(); + return ( + + +
+ + {children} +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index e525b6c76..f9fdd4226 100644 --- a/app/globals.css +++ b/app/globals.css @@ -49,6 +49,14 @@ --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { --background: 240 10% 3.9%; @@ -75,6 +83,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/app/layout.tsx b/app/layout.tsx index d7d9e238f..13aa0859e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -65,7 +65,6 @@ export default async function RootLayout({ disableTransitionOnChange > - {children} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 000000000..c68b1e7a6 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,234 @@ +'use client'; +import { ChevronUp, User2 } from 'lucide-react'; +import Link from 'next/link'; +import { useParams, usePathname, useRouter } from 'next/navigation'; +import { type User } from 'next-auth'; +import { signOut } from 'next-auth/react'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; +import useSWR from 'swr'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarHeader, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, +} from '@/components/ui/sidebar'; +import { Chat } from '@/db/schema'; +import { cn, fetcher, getTitleFromChat } from '@/lib/utils'; + +import { + InfoIcon, + MessageIcon, + MoreHorizontalIcon, + PencilEditIcon, + TrashIcon, +} from './custom/icons'; +import { ThemeToggle } from './custom/theme-toggle'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from './ui/alert-dialog'; +import { Button } from './ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; + +export function AppSidebar({ user }: { user: User | undefined }) { + const { id } = useParams(); + const pathname = usePathname(); + const { + data: history, + isLoading, + mutate, + } = useSWR>(user ? '/api/history' : null, fetcher, { + fallbackData: [], + }); + + useEffect(() => { + mutate(); + }, [pathname, mutate]); + + const [deleteId, setDeleteId] = useState(null); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const router = useRouter(); + const handleDelete = async () => { + const deletePromise = fetch(`/api/chat?id=${deleteId}`, { + method: 'DELETE', + }); + + toast.promise(deletePromise, { + loading: 'Deleting chat...', + success: () => { + mutate((history) => { + if (history) { + return history.filter((h) => h.id !== id); + } + }); + return 'Chat deleted successfully'; + }, + error: 'Failed to delete chat', + }); + + setShowDeleteDialog(false); + if (deleteId === id) { + router.push('/'); + } + }; + + return ( + <> + + +
+ + Next.js Chatbot +
+
+ + + {user && ( + + )} + + + {!user ? ( +
+ +
Login to save and revisit previous chats!
+
+ ) : null} + + {!isLoading && history?.length === 0 && user ? ( +
+ +
No chats found
+
+ ) : null} + + {isLoading && user ? ( +
+ {[44, 32, 28, 52].map((item) => ( +
+
+
+ ))} +
+ ) : null} + + {history && + history.map((chat) => ( + + + + {getTitleFromChat(chat)} + + + + + + + + + + + + + + + + ))} + + + + + + + + + + {user?.email} + + + + + + + + + + + + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + chat and remove it from our servers. + + + + Cancel + + Continue + + + + + + ); +} diff --git a/components/custom/chat.tsx b/components/custom/chat.tsx index 382a89c48..ac28a917f 100644 --- a/components/custom/chat.tsx +++ b/components/custom/chat.tsx @@ -1,14 +1,16 @@ -"use client"; +'use client'; -import { Attachment, Message } from "ai"; -import { useChat } from "ai/react"; -import { useState } from "react"; +import { Attachment, Message } from 'ai'; +import { useChat } from 'ai/react'; +import { useState } from 'react'; -import { Message as PreviewMessage } from "@/components/custom/message"; -import { useScrollToBottom } from "@/components/custom/use-scroll-to-bottom"; +import { Message as PreviewMessage } from '@/components/custom/message'; +import { useScrollToBottom } from '@/components/custom/use-scroll-to-bottom'; +import { cn } from '@/lib/utils'; -import { MultimodalInput } from "./multimodal-input"; -import { Overview } from "./overview"; +import { MultimodalInput } from './multimodal-input'; +import { Overview } from './overview'; +import { useSidebar } from '../ui/sidebar'; export function Chat({ id, @@ -22,9 +24,10 @@ export function Chat({ body: { id }, initialMessages, onFinish: () => { - window.history.replaceState({}, "", `/chat/${id}`); + window.history.replaceState({}, '', `/chat/${id}`); }, }); + const { open } = useSidebar(); const [messagesContainerRef, messagesEndRef] = useScrollToBottom(); @@ -36,7 +39,11 @@ export function Chat({
{messages.length === 0 && } diff --git a/components/custom/history.tsx b/components/custom/history.tsx deleted file mode 100644 index 09e3907de..000000000 --- a/components/custom/history.tsx +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; - -import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; -import cx from "classnames"; -import Link from "next/link"; -import { useParams, usePathname } from "next/navigation"; -import { User } from "next-auth"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import useSWR from "swr"; - -import { Chat } from "@/db/schema"; -import { fetcher, getTitleFromChat } from "@/lib/utils"; - -import { - InfoIcon, - MenuIcon, - MoreHorizontalIcon, - PencilEditIcon, - TrashIcon, -} from "./icons"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "../ui/alert-dialog"; -import { Button } from "../ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "../ui/sheet"; - -export const History = ({ user }: { user: User | undefined }) => { - const { id } = useParams(); - const pathname = usePathname(); - - const [isHistoryVisible, setIsHistoryVisible] = useState(false); - const { - data: history, - isLoading, - mutate, - } = useSWR>(user ? "/api/history" : null, fetcher, { - fallbackData: [], - }); - - useEffect(() => { - mutate(); - }, [pathname, mutate]); - - const [deleteId, setDeleteId] = useState(null); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - - const handleDelete = async () => { - const deletePromise = fetch(`/api/chat?id=${deleteId}`, { - method: "DELETE", - }); - - toast.promise(deletePromise, { - loading: "Deleting chat...", - success: () => { - mutate((history) => { - if (history) { - return history.filter((h) => h.id !== id); - } - }); - return "Chat deleted successfully"; - }, - error: "Failed to delete chat", - }); - - setShowDeleteDialog(false); - }; - - return ( - <> - - - { - setIsHistoryVisible(state); - }} - > - - - - History - - {history === undefined ? "loading" : history.length} chats - - - - -
-
-
History
- -
- {history === undefined ? "loading" : history.length} chats -
-
-
- -
- {user && ( - - )} - -
- {!user ? ( -
- -
Login to save and revisit previous chats!
-
- ) : null} - - {!isLoading && history?.length === 0 && user ? ( -
- -
No chats found
-
- ) : null} - - {isLoading && user ? ( -
- {[44, 32, 28, 52].map((item) => ( -
-
-
- ))} -
- ) : null} - - {history && - history.map((chat) => ( -
- - - - - - - - - - - - -
- ))} -
-
- - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete your - chat and remove it from our servers. - - - - Cancel - - Continue - - - - - - ); -}; diff --git a/components/sign-out-form.tsx b/components/sign-out-form.tsx new file mode 100644 index 000000000..7fe9ee667 --- /dev/null +++ b/components/sign-out-form.tsx @@ -0,0 +1,25 @@ +import Form from 'next/form'; + +import { signOut } from '@/app/(auth)/auth'; + +export const SignOutForm = () => { + return ( +
{ + 'use server'; + + await signOut({ + redirectTo: '/', + }); + }} + > + +
+ ); +}; diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 0ba427735..81e2e6ee1 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,56 +1,56 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } -) +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button'; return ( - ) + ); } -) -Button.displayName = "Button" +); +Button.displayName = 'Button'; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 000000000..12d81c4a8 --- /dev/null +++ b/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx new file mode 100644 index 000000000..d1fd2ca01 --- /dev/null +++ b/components/ui/sidebar.tsx @@ -0,0 +1,764 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeft } from "lucide-react" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent } from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar:state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContext = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + if (setOpenProp) { + return setOpenProp?.( + typeof value === "function" ? value(open) : value + ) + } + + _setOpen(value) + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) + } +) +Sidebar.displayName = "Sidebar" + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) +}) +SidebarTrigger.displayName = "SidebarTrigger" + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + - - - - - - -
{ - 'use server'; - - await signOut({ - redirectTo: '/', - }); - }} - > - -
-
-
- - ) : ( - - )} -
- - ); -}; From 3ff81009f81a72c697e207edda0562e64dedbb95 Mon Sep 17 00:00:00 2001 From: athosg Date: Wed, 23 Oct 2024 21:37:53 +0100 Subject: [PATCH 03/25] Revert "Jp/new shadcn sidebar" --- app/(chat)/layout.tsx | 21 - app/globals.css | 16 - app/layout.tsx | 2 + components/app-sidebar.tsx | 234 ----------- components/custom/chat.tsx | 27 +- components/custom/history.tsx | 241 +++++++++++ components/custom/navbar.tsx | 72 ++++ components/sign-out-form.tsx | 25 -- components/ui/button.tsx | 48 +-- components/ui/separator.tsx | 31 -- components/ui/sidebar.tsx | 764 ---------------------------------- components/ui/skeleton.tsx | 15 - components/ui/tooltip.tsx | 30 -- hooks/use-mobile.tsx | 19 - package.json | 1 - pnpm-lock.yaml | 25 -- tailwind.config.ts | 116 +++--- 17 files changed, 402 insertions(+), 1285 deletions(-) delete mode 100644 app/(chat)/layout.tsx delete mode 100644 components/app-sidebar.tsx create mode 100644 components/custom/history.tsx create mode 100644 components/custom/navbar.tsx delete mode 100644 components/sign-out-form.tsx delete mode 100644 components/ui/separator.tsx delete mode 100644 components/ui/sidebar.tsx delete mode 100644 components/ui/skeleton.tsx delete mode 100644 components/ui/tooltip.tsx delete mode 100644 hooks/use-mobile.tsx diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx deleted file mode 100644 index 21bde8e73..000000000 --- a/app/(chat)/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { AppSidebar } from '@/components/app-sidebar'; -import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; - -import { auth } from '../(auth)/auth'; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - let session = await auth(); - return ( - - -
- - {children} -
-
- ); -} diff --git a/app/globals.css b/app/globals.css index f9fdd4226..e525b6c76 100644 --- a/app/globals.css +++ b/app/globals.css @@ -49,14 +49,6 @@ --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; } .dark { --background: 240 10% 3.9%; @@ -83,14 +75,6 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/app/layout.tsx b/app/layout.tsx index 81b5ea04a..d7d9e238f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import { Metadata } from 'next'; import { Toaster } from 'sonner'; +import { Navbar } from '@/components/custom/navbar'; import { ThemeProvider } from '@/components/custom/theme-provider'; import './globals.css'; @@ -64,6 +65,7 @@ export default async function RootLayout({ disableTransitionOnChange > + {children} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx deleted file mode 100644 index c68b1e7a6..000000000 --- a/components/app-sidebar.tsx +++ /dev/null @@ -1,234 +0,0 @@ -'use client'; -import { ChevronUp, User2 } from 'lucide-react'; -import Link from 'next/link'; -import { useParams, usePathname, useRouter } from 'next/navigation'; -import { type User } from 'next-auth'; -import { signOut } from 'next-auth/react'; -import { useEffect, useState } from 'react'; -import { toast } from 'sonner'; -import useSWR from 'swr'; - -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, -} from '@/components/ui/sidebar'; -import { Chat } from '@/db/schema'; -import { cn, fetcher, getTitleFromChat } from '@/lib/utils'; - -import { - InfoIcon, - MessageIcon, - MoreHorizontalIcon, - PencilEditIcon, - TrashIcon, -} from './custom/icons'; -import { ThemeToggle } from './custom/theme-toggle'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from './ui/alert-dialog'; -import { Button } from './ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from './ui/dropdown-menu'; - -export function AppSidebar({ user }: { user: User | undefined }) { - const { id } = useParams(); - const pathname = usePathname(); - const { - data: history, - isLoading, - mutate, - } = useSWR>(user ? '/api/history' : null, fetcher, { - fallbackData: [], - }); - - useEffect(() => { - mutate(); - }, [pathname, mutate]); - - const [deleteId, setDeleteId] = useState(null); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const router = useRouter(); - const handleDelete = async () => { - const deletePromise = fetch(`/api/chat?id=${deleteId}`, { - method: 'DELETE', - }); - - toast.promise(deletePromise, { - loading: 'Deleting chat...', - success: () => { - mutate((history) => { - if (history) { - return history.filter((h) => h.id !== id); - } - }); - return 'Chat deleted successfully'; - }, - error: 'Failed to delete chat', - }); - - setShowDeleteDialog(false); - if (deleteId === id) { - router.push('/'); - } - }; - - return ( - <> - - -
- - Next.js Chatbot -
-
- - - {user && ( - - )} - - - {!user ? ( -
- -
Login to save and revisit previous chats!
-
- ) : null} - - {!isLoading && history?.length === 0 && user ? ( -
- -
No chats found
-
- ) : null} - - {isLoading && user ? ( -
- {[44, 32, 28, 52].map((item) => ( -
-
-
- ))} -
- ) : null} - - {history && - history.map((chat) => ( - - - - {getTitleFromChat(chat)} - - - - - - - - - - - - - - - - ))} - - - - - - - - - - {user?.email} - - - - - - - - - - - - - - - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete your - chat and remove it from our servers. - - - - Cancel - - Continue - - - - - - ); -} diff --git a/components/custom/chat.tsx b/components/custom/chat.tsx index ac28a917f..382a89c48 100644 --- a/components/custom/chat.tsx +++ b/components/custom/chat.tsx @@ -1,16 +1,14 @@ -'use client'; +"use client"; -import { Attachment, Message } from 'ai'; -import { useChat } from 'ai/react'; -import { useState } from 'react'; +import { Attachment, Message } from "ai"; +import { useChat } from "ai/react"; +import { useState } from "react"; -import { Message as PreviewMessage } from '@/components/custom/message'; -import { useScrollToBottom } from '@/components/custom/use-scroll-to-bottom'; -import { cn } from '@/lib/utils'; +import { Message as PreviewMessage } from "@/components/custom/message"; +import { useScrollToBottom } from "@/components/custom/use-scroll-to-bottom"; -import { MultimodalInput } from './multimodal-input'; -import { Overview } from './overview'; -import { useSidebar } from '../ui/sidebar'; +import { MultimodalInput } from "./multimodal-input"; +import { Overview } from "./overview"; export function Chat({ id, @@ -24,10 +22,9 @@ export function Chat({ body: { id }, initialMessages, onFinish: () => { - window.history.replaceState({}, '', `/chat/${id}`); + window.history.replaceState({}, "", `/chat/${id}`); }, }); - const { open } = useSidebar(); const [messagesContainerRef, messagesEndRef] = useScrollToBottom(); @@ -39,11 +36,7 @@ export function Chat({
{messages.length === 0 && } diff --git a/components/custom/history.tsx b/components/custom/history.tsx new file mode 100644 index 000000000..09e3907de --- /dev/null +++ b/components/custom/history.tsx @@ -0,0 +1,241 @@ +"use client"; + +import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import cx from "classnames"; +import Link from "next/link"; +import { useParams, usePathname } from "next/navigation"; +import { User } from "next-auth"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import useSWR from "swr"; + +import { Chat } from "@/db/schema"; +import { fetcher, getTitleFromChat } from "@/lib/utils"; + +import { + InfoIcon, + MenuIcon, + MoreHorizontalIcon, + PencilEditIcon, + TrashIcon, +} from "./icons"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { Button } from "../ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "../ui/sheet"; + +export const History = ({ user }: { user: User | undefined }) => { + const { id } = useParams(); + const pathname = usePathname(); + + const [isHistoryVisible, setIsHistoryVisible] = useState(false); + const { + data: history, + isLoading, + mutate, + } = useSWR>(user ? "/api/history" : null, fetcher, { + fallbackData: [], + }); + + useEffect(() => { + mutate(); + }, [pathname, mutate]); + + const [deleteId, setDeleteId] = useState(null); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const handleDelete = async () => { + const deletePromise = fetch(`/api/chat?id=${deleteId}`, { + method: "DELETE", + }); + + toast.promise(deletePromise, { + loading: "Deleting chat...", + success: () => { + mutate((history) => { + if (history) { + return history.filter((h) => h.id !== id); + } + }); + return "Chat deleted successfully"; + }, + error: "Failed to delete chat", + }); + + setShowDeleteDialog(false); + }; + + return ( + <> + + + { + setIsHistoryVisible(state); + }} + > + + + + History + + {history === undefined ? "loading" : history.length} chats + + + + +
+
+
History
+ +
+ {history === undefined ? "loading" : history.length} chats +
+
+
+ +
+ {user && ( + + )} + +
+ {!user ? ( +
+ +
Login to save and revisit previous chats!
+
+ ) : null} + + {!isLoading && history?.length === 0 && user ? ( +
+ +
No chats found
+
+ ) : null} + + {isLoading && user ? ( +
+ {[44, 32, 28, 52].map((item) => ( +
+
+
+ ))} +
+ ) : null} + + {history && + history.map((chat) => ( +
+ + + + + + + + + + + + +
+ ))} +
+
+ + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + chat and remove it from our servers. + + + + Cancel + + Continue + + + + + + ); +}; diff --git a/components/custom/navbar.tsx b/components/custom/navbar.tsx new file mode 100644 index 000000000..3539e6efb --- /dev/null +++ b/components/custom/navbar.tsx @@ -0,0 +1,72 @@ +import Form from 'next/form'; +import Link from 'next/link'; + +import { auth, signOut } from '@/app/(auth)/auth'; + +import { History } from './history'; +import { ThemeToggle } from './theme-toggle'; +import { Button } from '../ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; + +export const Navbar = async () => { + let session = await auth(); + + return ( + <> +
+
+ +
+
Next.js Chatbot
+
+
+ + {session ? ( + + + + + + + + + +
{ + 'use server'; + + await signOut({ + redirectTo: '/', + }); + }} + > + +
+
+
+
+ ) : ( + + )} +
+ + ); +}; diff --git a/components/sign-out-form.tsx b/components/sign-out-form.tsx deleted file mode 100644 index 7fe9ee667..000000000 --- a/components/sign-out-form.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Form from 'next/form'; - -import { signOut } from '@/app/(auth)/auth'; - -export const SignOutForm = () => { - return ( -
{ - 'use server'; - - await signOut({ - redirectTo: '/', - }); - }} - > - -
- ); -}; diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 81e2e6ee1..0ba427735 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,56 +1,56 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils" const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, } -); +) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; + asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; + const Comp = asChild ? Slot : "button" return ( - ); + ) } -); -Button.displayName = 'Button'; +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx deleted file mode 100644 index 12d81c4a8..000000000 --- a/components/ui/separator.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" - -import { cn } from "@/lib/utils" - -const Separator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref - ) => ( - - ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName - -export { Separator } diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx deleted file mode 100644 index d1fd2ca01..000000000 --- a/components/ui/sidebar.tsx +++ /dev/null @@ -1,764 +0,0 @@ -"use client" - -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { VariantProps, cva } from "class-variance-authority" -import { PanelLeft } from "lucide-react" - -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { Sheet, SheetContent } from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" - -const SIDEBAR_COOKIE_NAME = "sidebar:state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" - -type SidebarContext = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} - -const SidebarContext = React.createContext(null) - -function useSidebar() { - const context = React.useContext(SidebarContext) - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") - } - - return context -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen) - const open = openProp ?? _open - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - if (setOpenProp) { - return setOpenProp?.( - typeof value === "function" ? value(open) : value - ) - } - - _setOpen(value) - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` - }, - [setOpenProp, open] - ) - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - toggleSidebar() - } - } - - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) - - return ( - - -
- {children} -
-
-
- ) - } -) -SidebarProvider.displayName = "SidebarProvider" - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() - - if (collapsible === "none") { - return ( -
- {children} -
- ) - } - - if (isMobile) { - return ( - - -
{children}
-
-
- ) - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ) - } -) -Sidebar.displayName = "Sidebar" - -const SidebarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar() - - return ( - - ) -}) -SidebarTrigger.displayName = "SidebarTrigger" - -const SidebarRail = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> ->(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar() - - return ( - + + ))} + + + + {showChatButton && ( + + + + + + )} + +
+ + + + + + + +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 81b5ea04a..827b40c40 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,8 @@ import { Metadata } from 'next'; +import NextTopLoader from 'nextjs-toploader'; import { Toaster } from 'sonner'; +import { ReactQueryProvider } from '@/components/custom/query-provider'; // Import the ReactQueryProvider import { ThemeProvider } from '@/components/custom/theme-provider'; import './globals.css'; @@ -57,15 +59,18 @@ export default async function RootLayout({ /> - - - {children} - + + + + + {children} + + ); diff --git a/components/custom/query-provider.tsx b/components/custom/query-provider.tsx new file mode 100644 index 000000000..4e785af89 --- /dev/null +++ b/components/custom/query-provider.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { useState } from 'react'; + +export function ReactQueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + {children} + + + ); +} diff --git a/components/custom/sidebar-user-nav.tsx b/components/custom/sidebar-user-nav.tsx index cfda9813a..3c116a1e0 100644 --- a/components/custom/sidebar-user-nav.tsx +++ b/components/custom/sidebar-user-nav.tsx @@ -1,6 +1,7 @@ 'use client'; -import { ChevronUp } from 'lucide-react'; +import { ChevronUp, Hammer, Moon, Sun } from 'lucide-react'; import Image from 'next/image'; +import Link from 'next/link'; import { type User } from 'next-auth'; import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; @@ -44,8 +45,19 @@ export function SidebarUserNav({ user }: { user: User }) { setTheme(theme === 'dark' ? 'light' : 'dark')} > + {theme === 'light' ? ( + + ) : ( + + )} {`Toggle ${theme === 'light' ? 'dark' : 'light'} mode`} + + + + {`Settings`} + + + + + + + + + + {isSetupCompleted && allStepsCompleted && ( + + )} + + + + ); +} diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx new file mode 100644 index 000000000..0a13c93fb --- /dev/null +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -0,0 +1,374 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { + Upload, + Trash2, + RefreshCw, + ArrowUpDown, + Search, + ArrowLeft, + CheckCircle, + XCircle, + Clock, +} from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState, useEffect, useRef } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Progress } from '@/components/ui/progress'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +interface File { + id: string; + name: string; + size: number; + type: string; + uploadDate: Date; + status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; + progress: number; +} + +interface KnowledgebaseCarouselProps { + onClose: () => void; +} + +export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { + const router = useRouter(); + const fileInputRef = useRef(null); + const [files, setFiles] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [sortColumn, setSortColumn] = useState('uploadDate'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + const [isUploading, setIsUploading] = useState(false); + const [canReturnToSettings, setCanReturnToSettings] = useState(false); + + useEffect(() => { + const hasProcessedFile = files.some((file) => file.status === 'processed'); + setCanReturnToSettings(hasProcessedFile); + }, [files]); + + const simulateFileUpload = (file: File) => { + const uploadInterval = setInterval(() => { + setFiles((prevFiles) => + prevFiles.map((f) => + f.id === file.id + ? { ...f, progress: Math.min(f.progress + 10, 100) } + : f + ) + ); + }, 200); + + setTimeout( + () => { + clearInterval(uploadInterval); + setFiles((prevFiles) => + prevFiles.map((f) => + f.id === file.id ? { ...f, status: 'uploaded', progress: 100 } : f + ) + ); + }, + 2000 + Math.random() * 1000 + ); + }; + + const handleFileUpload = (event: React.ChangeEvent) => { + const fileList = event.target.files; + if (fileList) { + setIsUploading(true); + const newFiles: File[] = Array.from(fileList).map((file) => ({ + id: Math.random().toString(36).substr(2, 9), + name: file.name, + size: file.size, + type: file.type, + uploadDate: new Date(), + status: 'uploading', + progress: 0, + })); + + setFiles((prevFiles) => [...prevFiles, ...newFiles]); + newFiles.forEach(simulateFileUpload); + + setTimeout( + () => { + setIsUploading(false); + }, + 2000 + newFiles.length * 500 + ); + } + }; + + const handleProcessFile = (id: string) => { + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id ? { ...file, status: 'processing', progress: 0 } : file + ) + ); + + const processInterval = setInterval(() => { + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id + ? { ...file, progress: Math.min(file.progress + 10, 100) } + : file + ) + ); + }, 200); + + setTimeout( + () => { + clearInterval(processInterval); + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id + ? { ...file, status: 'processed', progress: 100 } + : file + ) + ); + }, + 2000 + Math.random() * 1000 + ); + }; + + const handleDeleteFile = (id: string) => { + setFiles((prevFiles) => prevFiles.filter((file) => file.id !== id)); + }; + + const handleSort = (column: keyof File) => { + if (column === sortColumn) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortDirection('asc'); + } + }; + + const sortedFiles = [...files].sort((a, b) => { + if (a[sortColumn] < b[sortColumn]) return sortDirection === 'asc' ? -1 : 1; + if (a[sortColumn] > b[sortColumn]) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); + + const filteredFiles = sortedFiles.filter((file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getStatusIcon = (status: File['status'], progress: number) => { + switch (status) { + case 'uploading': + case 'processing': + return ( +
+ + + + +
+ ); + case 'uploaded': + return ; + case 'processed': + return ; + case 'error': + return ; + default: + return null; + } + }; + + return ( + + + + File Management + + Upload and manage your knowledgebase files + + + +
+
+ setSearchTerm(e.target.value)} + /> +
+
+ + + +
+
+ + + + + handleSort('name')} + className="cursor-pointer" + > + Name + + handleSort('size')} + className="cursor-pointer" + > + Size + + handleSort('type')} + className="cursor-pointer" + > + Type + + handleSort('uploadDate')} + className="cursor-pointer" + > + Upload Date{' '} + + + handleSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + + {filteredFiles.map((file) => ( + + {file.name} + {formatFileSize(file.size)} + {file.type} + {file.uploadDate.toLocaleString()} + + + + +
+ {getStatusIcon(file.status, file.progress)} + {file.status} +
+
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ +
+ + +
+
+
+ ))} +
+
+
+
+ +
{files.length} file(s) uploaded
+
+
+
+ ); +} diff --git a/app/(settings)/settings/page.tsx b/app/(settings)/settings/page.tsx index 7f5539b09..63dd87247 100644 --- a/app/(settings)/settings/page.tsx +++ b/app/(settings)/settings/page.tsx @@ -16,13 +16,16 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, - CardHeader, - CardTitle, CardDescription, CardFooter, + CardHeader, + CardTitle, } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; +import { Configuration } from './configuration'; +import { Knowledgebase } from './knowledgebase'; + export default function Component() { const router = useRouter(); const [isConfigurationComplete, setIsConfigurationComplete] = useState(false); @@ -32,6 +35,10 @@ export default function Component() { const [progress, setProgress] = useState(0); const [showChatButton, setShowChatButton] = useState(false); const [showSteps, setShowSteps] = useState(true); + const [showConfigurationCarousel, setShowConfigurationCarousel] = + useState(false); + const [showKnowledgebaseCarousel, setShowKnowledgebaseCarousel] = + useState(false); // Setup steps const setupSteps = [ @@ -43,7 +50,7 @@ export default function Component() { disabled: false, }, { - title: 'knowledgebase', + title: 'Knowledgebase', icon: Anvil, route: '/knowledgebase', isComplete: isKnowledgebaseComplete, @@ -79,7 +86,23 @@ export default function Component() { ]); const handleRouteNavigation = (route: string) => { - router.push(route); + if (route === '/configuration') { + setShowConfigurationCarousel(true); + } else if (route === '/knowledgebase') { + setShowKnowledgebaseCarousel(true); + } else { + router.push(route); + } + }; + + const handleCloseConfigurationCarousel = () => { + setShowConfigurationCarousel(false); + setIsConfigurationComplete(true); + }; + + const handleCloseKnowledgebaseCarousel = () => { + setShowKnowledgebaseCarousel(false); + setIsKnowledgebaseComplete(true); }; return ( @@ -167,7 +190,7 @@ export default function Component() { variant="ghost" onClick={() => setIsKnowledgebaseComplete(true)} > - Simulate knowledgebase Completion + Simulate Knowledgebase Completion
); } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 000000000..01ff19c7e --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 000000000..7f3502f8b --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} From a1320f051e1489e20d2a649c33e849d1b6f1f05f Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Mon, 4 Nov 2024 21:45:23 +0000 Subject: [PATCH 09/25] Add search bar, file uploader, file table, and file row components for knowledgebase --- .../settings/knowledgebase/FileRow.tsx | 125 ++++++++++ .../settings/knowledgebase/FileTable.tsx | 81 +++++++ .../settings/knowledgebase/FileUploader.tsx | 32 +++ .../settings/knowledgebase/SearchBar.tsx | 17 ++ .../settings/knowledgebase/index.tsx | 217 ++---------------- 5 files changed, 278 insertions(+), 194 deletions(-) create mode 100644 app/(settings)/settings/knowledgebase/FileRow.tsx create mode 100644 app/(settings)/settings/knowledgebase/FileTable.tsx create mode 100644 app/(settings)/settings/knowledgebase/FileUploader.tsx create mode 100644 app/(settings)/settings/knowledgebase/SearchBar.tsx diff --git a/app/(settings)/settings/knowledgebase/FileRow.tsx b/app/(settings)/settings/knowledgebase/FileRow.tsx new file mode 100644 index 000000000..70ce61077 --- /dev/null +++ b/app/(settings)/settings/knowledgebase/FileRow.tsx @@ -0,0 +1,125 @@ +import { motion } from 'framer-motion'; +import { CheckCircle, Clock, RefreshCw, Trash2, XCircle } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { TableCell } from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +interface File { + id: string; + name: string; + size: number; + type: string; + uploadDate: Date; + status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; + progress: number; +} + +interface FileRowProps { + file: File; + onProcessFile: (id: string) => void; + onDeleteFile: (id: string) => void; +} + +export function FileRow({ file, onProcessFile, onDeleteFile }: FileRowProps) { + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getStatusIcon = (status: File['status'], progress: number) => { + switch (status) { + case 'uploading': + case 'processing': + return ( +
+ + + + +
+ ); + case 'uploaded': + return ; + case 'processed': + return ; + case 'error': + return ; + default: + return null; + } + }; + + return ( + + {file.name} + {formatFileSize(file.size)} + {file.type} + {file.uploadDate.toLocaleString()} + + + + +
+ {getStatusIcon(file.status, file.progress)} + {file.status} +
+
+ + {file.status === 'uploading' || file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ +
+ + +
+
+
+ ); +} diff --git a/app/(settings)/settings/knowledgebase/FileTable.tsx b/app/(settings)/settings/knowledgebase/FileTable.tsx new file mode 100644 index 000000000..0168e5ea5 --- /dev/null +++ b/app/(settings)/settings/knowledgebase/FileTable.tsx @@ -0,0 +1,81 @@ +import { AnimatePresence } from 'framer-motion'; +import { ArrowUpDown } from 'lucide-react'; + +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; + +import { FileRow } from './FileRow'; + +interface File { + id: string; + name: string; + size: number; + type: string; + uploadDate: Date; + status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; + progress: number; +} + +interface FileTableProps { + files: File[]; + sortColumn: keyof File; + sortDirection: 'asc' | 'desc'; + onSort: (column: keyof File) => void; + onProcessFile: (id: string) => void; + onDeleteFile: (id: string) => void; +} + +export function FileTable({ + files, + onSort, + onProcessFile, + onDeleteFile, +}: FileTableProps) { + return ( + + + + onSort('name')} className="cursor-pointer"> + Name + + onSort('size')} className="cursor-pointer"> + Size + + onSort('type')} className="cursor-pointer"> + Type + + onSort('uploadDate')} + className="cursor-pointer" + > + Upload Date + + onSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + + {files.map((file) => ( + + ))} + + +
+ ); +} diff --git a/app/(settings)/settings/knowledgebase/FileUploader.tsx b/app/(settings)/settings/knowledgebase/FileUploader.tsx new file mode 100644 index 000000000..428f25820 --- /dev/null +++ b/app/(settings)/settings/knowledgebase/FileUploader.tsx @@ -0,0 +1,32 @@ +import { Upload } from 'lucide-react'; +import { useRef } from 'react'; + +import { Button } from '@/components/ui/button'; + +interface FileUploaderProps { + onFileUpload: (event: React.ChangeEvent) => void; + isUploading: boolean; +} + +export function FileUploader({ onFileUpload, isUploading }: FileUploaderProps) { + const fileInputRef = useRef(null); + + return ( + <> + + + + ); +} diff --git a/app/(settings)/settings/knowledgebase/SearchBar.tsx b/app/(settings)/settings/knowledgebase/SearchBar.tsx new file mode 100644 index 000000000..aedc0f3dd --- /dev/null +++ b/app/(settings)/settings/knowledgebase/SearchBar.tsx @@ -0,0 +1,17 @@ +import { Input } from '@/components/ui/input'; + +interface SearchBarProps { + searchTerm: string; + onSearchTermChange: (term: string) => void; +} + +export function SearchBar({ searchTerm, onSearchTermChange }: SearchBarProps) { + return ( + onSearchTermChange(e.target.value)} + /> + ); +} diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index 0a13c93fb..4b0232dbe 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -1,19 +1,6 @@ -'use client'; - -import { motion, AnimatePresence } from 'framer-motion'; -import { - Upload, - Trash2, - RefreshCw, - ArrowUpDown, - Search, - ArrowLeft, - CheckCircle, - XCircle, - Clock, -} from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { useState, useEffect, useRef } from 'react'; +import { motion } from 'framer-motion'; +import { ArrowLeft } from 'lucide-react'; +import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { @@ -24,22 +11,10 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Progress } from '@/components/ui/progress'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; + +import { FileTable } from './FileTable'; +import { FileUploader } from './FileUploader'; +import { SearchBar } from './SearchBar'; interface File { id: string; @@ -51,13 +26,11 @@ interface File { progress: number; } -interface KnowledgebaseCarouselProps { +interface KnowledgebaseProps { onClose: () => void; } -export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { - const router = useRouter(); - const fileInputRef = useRef(null); +export function Knowledgebase({ onClose }: KnowledgebaseProps) { const [files, setFiles] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [sortColumn, setSortColumn] = useState('uploadDate'); @@ -175,48 +148,6 @@ export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }; - - const getStatusIcon = (status: File['status'], progress: number) => { - switch (status) { - case 'uploading': - case 'processing': - return ( -
- - - - -
- ); - case 'uploaded': - return ; - case 'processed': - return ; - case 'error': - return ; - default: - return null; - } - }; - return (
- setSearchTerm(e.target.value)} +
- -
- - - - - handleSort('name')} - className="cursor-pointer" - > - Name - - handleSort('size')} - className="cursor-pointer" - > - Size - - handleSort('type')} - className="cursor-pointer" - > - Type - - handleSort('uploadDate')} - className="cursor-pointer" - > - Upload Date{' '} - - - handleSort('status')} - className="cursor-pointer" - > - Status - - Actions - - - - - {filteredFiles.map((file) => ( - - {file.name} - {formatFileSize(file.size)} - {file.type} - {file.uploadDate.toLocaleString()} - - - - -
- {getStatusIcon(file.status, file.progress)} - {file.status} -
-
- - {file.status === 'uploading' || - file.status === 'processing' - ? `${file.progress}% complete` - : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} - -
-
-
- -
- - -
-
-
- ))} -
-
-
+
{files.length} file(s) uploaded
From b7e154536aee294cd133c1f86084fbe06d28ca4b Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 15:03:28 +0000 Subject: [PATCH 10/25] Update Next.js version to 15.0.3-canary.6 --- package.json | 2 +- yarn.lock | 184 ++++++++++++++++++++++++++------------------------- 2 files changed, 96 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index da2d97d8f..ae1dbbcc5 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "framer-motion": "^11.3.19", "geist": "^1.3.1", "lucide-react": "^0.446.0", - "next": "15.0.3-canary.2", + "next": "15.0.3-canary.6", "next-auth": "5.0.0-beta.25", "next-themes": "^0.3.0", "nextjs-toploader": "^3.7.15", diff --git a/yarn.lock b/yarn.lock index 763b44b73..17b3a5551 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,36 +44,37 @@ dependencies: json-schema "^0.4.0" -"@ai-sdk/react@0.0.68": - version "0.0.68" - resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-0.0.68.tgz#c63e13e0b374401fda43c8f21d9b0218b1578be3" - integrity sha512-dD7cm2UsPWkuWg+qKRXjF+sNLVcUzWUnV25FxvEliJP7I2ajOpq8c+/xyGlm+YodyvAB0fX+oSODOeIWi7lCKg== +"@ai-sdk/react@0.0.70": + version "0.0.70" + resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-0.0.70.tgz#25dee1755c67da2ac0ed4f207102de93d7196f44" + integrity sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ== dependencies: "@ai-sdk/provider-utils" "1.0.22" - "@ai-sdk/ui-utils" "0.0.49" + "@ai-sdk/ui-utils" "0.0.50" swr "^2.2.5" + throttleit "2.1.0" -"@ai-sdk/solid@0.0.53": - version "0.0.53" - resolved "https://registry.yarnpkg.com/@ai-sdk/solid/-/solid-0.0.53.tgz#498bdc6b869649b9817c656172ca053df8f9fac2" - integrity sha512-0yXkwTE75QKdmz40CBtAFy3sQdUnn/TNMTkTE2xfqC9YN7Ixql472TtC+3h6s4dPjRJm5bNnGJAWHwjT2PBmTw== +"@ai-sdk/solid@0.0.54": + version "0.0.54" + resolved "https://registry.yarnpkg.com/@ai-sdk/solid/-/solid-0.0.54.tgz#60f2007d511f153159d9e5ddc1e8b800fb472c58" + integrity sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ== dependencies: "@ai-sdk/provider-utils" "1.0.22" - "@ai-sdk/ui-utils" "0.0.49" + "@ai-sdk/ui-utils" "0.0.50" -"@ai-sdk/svelte@0.0.56": - version "0.0.56" - resolved "https://registry.yarnpkg.com/@ai-sdk/svelte/-/svelte-0.0.56.tgz#cf91c0678857c55d9eaf0c9b06cd232970d71488" - integrity sha512-EmBHGxVkmC6Ugc2O3tH6+F0udYKUhdlqokKAdO3zZihpNCj4qC5msyzqbhRqX0415tD1eJib5SX2Sva47CHmLA== +"@ai-sdk/svelte@0.0.57": + version "0.0.57" + resolved "https://registry.yarnpkg.com/@ai-sdk/svelte/-/svelte-0.0.57.tgz#82e97db343f2d5f8e50da055e6897e03f03c2ee6" + integrity sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw== dependencies: "@ai-sdk/provider-utils" "1.0.22" - "@ai-sdk/ui-utils" "0.0.49" + "@ai-sdk/ui-utils" "0.0.50" sswr "^2.1.0" -"@ai-sdk/ui-utils@0.0.49": - version "0.0.49" - resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-0.0.49.tgz#da29e61ef547da292ce81a22b1c7ee81f1968d3b" - integrity sha512-urg0KYrfJmfEBSva9d132YRxAVmdU12ISGVlOV7yJkL86NPaU15qcRRWpOJqmMl4SJYkyZGyL1Rw9/GtLVurKw== +"@ai-sdk/ui-utils@0.0.50": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz#f396d24b5ac1e7a8090684a6d8de47282d0bad96" + integrity sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw== dependencies: "@ai-sdk/provider" "0.0.26" "@ai-sdk/provider-utils" "1.0.22" @@ -81,13 +82,13 @@ secure-json-parse "^2.7.0" zod-to-json-schema "^3.23.3" -"@ai-sdk/vue@0.0.58": - version "0.0.58" - resolved "https://registry.yarnpkg.com/@ai-sdk/vue/-/vue-0.0.58.tgz#391da858e4683b0367fd2358dd9fb4e2f26fc037" - integrity sha512-8cuIekJV+jYz68Z+EDp8Df1WNiBEO1NOUGNCy+5gqIi+j382YjuhZfzC78zbzg0PndfF5JzcXhWPqmcc0loUQA== +"@ai-sdk/vue@0.0.59": + version "0.0.59" + resolved "https://registry.yarnpkg.com/@ai-sdk/vue/-/vue-0.0.59.tgz#29190415a123e631bfe7cf08f6454b73b5585714" + integrity sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw== dependencies: "@ai-sdk/provider-utils" "1.0.22" - "@ai-sdk/ui-utils" "0.0.49" + "@ai-sdk/ui-utils" "0.0.50" swrv "^1.0.4" "@alloc/quick-lru@^5.2.0": @@ -728,10 +729,10 @@ dependencies: "@types/pg" "8.11.6" -"@next/env@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.0.3-canary.2.tgz#dcd5416f860a053c05611cedf05ac415ee9fbf5d" - integrity sha512-+1Gbej9xUFJuXl8R0hQh7yGsfOOhf/U7CPpu9o980G5upph6iCPIVcn6GE4pnF7gK4mPn+paBMMtqjIl3XOdpA== +"@next/env@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.0.3-canary.6.tgz#72a2f2bb2157065f22407b41ca13d17ac1a34e18" + integrity sha512-NThzZD2oa1gWKZJ9U6V68KOt+vWBkYd/TDA0NoQnLDJzmeBFxzIwxRD3jLqWuiPyyS307NMn9sVfcpUbkBl5KQ== "@next/eslint-plugin-next@14.2.5": version "14.2.5" @@ -740,45 +741,45 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.3-canary.2.tgz#1fad0b5a99aee34844657d52272ebd8663294e87" - integrity sha512-gB0z5Src6KZ6/JqWM/CL/6kJUD8SKanrBlrsNUfOj3ZzQlP7aov6K63P7zUIKXuB7QK0rwfgLuncUHKm+PWsuA== - -"@next/swc-darwin-x64@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3-canary.2.tgz#8ed7df162137a2fd93822c6607a5f95a3c282042" - integrity sha512-R6xFPG7au0tMoXfCgxHTUAhKFAtbT3om2WaXA9QVDe9b2EkxPngPinmTTdEgMuv7R1AwitcpNhhOrn9U1vqTVQ== - -"@next/swc-linux-arm64-gnu@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3-canary.2.tgz#23e1d584f2684a8ff5115668b8ae65c5fa2ce41d" - integrity sha512-5fYl9TBVbUj/wXNiqjkz/WVmgVDYSi5L6KHQWRvK9UGlRAcbDLwKwBtBAm4PB4GgzPJ/aVHUdbXnEAQIP7g51A== - -"@next/swc-linux-arm64-musl@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3-canary.2.tgz#3670bd8bda35e304ca180d1d25e370b6faeb698e" - integrity sha512-n+gMZ6dtj0BD2y6dPzzy+8r5TIQPsYNzODfAw7nwor0kGp5Y6HJwrKmnDj3wDTEEh2107gq27T7g8BlcNXl0Nw== - -"@next/swc-linux-x64-gnu@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3-canary.2.tgz#70ffdc30e1192094e10cb1f316458248e7444d2c" - integrity sha512-PEpD1RinW/+IK/zLilM8dhinLhZXduY0+PYN7V5lKDbzQs5y0yp4vneXAtEt5oHGoi7bWA2arlVO5e2AWqRpFA== - -"@next/swc-linux-x64-musl@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3-canary.2.tgz#6681c4e8f43ae24a595913f8241d75c6bf495e9e" - integrity sha512-bCGz+q00/mzA3K4Jsb/kzH5ivFrHgXpZY+zWxC9sgfxObhvl9bx/MlPdoYI4FwOGuQC+mbgLUOHpNvXSswbrjw== - -"@next/swc-win32-arm64-msvc@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3-canary.2.tgz#9d5aa8e78d1067331bd5767052435e8fce7c36ad" - integrity sha512-W5MCeg0LVV3xxUoLt5fIV1d+M6LSLkp4vvg32ZTSkshf5lPvWG612U/8rWAoJacfYue8FaPTFOQ7Gg10E+Qhig== - -"@next/swc-win32-x64-msvc@15.0.3-canary.2": - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3-canary.2.tgz#8b9b0333c21d964b172ab04ab29e6a3be9a95ecb" - integrity sha512-o1RAgC6m5wY0TpDIMTw+V1wQp9gOC/+WqKBWE0YslF9B9jg7Tj3+/cOcs4t6T/cvm1S8ILtD0qUCZqkWSDtcrQ== +"@next/swc-darwin-arm64@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.3-canary.6.tgz#98fa23437217f28465a0563de088e614272c7d8d" + integrity sha512-hMlvQcmImBKLZYMxWsXtRiXK8JRirh6+0X409PJ+/LYE02S7FfZtvD4XlAB7QtTiAFMKVCVhxZE18HaEe8lbPw== + +"@next/swc-darwin-x64@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3-canary.6.tgz#39ed89e7db2f91ec2ddaf0436bf097e97987e475" + integrity sha512-rBdY3bBAW/cJX37NgArkaO99LK/V3HQza96ajsYZowbYRnKM1rCzj18PpwmmsdOsVbvHTTXabOqVE1lK1gLdyg== + +"@next/swc-linux-arm64-gnu@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3-canary.6.tgz#25ad1442c77e7e8491d2b9d2b6a9b9a92af1c5bc" + integrity sha512-fi1GXL8CRWMRTxjKGvc3761F6tq9NujdYTO0R59lnQMMbiU15deVB/o2JnxU/wBiXUN6sbxgpZZKMO0tDWBJAA== + +"@next/swc-linux-arm64-musl@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3-canary.6.tgz#d4b3c346efa0b217eb39fc54d6e16fd64aa8284e" + integrity sha512-PwSDme39d1iRTFWu+c8BOUzDDUnlGTnPPlTxIWN99IXu70pnX16qXuVrhAGB76yuHnv5lWdXL1FkXfLqwC04TQ== + +"@next/swc-linux-x64-gnu@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3-canary.6.tgz#1c3d12a746f7b6fc732a40cd1882bdec6a5ad3c0" + integrity sha512-0xEItGd2fwQCfXDxCtl14RJUF7hpcYJl+viTl9jjCEdzHPozTiw3PU0aQakhIsiCbDftk4zuCLrSL+Qodv5prA== + +"@next/swc-linux-x64-musl@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3-canary.6.tgz#623d0c2eba5f11bf752fdfa7b83714ff6391713e" + integrity sha512-IBlDcfEnMrmODeUZc3XkH/zyV+/vpXtuDim5yv7MXVnGwcrlQT835K93A1Feq4Y1VjdnosNoXp9ot2al2cVhXA== + +"@next/swc-win32-arm64-msvc@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3-canary.6.tgz#b3673f4774f62f22948aebae71a7dc708c672652" + integrity sha512-NwW/En47weQiDKJVilO+2zwVwZvbefLtxnzI47dIJCB3U841IQxANQAQrBY+sdjel9rMjhGJgbklWemaNquW4w== + +"@next/swc-win32-x64-msvc@15.0.3-canary.6": + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3-canary.6.tgz#62794d99bdd8ba36bbc7c568bf8e73c375c6d162" + integrity sha512-8ApgIZE3Lz8uOgm5L8N6DQJqd250Fl7mO14lI3iIQAO+S9mZu572NG5HIMC2J9RvBOPCCagLfksjNRgJrKOVPw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1459,18 +1460,18 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -ai@3.4.27: - version "3.4.27" - resolved "https://registry.yarnpkg.com/ai/-/ai-3.4.27.tgz#bb8f35420a7a3353104d861becc4381d7cc39d12" - integrity sha512-xM5EuCSnqnZ2+3VeIkghj5Tgu+SttOkUHsCBhZrspt7EqI1+kesEeLxJMk/9vLpb9jakF4Fd0+P7wirUPRWDRg== +ai@3.4.32: + version "3.4.32" + resolved "https://registry.yarnpkg.com/ai/-/ai-3.4.32.tgz#1fee3aa54b7633461b96eed61a2464a4b9b0f63a" + integrity sha512-d5Im7kWsjw1T/IFfYPH4KDLUn4W+XIZqip34q7wcyVBAKLcjGSbJh+5F7Ktc1MIzThHUPh/NUzbHlDDNhTlCgA== dependencies: "@ai-sdk/provider" "0.0.26" "@ai-sdk/provider-utils" "1.0.22" - "@ai-sdk/react" "0.0.68" - "@ai-sdk/solid" "0.0.53" - "@ai-sdk/svelte" "0.0.56" - "@ai-sdk/ui-utils" "0.0.49" - "@ai-sdk/vue" "0.0.58" + "@ai-sdk/react" "0.0.70" + "@ai-sdk/solid" "0.0.54" + "@ai-sdk/svelte" "0.0.57" + "@ai-sdk/ui-utils" "0.0.50" + "@ai-sdk/vue" "0.0.59" "@opentelemetry/api" "1.9.0" eventsource-parser "1.1.2" json-schema "^0.4.0" @@ -3925,12 +3926,12 @@ next-themes@^0.3.0: resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== -next@15.0.3-canary.2: - version "15.0.3-canary.2" - resolved "https://registry.yarnpkg.com/next/-/next-15.0.3-canary.2.tgz#e45845e52af8ec393ddd13650253799989b0cb69" - integrity sha512-vGutHxoPrZuZNVxD/HZRRyOY1X2+StTlbwzJA2Ck4lGofVsccdQjDuc89WAmg9vFYXjHf+mYqi8WYcXM5SJymw== +next@15.0.3-canary.6: + version "15.0.3-canary.6" + resolved "https://registry.yarnpkg.com/next/-/next-15.0.3-canary.6.tgz#cccb339d4ca52a1039c1685b3928e45012ed1625" + integrity sha512-1khncAd9gxT65AGaQhQ5fAKxzan5qRB2NdonAye7sCY2iG3Bg3pyza2TrmeSz2PwqhqURCj8J3apCLlDjokJnw== dependencies: - "@next/env" "15.0.3-canary.2" + "@next/env" "15.0.3-canary.6" "@swc/counter" "0.1.3" "@swc/helpers" "0.5.13" busboy "1.6.0" @@ -3938,14 +3939,14 @@ next@15.0.3-canary.2: postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.0.3-canary.2" - "@next/swc-darwin-x64" "15.0.3-canary.2" - "@next/swc-linux-arm64-gnu" "15.0.3-canary.2" - "@next/swc-linux-arm64-musl" "15.0.3-canary.2" - "@next/swc-linux-x64-gnu" "15.0.3-canary.2" - "@next/swc-linux-x64-musl" "15.0.3-canary.2" - "@next/swc-win32-arm64-msvc" "15.0.3-canary.2" - "@next/swc-win32-x64-msvc" "15.0.3-canary.2" + "@next/swc-darwin-arm64" "15.0.3-canary.6" + "@next/swc-darwin-x64" "15.0.3-canary.6" + "@next/swc-linux-arm64-gnu" "15.0.3-canary.6" + "@next/swc-linux-arm64-musl" "15.0.3-canary.6" + "@next/swc-linux-x64-gnu" "15.0.3-canary.6" + "@next/swc-linux-x64-musl" "15.0.3-canary.6" + "@next/swc-win32-arm64-msvc" "15.0.3-canary.6" + "@next/swc-win32-x64-msvc" "15.0.3-canary.6" sharp "^0.33.5" nextjs-toploader@^3.7.15: @@ -5116,6 +5117,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +throttleit@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4" + integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" From cf6562b01e32d3c757b462d7f59fa9b7fe6660a1 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 15:57:41 +0000 Subject: [PATCH 11/25] Integrate Zod validation with React Hook Form in configuration settings --- .../settings/configuration/index.tsx | 121 ++++++++++-------- package.json | 1 + yarn.lock | 5 + 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/app/(settings)/settings/configuration/index.tsx b/app/(settings)/settings/configuration/index.tsx index f95f8a793..8172e819d 100644 --- a/app/(settings)/settings/configuration/index.tsx +++ b/app/(settings)/settings/configuration/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeft, @@ -10,7 +11,9 @@ import { Settings, Zap, } from 'lucide-react'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { @@ -75,46 +78,41 @@ interface ConfigurationCarouselProps { onClose: () => void; } +const configurationSchema = z.object({ + processing: z.string().nonempty({ message: 'Processing is required' }), + vectorDB: z.string().nonempty({ message: 'Vector DB is required' }), + reranking: z.string().nonempty({ message: 'Reranking is required' }), + embedding: z.string().nonempty({ message: 'Embedding is required' }), + parameter1: z.string().nonempty({ message: 'Parameter1 is required' }), + parameter2: z.string().nonempty({ message: 'Parameter2 is required' }), + chunkSize: z.string().nonempty({ message: 'Chunk size is required' }), + overlap: z.string().nonempty({ message: 'Overlap is required' }), + method: z.string().nonempty({ message: 'Method is required' }), + topK: z.string().nonempty({ message: 'TopK is required' }), +}); + export function Configuration({ onClose }: ConfigurationCarouselProps) { const [currentStep, setCurrentStep] = useState(0); - const [stepSelections, setStepSelections] = useState< - Record> - >({}); const [visitedSteps, setVisitedSteps] = useState([0]); const [isSetupCompleted, setIsSetupCompleted] = useState(false); - useEffect(() => { - // Initialize stepSelections with empty objects for each step - setStepSelections( - steps.reduce( - (acc, _, index) => { - acc[index] = {}; - return acc; - }, - {} as Record> - ) - ); - }, []); - - const handleSelection = (step: number, key: string, value: string) => { - setStepSelections((prev) => ({ - ...prev, - [step]: { ...prev[step], [key]: value }, - })); - }; + const { control, handleSubmit, formState, getValues, trigger } = useForm({ + resolver: zodResolver(configurationSchema), + mode: 'onChange', + }); - const isStepComplete = (step: number) => { - const selections = stepSelections[step] || {}; - return Object.keys(steps[step].options).every((key) => selections[key]); - }; + const handleNextStep = async () => { + const fields = Object.keys(steps[currentStep].options); + const isValid = await trigger(fields); - const handleNextStep = () => { - if (currentStep < steps.length - 1) { - const nextStep = currentStep + 1; - setCurrentStep(nextStep); - setVisitedSteps((prev) => [...new Set([...prev, nextStep])]); - } else if (isStepComplete(currentStep)) { - setIsSetupCompleted(true); + if (isValid) { + if (currentStep < steps.length - 1) { + const nextStep = currentStep + 1; + setCurrentStep(nextStep); + setVisitedSteps((prev) => [...new Set([...prev, nextStep])]); + } else { + setIsSetupCompleted(true); + } } }; @@ -124,6 +122,12 @@ export function Configuration({ onClose }: ConfigurationCarouselProps) { } }; + const isStepComplete = (step: number) => { + const fields = Object.keys(steps[step].options); + const values = getValues(); + return fields.every((key) => values[key]); + }; + const allStepsCompleted = steps.every((_, index) => isStepComplete(index)); return ( @@ -200,23 +204,33 @@ export function Configuration({ onClose }: ConfigurationCarouselProps) { > {key.charAt(0).toUpperCase() + key.slice(1)} - + ( + + )} + /> + {formState.errors[key] && ( +

+ {formState.errors[key] && + String(formState.errors[key].message)} +

+ )} ) )} @@ -230,10 +244,7 @@ export function Configuration({ onClose }: ConfigurationCarouselProps) { Previous - - -
- -
+ currentStep={currentStep} + formMethods={formMethods} + handleNextStep={handleNextStep} + handlePreviousStep={handlePreviousStep} + /> diff --git a/app/(settings)/settings/configuration/step-form.tsx b/app/(settings)/settings/configuration/step-form.tsx new file mode 100644 index 000000000..7d9fb5908 --- /dev/null +++ b/app/(settings)/settings/configuration/step-form.tsx @@ -0,0 +1,113 @@ +// StepForm.tsx + +import { motion } from 'framer-motion'; +import { ArrowLeft, ArrowRight, Check } from 'lucide-react'; +import { Controller, UseFormReturn } from 'react-hook-form'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { steps } from './steps'; +import { ConfigurationFormData } from './validation'; + +interface StepFormProps { + currentStep: number; + formMethods: UseFormReturn; + handleNextStep: () => void; + handlePreviousStep: () => void; +} + +export const StepForm = ({ + currentStep, + formMethods, + handleNextStep, + handlePreviousStep, +}: StepFormProps) => { + const { control, formState } = formMethods; + return ( + + + + {steps[currentStep].title} + {steps[currentStep].description} + + + {Object.entries(steps[currentStep].options).map(([key, options]) => ( +
+ + ( + + )} + /> + {formState.errors[key as keyof ConfigurationFormData] && ( +

+ { + formState.errors[key as keyof ConfigurationFormData] + ?.message + } +

+ )} +
+ ))} +
+ + + + +
+
+ ); +}; diff --git a/app/(settings)/settings/configuration/stepper.tsx b/app/(settings)/settings/configuration/stepper.tsx new file mode 100644 index 000000000..1d000cb6d --- /dev/null +++ b/app/(settings)/settings/configuration/stepper.tsx @@ -0,0 +1,53 @@ +// Stepper.tsx + +import { motion } from 'framer-motion'; + +import { steps } from './steps'; + +interface StepperProps { + currentStep: number; + setCurrentStep: (step: number) => void; + isStepComplete: (step: number) => boolean; +} + +export const Stepper = ({ + currentStep, + setCurrentStep, + isStepComplete, +}: StepperProps) => { + return ( +
+ {steps.map((step, index) => { + const StepIcon = step.icon; + return ( +
+ setCurrentStep(index)} + > + + + {index < steps.length - 1 && ( + + )} +
+ ); + })} +
+ ); +}; diff --git a/app/(settings)/settings/configuration/steps.tsx b/app/(settings)/settings/configuration/steps.tsx new file mode 100644 index 000000000..8fe15da56 --- /dev/null +++ b/app/(settings)/settings/configuration/steps.tsx @@ -0,0 +1,45 @@ +// steps.ts + +import { Database, FileText, Settings, Zap } from 'lucide-react'; + +export const steps = [ + { + title: 'Set up your providers', + description: + 'Configure processing/chunking, vector DB, reranking, and embedding', + icon: Database, + options: { + processing: ['Option 1', 'Option 2', 'Option 3'], + vectorDB: ['Option A', 'Option B', 'Option C'], + reranking: ['Choice 1', 'Choice 2', 'Choice 3'], + embedding: ['Type 1', 'Type 2', 'Type 3'], + }, + }, + { + title: 'Configure your processing settings', + description: 'Set up the processing parameters for your pipeline', + icon: FileText, + options: { + parameter1: ['Low', 'Medium', 'High'], + parameter2: ['Fast', 'Balanced', 'Thorough'], + }, + }, + { + title: 'Configure your chunking settings', + description: 'Set up the chunking parameters for your pipeline', + icon: Settings, + options: { + chunkSize: ['Small', 'Medium', 'Large'], + overlap: ['None', 'Minimal', 'Moderate', 'Significant'], + }, + }, + { + title: 'Configure your retrieval settings', + description: 'Set up the retrieval parameters for your pipeline', + icon: Zap, + options: { + method: ['BM25', 'TF-IDF', 'Semantic'], + topK: ['5', '10', '20', '50'], + }, + }, +]; diff --git a/app/(settings)/settings/configuration/validation.tsx b/app/(settings)/settings/configuration/validation.tsx new file mode 100644 index 000000000..c35d36a0d --- /dev/null +++ b/app/(settings)/settings/configuration/validation.tsx @@ -0,0 +1,18 @@ +// validation.ts + +import { z } from 'zod'; + +export const configurationSchema = z.object({ + processing: z.string().nonempty({ message: 'Processing is required' }), + vectorDB: z.string().nonempty({ message: 'Vector DB is required' }), + reranking: z.string().nonempty({ message: 'Reranking is required' }), + embedding: z.string().nonempty({ message: 'Embedding is required' }), + parameter1: z.string().nonempty({ message: 'Parameter1 is required' }), + parameter2: z.string().nonempty({ message: 'Parameter2 is required' }), + chunkSize: z.string().nonempty({ message: 'Chunk size is required' }), + overlap: z.string().nonempty({ message: 'Overlap is required' }), + method: z.string().nonempty({ message: 'Method is required' }), + topK: z.string().nonempty({ message: 'TopK is required' }), +}); + +export type ConfigurationFormData = z.infer; diff --git a/app/(settings)/settings/knowledgebase/FileRow.tsx b/app/(settings)/settings/knowledgebase/FileRow.tsx deleted file mode 100644 index 70ce61077..000000000 --- a/app/(settings)/settings/knowledgebase/FileRow.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { motion } from 'framer-motion'; -import { CheckCircle, Clock, RefreshCw, Trash2, XCircle } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { TableCell } from '@/components/ui/table'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; - -interface File { - id: string; - name: string; - size: number; - type: string; - uploadDate: Date; - status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; - progress: number; -} - -interface FileRowProps { - file: File; - onProcessFile: (id: string) => void; - onDeleteFile: (id: string) => void; -} - -export function FileRow({ file, onProcessFile, onDeleteFile }: FileRowProps) { - const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }; - - const getStatusIcon = (status: File['status'], progress: number) => { - switch (status) { - case 'uploading': - case 'processing': - return ( -
- - - - -
- ); - case 'uploaded': - return ; - case 'processed': - return ; - case 'error': - return ; - default: - return null; - } - }; - - return ( - - {file.name} - {formatFileSize(file.size)} - {file.type} - {file.uploadDate.toLocaleString()} - - - - -
- {getStatusIcon(file.status, file.progress)} - {file.status} -
-
- - {file.status === 'uploading' || file.status === 'processing' - ? `${file.progress}% complete` - : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} - -
-
-
- -
- - -
-
-
- ); -} diff --git a/app/(settings)/settings/knowledgebase/FileTable.tsx b/app/(settings)/settings/knowledgebase/FileTable.tsx deleted file mode 100644 index 0168e5ea5..000000000 --- a/app/(settings)/settings/knowledgebase/FileTable.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { AnimatePresence } from 'framer-motion'; -import { ArrowUpDown } from 'lucide-react'; - -import { - Table, - TableBody, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; - -import { FileRow } from './FileRow'; - -interface File { - id: string; - name: string; - size: number; - type: string; - uploadDate: Date; - status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; - progress: number; -} - -interface FileTableProps { - files: File[]; - sortColumn: keyof File; - sortDirection: 'asc' | 'desc'; - onSort: (column: keyof File) => void; - onProcessFile: (id: string) => void; - onDeleteFile: (id: string) => void; -} - -export function FileTable({ - files, - onSort, - onProcessFile, - onDeleteFile, -}: FileTableProps) { - return ( - - - - onSort('name')} className="cursor-pointer"> - Name - - onSort('size')} className="cursor-pointer"> - Size - - onSort('type')} className="cursor-pointer"> - Type - - onSort('uploadDate')} - className="cursor-pointer" - > - Upload Date - - onSort('status')} - className="cursor-pointer" - > - Status - - Actions - - - - - {files.map((file) => ( - - ))} - - -
- ); -} diff --git a/app/(settings)/settings/knowledgebase/FileUploader.tsx b/app/(settings)/settings/knowledgebase/FileUploader.tsx deleted file mode 100644 index 428f25820..000000000 --- a/app/(settings)/settings/knowledgebase/FileUploader.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Upload } from 'lucide-react'; -import { useRef } from 'react'; - -import { Button } from '@/components/ui/button'; - -interface FileUploaderProps { - onFileUpload: (event: React.ChangeEvent) => void; - isUploading: boolean; -} - -export function FileUploader({ onFileUpload, isUploading }: FileUploaderProps) { - const fileInputRef = useRef(null); - - return ( - <> - - - - ); -} diff --git a/app/(settings)/settings/knowledgebase/SearchBar.tsx b/app/(settings)/settings/knowledgebase/SearchBar.tsx deleted file mode 100644 index aedc0f3dd..000000000 --- a/app/(settings)/settings/knowledgebase/SearchBar.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Input } from '@/components/ui/input'; - -interface SearchBarProps { - searchTerm: string; - onSearchTermChange: (term: string) => void; -} - -export function SearchBar({ searchTerm, onSearchTermChange }: SearchBarProps) { - return ( - onSearchTermChange(e.target.value)} - /> - ); -} diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index 4b0232dbe..0a13c93fb 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -1,6 +1,19 @@ -import { motion } from 'framer-motion'; -import { ArrowLeft } from 'lucide-react'; -import { useState, useEffect } from 'react'; +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { + Upload, + Trash2, + RefreshCw, + ArrowUpDown, + Search, + ArrowLeft, + CheckCircle, + XCircle, + Clock, +} from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { @@ -11,10 +24,22 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; - -import { FileTable } from './FileTable'; -import { FileUploader } from './FileUploader'; -import { SearchBar } from './SearchBar'; +import { Input } from '@/components/ui/input'; +import { Progress } from '@/components/ui/progress'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; interface File { id: string; @@ -26,11 +51,13 @@ interface File { progress: number; } -interface KnowledgebaseProps { +interface KnowledgebaseCarouselProps { onClose: () => void; } -export function Knowledgebase({ onClose }: KnowledgebaseProps) { +export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { + const router = useRouter(); + const fileInputRef = useRef(null); const [files, setFiles] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [sortColumn, setSortColumn] = useState('uploadDate'); @@ -148,6 +175,48 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getStatusIcon = (status: File['status'], progress: number) => { + switch (status) { + case 'uploading': + case 'processing': + return ( +
+ + + + +
+ ); + case 'uploaded': + return ; + case 'processed': + return ; + case 'error': + return ; + default: + return null; + } + }; + return (
- setSearchTerm(e.target.value)} />
- +
- + + + + + handleSort('name')} + className="cursor-pointer" + > + Name + + handleSort('size')} + className="cursor-pointer" + > + Size + + handleSort('type')} + className="cursor-pointer" + > + Type + + handleSort('uploadDate')} + className="cursor-pointer" + > + Upload Date{' '} + + + handleSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + + {filteredFiles.map((file) => ( + + {file.name} + {formatFileSize(file.size)} + {file.type} + {file.uploadDate.toLocaleString()} + + + + +
+ {getStatusIcon(file.status, file.progress)} + {file.status} +
+
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ +
+ + +
+
+
+ ))} +
+
+
{files.length} file(s) uploaded
From 7dc8bd37bf3a437635d5781b17d0e69f9ff72958 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 17:30:38 +0000 Subject: [PATCH 13/25] Refactor configuration settings to enhance validation and improve user experience --- .../settings/knowledgebase/index.tsx | 261 +++++++++++------- 1 file changed, 159 insertions(+), 102 deletions(-) diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index 0a13c93fb..e6c42e716 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -11,8 +11,9 @@ import { CheckCircle, XCircle, Clock, + ChevronLeft, + ChevronRight, } from 'lucide-react'; -import { useRouter } from 'next/navigation'; import { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; @@ -25,7 +26,6 @@ import { CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Progress } from '@/components/ui/progress'; import { Table, TableBody, @@ -51,12 +51,11 @@ interface File { progress: number; } -interface KnowledgebaseCarouselProps { +interface KnowledgebaseProps { onClose: () => void; } -export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { - const router = useRouter(); +export function Knowledgebase({ onClose }: KnowledgebaseProps) { const fileInputRef = useRef(null); const [files, setFiles] = useState([]); const [searchTerm, setSearchTerm] = useState(''); @@ -64,6 +63,8 @@ export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [isUploading, setIsUploading] = useState(false); const [canReturnToSettings, setCanReturnToSettings] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const filesPerPage = 5; useEffect(() => { const hasProcessedFile = files.some((file) => file.status === 'processed'); @@ -175,6 +176,12 @@ export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + const totalPages = Math.ceil(filteredFiles.length / filesPerPage); + const paginatedFiles = filteredFiles.slice( + (currentPage - 1) * filesPerPage, + currentPage * filesPerPage + ); + const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -239,6 +246,7 @@ export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) { placeholder="Search files..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} + className="w-full" />
@@ -267,106 +275,155 @@ export function Knowledgebase({ onClose }: KnowledgebaseCarouselProps) {
- - - - handleSort('name')} - className="cursor-pointer" - > - Name - - handleSort('size')} - className="cursor-pointer" - > - Size - - handleSort('type')} - className="cursor-pointer" - > - Type - - handleSort('uploadDate')} - className="cursor-pointer" - > - Upload Date{' '} - - - handleSort('status')} - className="cursor-pointer" - > - Status - - Actions - - - - - {filteredFiles.map((file) => ( - +
+ + + handleSort('name')} + className="cursor-pointer" + > + Name + + handleSort('size')} + className="cursor-pointer" + > + Size + + handleSort('type')} + className="cursor-pointer" + > + Type + + handleSort('uploadDate')} + className="cursor-pointer" > - {file.name} - {formatFileSize(file.size)} - {file.type} - {file.uploadDate.toLocaleString()} - - - - -
- {getStatusIcon(file.status, file.progress)} - {file.status} -
-
- - {file.status === 'uploading' || - file.status === 'processing' - ? `${file.progress}% complete` - : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} - -
-
-
- -
- - -
-
- - ))} - - -
+ Upload Date{' '} + + + handleSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + + {paginatedFiles.map((file) => ( + + + + + + + {file.name} + + + + + + {formatFileSize(file.size)} + + + {file.type} + + + {file.uploadDate.toLocaleString()} + + + + + +
+ {getStatusIcon(file.status, file.progress)} + + {file.status} + +
+
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ +
+ + +
+
+
+ ))} + {Array.from({ + length: Math.max(0, 5 - paginatedFiles.length), + }).map((_, index) => ( + + + + ))} +
+
+
+
- +
{files.length} file(s) uploaded
+
+ + + Page {currentPage} of {totalPages} + + +
From c675ce9de5a98a8498c55401b5e0af9b285f1e86 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 17:40:56 +0000 Subject: [PATCH 14/25] Enhance configuration settings with improved validation and user experience --- .../settings/knowledgebase/index.tsx | 252 +++++++++--------- 1 file changed, 123 insertions(+), 129 deletions(-) diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index e6c42e716..2346c41ac 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -26,6 +26,7 @@ import { CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; +import { Progress } from '@/components/ui/progress'; import { Table, TableBody, @@ -239,8 +240,8 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { -
-
+
+
-
+
0 && !canReturnToSettings} > Return to Settings @@ -275,136 +276,129 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) {
-
- - - - handleSort('name')} - className="cursor-pointer" +
+ + + handleSort('name')} + className="cursor-pointer" + > + Name + + handleSort('size')} + className="cursor-pointer hidden sm:table-cell" + > + Size + + handleSort('type')} + className="cursor-pointer hidden md:table-cell" + > + Type + + handleSort('uploadDate')} + className="cursor-pointer hidden lg:table-cell" + > + Upload Date{' '} + + + handleSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + + {paginatedFiles.map((file) => ( + - Name - - handleSort('size')} - className="cursor-pointer" - > - Size - - handleSort('type')} - className="cursor-pointer" - > - Type - - handleSort('uploadDate')} - className="cursor-pointer" - > - Upload Date{' '} - - - handleSort('status')} - className="cursor-pointer" - > - Status - - Actions - - - - - {paginatedFiles.map((file) => ( - - - - - - - {file.name} - - - - - - {formatFileSize(file.size)} - - - {file.type} - - - {file.uploadDate.toLocaleString()} - - - - - -
- {getStatusIcon(file.status, file.progress)} - - {file.status} - -
-
- - {file.status === 'uploading' || - file.status === 'processing' - ? `${file.progress}% complete` - : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} - -
-
-
- -
- - -
-
-
- ))} - {Array.from({ - length: Math.max(0, 5 - paginatedFiles.length), - }).map((_, index) => ( - - - - ))} -
-
-
-
+ + + + + {file.name} + + + {file.name} + + + + + + {formatFileSize(file.size)} + + + {file.type} + + + {file.uploadDate.toLocaleString()} + + + + + +
+ {getStatusIcon(file.status, file.progress)} + + {file.status} + +
+
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ +
+ + +
+
+ + ))} + + + - +
{files.length} file(s) uploaded
- ))} - + )) + ) : ( + + + No files available + + + )}
{files.length} file(s) uploaded
-
- - - Page {currentPage} of {totalPages} - - -
+ {totalPages > 0 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )}
From a2101c47c52c8ecc82d3a76deae02a874953267c Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 18:56:36 +0000 Subject: [PATCH 16/25] Add empty row handling in knowledgebase table for improved pagination display --- .../settings/knowledgebase/index.tsx | 182 +++++++++--------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index 916f88978..22791bc78 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -186,6 +186,8 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { currentPage * filesPerPage ); + const emptyRowsCount = Math.max(0, filesPerPage - paginatedFiles.length); + const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -317,99 +319,97 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { - {paginatedFiles.length > 0 ? ( - paginatedFiles.map((file) => ( - - - - - - {file.name} - - - {file.name} - - - - - - {formatFileSize(file.size)} - - - - - - {file.type.length > 10 - ? `${file.type.slice(0, 10)}…` - : file.type} - - - {file.type} - - - - - - {file.uploadDate.toLocaleString()} - - - - - -
- {getStatusIcon(file.status, file.progress)} - - {file.status} - -
-
- - {file.status === 'uploading' || - file.status === 'processing' - ? `${file.progress}% complete` - : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} - -
-
-
- -
- - -
-
-
- )) - ) : ( - - - No files available + {paginatedFiles.map((file) => ( + + + + + + {file.name} + + + {file.name} + + + + + + {formatFileSize(file.size)} + + + + + + {file.type.length > 10 + ? `${file.type.slice(0, 10)}…` + : file.type} + + + {file.type} + + + + + + {file.uploadDate.toLocaleString()} + + + + + +
+ {getStatusIcon(file.status, file.progress)} + + {file.status} + +
+
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ + +
+ + +
+
+ ))} + {Array.from({ length: emptyRowsCount }).map((_, index) => ( + +   - )} + ))}
From 22f275451d5a26894881bfc969bee54c79a6d429 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 19:03:37 +0000 Subject: [PATCH 17/25] Increase card width and tooltip display limits in knowledgebase for better visibility --- .../settings/knowledgebase/index.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index 22791bc78..c7f020182 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -237,7 +237,7 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { exit={{ opacity: 0 }} className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4 z-50" > - + File Management @@ -330,7 +330,7 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { - + {file.name} @@ -345,7 +345,7 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { - + {file.type.length > 10 ? `${file.type.slice(0, 10)}…` : file.type} @@ -362,14 +362,12 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { - -
- {getStatusIcon(file.status, file.progress)} - - {file.status} - -
-
+
+ {getStatusIcon(file.status, file.progress)} + + {file.status} + +
{file.status === 'uploading' || file.status === 'processing' From 577ec031bd1534453f2bba77af1407541f5b52b9 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Tue, 5 Nov 2024 19:13:52 +0000 Subject: [PATCH 18/25] Enhance empty row rendering in knowledgebase table with animations and improved structure --- .../settings/knowledgebase/index.tsx | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx index c7f020182..cac63fd4e 100644 --- a/app/(settings)/settings/knowledgebase/index.tsx +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -404,9 +404,54 @@ export function Knowledgebase({ onClose }: KnowledgebaseProps) { ))} {Array.from({ length: emptyRowsCount }).map((_, index) => ( - -   - + + + {/* Empty content, but same structure */} + Placeholder + + + Placeholder + + + Placeholder + + + Placeholder + + +
+ {/* Use invisible icons to maintain height */} + + Placeholder +
+
+ +
+ + +
+
+
))} From b2e8471f7eb348b41d9b19ae25459782bd70c876 Mon Sep 17 00:00:00 2001 From: Athrael Soju Date: Thu, 7 Nov 2024 23:36:48 +0000 Subject: [PATCH 19/25] Add Radix UI components and update configuration UI - Introduced new Switch and RadioGroup components using Radix UI. - Updated configuration button text to "Save & Return Home". - Added profile customization state management in settings page. - Updated package.json and yarn.lock to include new Radix UI dependencies. --- .../settings/configuration/index.tsx | 13 +- app/(settings)/settings/page.tsx | 16 ++ app/(settings)/settings/profile/index.tsx | 180 ++++++++++++++++++ components/ui/form.tsx | 178 +++++++++++++++++ components/ui/radio-group.tsx | 44 +++++ components/ui/switch.tsx | 29 +++ package.json | 2 + yarn.lock | 37 +++- 8 files changed, 491 insertions(+), 8 deletions(-) create mode 100644 app/(settings)/settings/profile/index.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/switch.tsx diff --git a/app/(settings)/settings/configuration/index.tsx b/app/(settings)/settings/configuration/index.tsx index 95dc1310d..2e32a2699 100644 --- a/app/(settings)/settings/configuration/index.tsx +++ b/app/(settings)/settings/configuration/index.tsx @@ -106,12 +106,17 @@ export function Configuration({ onClose }: ConfigurationCarouselProps) { /> - - {isSetupCompleted && allStepsCompleted && ( + + - )} +
diff --git a/app/(settings)/settings/page.tsx b/app/(settings)/settings/page.tsx index 63dd87247..3104f95cf 100644 --- a/app/(settings)/settings/page.tsx +++ b/app/(settings)/settings/page.tsx @@ -25,6 +25,7 @@ import { Progress } from '@/components/ui/progress'; import { Configuration } from './configuration'; import { Knowledgebase } from './knowledgebase'; +import { Profile } from './profile'; export default function Component() { const router = useRouter(); @@ -39,6 +40,8 @@ export default function Component() { useState(false); const [showKnowledgebaseCarousel, setShowKnowledgebaseCarousel] = useState(false); + const [showProfileCustomization, setShowProfileCustomization] = + useState(false); // Setup steps const setupSteps = [ @@ -90,6 +93,8 @@ export default function Component() { setShowConfigurationCarousel(true); } else if (route === '/knowledgebase') { setShowKnowledgebaseCarousel(true); + } else if (route === '/profile') { + setShowProfileCustomization(true); } else { router.push(route); } @@ -105,6 +110,11 @@ export default function Component() { setIsKnowledgebaseComplete(true); }; + const handleCloseProfileCustomization = () => { + setShowProfileCustomization(false); + setIsPersonalizationComplete(true); + }; + return (
@@ -212,6 +222,12 @@ export default function Component() { )} + + + {showProfileCustomization && ( + + )} +
); } diff --git a/app/(settings)/settings/profile/index.tsx b/app/(settings)/settings/profile/index.tsx new file mode 100644 index 000000000..ec0dffa8e --- /dev/null +++ b/app/(settings)/settings/profile/index.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { User, Calendar, Globe } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; + +interface ProfileProps { + onClose: () => void; +} + +export function Profile({ onClose }: ProfileProps) { + const router = useRouter(); + + // State variables for form fields + const [name, setName] = useState(''); + const [gender, setGender] = useState('male'); + const [dateOfBirth, setDateOfBirth] = useState(''); + const [language, setLanguage] = useState(''); + const [personalizedResponses, setPersonalizedResponses] = useState(false); + + return ( +
+ + + + Personalize Your AI Assistant + + + Customize your experience by providing some information about + yourself + + + +
+ +
+ + setName(e.target.value)} /> +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ + setDateOfBirth(e.target.value)} + /> +
+
+ + +
+ + +
+
+ + +
+
+ +
+ Enable personalized responses based on your preferences. +
+
+ +
+
+
+
+ + + + + +
+
+ ); +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 000000000..ce264aef2 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +