AI Sidebar

Panneau d'assistant IA coulissant avec fournisseur, emplacements en-tete / contenu / pied de page et un declencheur autonome. Installez AI Sidebar depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Quand utiliser ceci dans une application IA

Add AI Sidebar when you want a co-pilot alongside an existing product UI rather than a full-page chat. The provider + trigger pattern lets any screen open the assistant in context.

Parcourir tous les composants pour agents IA

Panneau d'assistant IA coulissant avec fournisseur, emplacements en-tete / contenu / pied de page et un declencheur autonome. Fait partie de la famille ai dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez AI Sidebar a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/ai-sidebar.json

Source

"use client"; import { type ComponentPropsWithoutRef, createContext, type ReactNode, use, useCallback, useMemo, useState, } from "react"; import { Bot, MessageSquarePlus, X } from "lucide-react"; import { useEscapeKey } from "../../lib/use-escape-key"; import { cn } from "../../lib/utils"; import { Button } from "../button/button"; const DEFAULT_WIDTH = 400; const MIN_WIDTH = 280; const MAX_WIDTH = 720; /** * Side of the viewport the sidebar attaches to. * * @public */ export type AISidebarPosition = "left" | "right"; /** * Localizable strings for {@link AISidebar} subcomponents. * * @public */ export type AISidebarLabels = { /** Aria-label for the close control. Defaults to `"Close assistant"`. */ close?: string; /** Default heading text for {@link AISidebarTitle}. */ defaultTitle?: string; /** Aria-label for the open trigger. Defaults to `"Open AI assistant"`. */ open?: string; }; const DEFAULT_LABELS = { close: "Close assistant", defaultTitle: "AI Assistant", open: "Open AI assistant", } as const satisfies Required<AISidebarLabels>; type AISidebarContextValue = { close: () => void; labels: Required<AISidebarLabels>; open: () => void; openState: boolean; position: AISidebarPosition; setOpen: (next: boolean) => void; toggle: () => void; width: number; }; const NO_OP = (): void => { return; }; const DEFAULT_CONTEXT: AISidebarContextValue = { close: NO_OP, labels: DEFAULT_LABELS, open: NO_OP, openState: false, position: "right", setOpen: NO_OP, toggle: NO_OP, width: DEFAULT_WIDTH, }; const AISidebarContext = createContext<AISidebarContextValue>(DEFAULT_CONTEXT); /** * Hook for reading sidebar state from anywhere inside an * {@link AISidebarProvider}. * * @public */ export function useAISidebar(): AISidebarContextValue { return use(AISidebarContext); } /** * Props for {@link AISidebarProvider}. * * @public */ export type AISidebarProviderProps = { children?: ReactNode; /** Initial open state when uncontrolled. Defaults to `false`. */ defaultOpen?: boolean; /** Initial position. Defaults to `"right"`. */ defaultPosition?: AISidebarPosition; /** Initial width in px. Defaults to `400`. */ defaultWidth?: number; /** Localizable strings. */ labels?: AISidebarLabels; /** Fires when the open state changes (controlled or uncontrolled). */ onOpenChange?: (open: boolean) => void; /** Controlled open state. */ open?: boolean; }; function clampWidth(value: number): number { return Math.min(Math.max(value, MIN_WIDTH), MAX_WIDTH); } /** * Provider for the AI sidebar context. Wrap your app shell with this so * {@link AISidebar}, {@link AISidebarTrigger}, and {@link useAISidebar} * share the same state. * * @public */ export function AISidebarProvider({ children, defaultOpen = false, defaultPosition = "right", defaultWidth = DEFAULT_WIDTH, labels, onOpenChange, open: controlledOpen, }: AISidebarProviderProps): ReactNode { const resolvedLabels = useMemo( () => ({ ...DEFAULT_LABELS, ...labels }), [labels], ); const [uncontrolled, setUncontrolled] = useState(defaultOpen); const isControlled = controlledOpen !== undefined; const openState = isControlled ? controlledOpen : uncontrolled; const setOpen = useCallback( (next: boolean) => { if (!isControlled) setUncontrolled(next); onOpenChange?.(next); }, [isControlled, onOpenChange], ); const open = useCallback(() => { setOpen(true); }, [setOpen]); const close = useCallback(() => { setOpen(false); }, [setOpen]); const toggle = useCallback(() => { setOpen(!openState); }, [openState, setOpen]); const value = useMemo<AISidebarContextValue>( () => ({ close, labels: resolvedLabels, open, openState, position: defaultPosition, setOpen, toggle, width: clampWidth(defaultWidth), }), [ close, defaultPosition, defaultWidth, open, openState, resolvedLabels, setOpen, toggle, ], ); return ( <AISidebarContext.Provider value={value}> {children} </AISidebarContext.Provider> ); } /** * Props for {@link AISidebar}. * * @public */ export type AISidebarProps = { /** When true, pressing Escape closes the sidebar. Defaults to `true`. */ closeOnEscape?: boolean; } & ComponentPropsWithoutRef<"aside">; /** * Slide-out AI assistant panel anchored to the left or right edge. Renders * an `<aside role="complementary">` so screen readers announce it as a * complementary region. Sets `aria-hidden` on close so its content is * skipped by assistive tech. * * @example * ```tsx * <AISidebarProvider defaultOpen={false}> * <AISidebar> * <AISidebarHeader> * <AISidebarTitle>AI Assistant</AISidebarTitle> * <AISidebarClose /> * </AISidebarHeader> * <AISidebarContent>{messages}</AISidebarContent> * <AISidebarFooter>{composer}</AISidebarFooter> * </AISidebar> * <AISidebarTrigger /> * <main>{children}</main> * </AISidebarProvider> * ``` * * @public */ export const AISidebar = ({ ref, ...props }: AISidebarProps & { ref?: React.Ref<HTMLElement> }) => { const { children, className, closeOnEscape = true, ...rest } = props; const { close, labels, openState, position, width } = useAISidebar(); useEscapeKey(close, { enabled: closeOnEscape && openState, target: "document", }); return ( <aside aria-hidden={!openState} aria-label={labels.defaultTitle} className={cn( "fixed top-0 z-40 flex h-full flex-col border-border bg-background shadow-lg transition-transform duration-200 ease-out", position === "right" ? "right-0 border-l" : "left-0 border-r", openState ? "translate-x-0" : position === "right" ? "translate-x-full" : "-translate-x-full", "max-w-full", className, )} data-state={openState ? "open" : "closed"} inert={!openState} ref={ref} style={{ width: `${width.toString()}px` }} {...rest} > {children} </aside> ); }; AISidebar.displayName = "AISidebar"; /** * Header slot for an {@link AISidebar}. Use to host the title, model * selector, and the close control. * * @public */ export const AISidebarHeader = ({ children, className, ref, ...rest }: ComponentPropsWithoutRef<"header"> & { ref?: React.Ref<HTMLElement> }) => ( <header className={cn( "flex items-center gap-2 border-b border-border px-4 py-3", className, )} ref={ref} {...rest} > {children} </header> ); AISidebarHeader.displayName = "AISidebarHeader"; /** * Title slot for {@link AISidebarHeader}. Defaults to the localized title * from the provider whenever the consumer omits children. * * @public */ export const AISidebarTitle = ({ children, className, ref, ...rest }: ComponentPropsWithoutRef<"h2"> & { ref?: React.Ref<HTMLHeadingElement>; }) => { const { labels } = useAISidebar(); return ( <h2 className={cn( "flex flex-1 items-center gap-2 text-sm font-semibold tracking-tight text-foreground", className, )} ref={ref} {...rest} > <Bot aria-hidden="true" className="size-4 text-muted-foreground" /> {children ?? labels.defaultTitle} </h2> ); }; AISidebarTitle.displayName = "AISidebarTitle"; /** * Close-button slot for {@link AISidebarHeader}. * * @public */ export const AISidebarClose = ({ className, onClick, ref, ...rest }: Omit<ComponentPropsWithoutRef<"button">, "type"> & { ref?: React.Ref<HTMLButtonElement>; }) => { const { close, labels } = useAISidebar(); const handleCloseSidebar = useCallback( (event: React.MouseEvent<HTMLButtonElement>) => { onClick?.(event); if (event.defaultPrevented) return; close(); }, [close, onClick], ); return ( <Button aria-label={labels.close} className={cn("size-8", className)} onClick={handleCloseSidebar} ref={ref} size="icon" type="button" variant="ghost" {...rest} > <X aria-hidden="true" className="size-4" /> </Button> ); }; AISidebarClose.displayName = "AISidebarClose"; /** * Scrollable middle section of {@link AISidebar}. * * @public */ export const AISidebarContent = ({ children, className, ref, ...rest }: ComponentPropsWithoutRef<"div"> & { ref?: React.Ref<HTMLDivElement> }) => ( <div className={cn("flex flex-1 flex-col gap-2 overflow-y-auto p-4", className)} ref={ref} {...rest} > {children} </div> ); AISidebarContent.displayName = "AISidebarContent"; /** * Bottom slot of {@link AISidebar}, typically the chat composer. * * @public */ export const AISidebarFooter = ({ children, className, ref, ...rest }: ComponentPropsWithoutRef<"footer"> & { ref?: React.Ref<HTMLElement> }) => ( <footer className={cn("border-t border-border bg-background px-4 py-3", className)} ref={ref} {...rest} > {children} </footer> ); AISidebarFooter.displayName = "AISidebarFooter"; /** * Props for {@link AISidebarTrigger}. * * @public */ export type AISidebarTriggerProps = Omit< ComponentPropsWithoutRef<"button">, "type" >; /** * Standalone control that opens the sidebar. Place anywhere inside an * {@link AISidebarProvider}. Falls back to the default icon + label when * the consumer omits children. * * @public */ export const AISidebarTrigger = ({ children, className, onClick, ref, ...rest }: AISidebarTriggerProps & { ref?: React.Ref<HTMLButtonElement> }) => { const { labels, openState, toggle } = useAISidebar(); const handleToggleSidebar = useCallback( (event: React.MouseEvent<HTMLButtonElement>) => { onClick?.(event); if (event.defaultPrevented) return; toggle(); }, [onClick, toggle], ); return ( <Button aria-expanded={openState} aria-label={children ? undefined : labels.open} className={cn(className)} data-state={openState ? "open" : "closed"} onClick={handleToggleSidebar} ref={ref} size={children ? "sm" : "icon"} type="button" variant="outline" {...rest} > {children ?? <MessageSquarePlus aria-hidden="true" className="size-4" />} </Button> ); }; AISidebarTrigger.displayName = "AISidebarTrigger";

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0
  • lucide-react