diff --git a/llama_stack/ui/README.md b/llama_stack/ui/README.md index 36eee4cff..e3e21bf0b 100644 --- a/llama_stack/ui/README.md +++ b/llama_stack/ui/README.md @@ -1,5 +1,8 @@ ## This is WIP. + +We use shadcdn/ui [Shadcn UI](https://ui.shadcn.com/) for the UI components. + ## Getting Started First, install dependencies: diff --git a/llama_stack/ui/app/layout.tsx b/llama_stack/ui/app/layout.tsx index a61fff38f..f029002dd 100644 --- a/llama_stack/ui/app/layout.tsx +++ b/llama_stack/ui/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; +import { ThemeProvider } from "@/components/ui/theme-provider"; import { Geist, Geist_Mono } from "next/font/google"; +import { ModeToggle } from "@/components/ui/mode-toggle"; import "./globals.css"; const geistSans = Geist({ @@ -17,21 +19,37 @@ export const metadata: Metadata = { description: "Llama Stack UI", }; -import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" -import { AppSidebar } from "@/components/app-sidebar" +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/app-sidebar"; export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - - -
- - {children} -
-
+ + + + + +
+ {/* Header with aligned elements */} +
+
+ +
+
+
+ +
+
+
{children}
+
+
+
- ) + ); } diff --git a/llama_stack/ui/app/logs/chat-completions/page.tsx b/llama_stack/ui/app/logs/chat-completions/page.tsx index 2a3ecf302..84cceb8b7 100644 --- a/llama_stack/ui/app/logs/chat-completions/page.tsx +++ b/llama_stack/ui/app/logs/chat-completions/page.tsx @@ -3,5 +3,5 @@ export default function ChatCompletions() {

Under Construction

- ) + ); } diff --git a/llama_stack/ui/app/logs/responses/page.tsx b/llama_stack/ui/app/logs/responses/page.tsx index 84607b647..cdc165d08 100644 --- a/llama_stack/ui/app/logs/responses/page.tsx +++ b/llama_stack/ui/app/logs/responses/page.tsx @@ -3,5 +3,5 @@ export default function Responses() {

Under Construction

- ) + ); } diff --git a/llama_stack/ui/app/page.tsx b/llama_stack/ui/app/page.tsx index fa24a4213..d1d781bdb 100644 --- a/llama_stack/ui/app/page.tsx +++ b/llama_stack/ui/app/page.tsx @@ -1,6 +1,6 @@ export default function Home() { return ( -
+

Welcome to Llama Stack!

); diff --git a/llama_stack/ui/components/app-sidebar.tsx b/llama_stack/ui/components/app-sidebar.tsx index b8dd070bb..3d541856f 100644 --- a/llama_stack/ui/components/app-sidebar.tsx +++ b/llama_stack/ui/components/app-sidebar.tsx @@ -1,5 +1,5 @@ -import { MessageSquareText, MessagesSquare } from "lucide-react" -import Link from "next/link" +import { MessageSquareText, MessagesSquare, MoveUpRight } from "lucide-react"; +import Link from "next/link"; import { Sidebar, @@ -11,8 +11,7 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarHeader, -} from "@/components/ui/sidebar" - +} from "@/components/ui/sidebar"; const logItems = [ { @@ -25,7 +24,12 @@ const logItems = [ url: "/logs/responses", icon: MessagesSquare, }, -] + { + title: "Documentation", + url: "https://llama-stack.readthedocs.io/en/latest/references/api_reference/index.html", + icon: MoveUpRight, + }, +]; export function AppSidebar() { return ( @@ -53,5 +57,5 @@ export function AppSidebar() { - ) + ); } diff --git a/llama_stack/ui/components/ui/button.tsx b/llama_stack/ui/components/ui/button.tsx index a2df8dce6..2adaf00da 100644 --- a/llama_stack/ui/components/ui/button.tsx +++ b/llama_stack/ui/components/ui/button.tsx @@ -1,8 +1,8 @@ -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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -32,8 +32,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -43,9 +43,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/llama_stack/ui/components/ui/dropdown-menu.tsx b/llama_stack/ui/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000..1fc1f4ee3 --- /dev/null +++ b/llama_stack/ui/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/llama_stack/ui/components/ui/input.tsx b/llama_stack/ui/components/ui/input.tsx index 03295ca6a..b1a060f50 100644 --- a/llama_stack/ui/components/ui/input.tsx +++ b/llama_stack/ui/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( @@ -11,11 +11,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className + className, )} {...props} /> - ) + ); } -export { Input } +export { Input }; diff --git a/llama_stack/ui/components/ui/mode-toggle.tsx b/llama_stack/ui/components/ui/mode-toggle.tsx new file mode 100644 index 000000000..92640161e --- /dev/null +++ b/llama_stack/ui/components/ui/mode-toggle.tsx @@ -0,0 +1,40 @@ +"use client"; + +import * as React from "react"; +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/llama_stack/ui/components/ui/separator.tsx b/llama_stack/ui/components/ui/separator.tsx index 67c73e5a5..06d1380a9 100644 --- a/llama_stack/ui/components/ui/separator.tsx +++ b/llama_stack/ui/components/ui/separator.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Separator({ className, @@ -18,11 +18,11 @@ function Separator({ orientation={orientation} className={cn( "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className + className, )} {...props} /> - ) + ); } -export { Separator } +export { Separator }; diff --git a/llama_stack/ui/components/ui/sheet.tsx b/llama_stack/ui/components/ui/sheet.tsx index 84649ad0f..d30779f4f 100644 --- a/llama_stack/ui/components/ui/sheet.tsx +++ b/llama_stack/ui/components/ui/sheet.tsx @@ -1,31 +1,31 @@ -"use client" +"use client"; -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Sheet({ ...props }: React.ComponentProps) { - return + return ; } function SheetTrigger({ ...props }: React.ComponentProps) { - return + return ; } function SheetClose({ ...props }: React.ComponentProps) { - return + return ; } function SheetPortal({ ...props }: React.ComponentProps) { - return + return ; } function SheetOverlay({ @@ -37,11 +37,11 @@ function SheetOverlay({ data-slot="sheet-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", - className + className, )} {...props} /> - ) + ); } function SheetContent({ @@ -50,7 +50,7 @@ function SheetContent({ side = "right", ...props }: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left" + side?: "top" | "right" | "bottom" | "left"; }) { return ( @@ -67,7 +67,7 @@ function SheetContent({ "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", side === "bottom" && "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", - className + className, )} {...props} > @@ -78,7 +78,7 @@ function SheetContent({ - ) + ); } function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex flex-col gap-1.5 p-4", className)} {...props} /> - ) + ); } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> - ) + ); } function SheetTitle({ @@ -111,7 +111,7 @@ function SheetTitle({ className={cn("text-foreground font-semibold", className)} {...props} /> - ) + ); } function SheetDescription({ @@ -124,7 +124,7 @@ function SheetDescription({ className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } export { @@ -136,4 +136,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +}; diff --git a/llama_stack/ui/components/ui/sidebar.tsx b/llama_stack/ui/components/ui/sidebar.tsx index 7ffbc078e..f8a0a3ed5 100644 --- a/llama_stack/ui/components/ui/sidebar.tsx +++ b/llama_stack/ui/components/ui/sidebar.tsx @@ -1,56 +1,56 @@ -"use client" +"use client"; -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { VariantProps, cva } from "class-variance-authority" -import { PanelLeftIcon } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; +import { PanelLeftIcon } 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 { 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, SheetDescription, SheetHeader, SheetTitle, -} from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" +} from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip" +} 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" +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 SidebarContextProps = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; -const SidebarContext = React.createContext(null) +const SidebarContext = React.createContext(null); function useSidebar() { - const context = React.useContext(SidebarContext) + const context = React.useContext(SidebarContext); if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") + throw new Error("useSidebar must be used within a SidebarProvider."); } - return context + return context; } function SidebarProvider({ @@ -62,36 +62,36 @@ function SidebarProvider({ children, ...props }: React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; }) { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) + 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 [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value + const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { - setOpenProp(openState) + setOpenProp(openState); } else { - _setOpen(openState) + _setOpen(openState); } // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, - [setOpenProp, open] - ) + [setOpenProp, open], + ); // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { @@ -100,18 +100,18 @@ function SidebarProvider({ event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { - event.preventDefault() - toggleSidebar() + event.preventDefault(); + toggleSidebar(); } - } + }; - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [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 state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo( () => ({ @@ -123,8 +123,8 @@ function SidebarProvider({ setOpenMobile, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); return ( @@ -140,7 +140,7 @@ function SidebarProvider({ } className={cn( "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", - className + className, )} {...props} > @@ -148,7 +148,7 @@ function SidebarProvider({
- ) + ); } function Sidebar({ @@ -159,11 +159,11 @@ function Sidebar({ children, ...props }: React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return ( @@ -171,13 +171,13 @@ function Sidebar({ data-slot="sidebar" className={cn( "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", - className + className, )} {...props} > {children} - ) + ); } if (isMobile) { @@ -202,7 +202,7 @@ function Sidebar({
{children}
- ) + ); } return ( @@ -223,7 +223,7 @@ function Sidebar({ "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" - : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", )} />
@@ -250,7 +250,7 @@ function Sidebar({
- ) + ); } function SidebarTrigger({ @@ -258,7 +258,7 @@ function SidebarTrigger({ onClick, ...props }: React.ComponentProps) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return ( - ) + ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (