"use client" /** * AskLeoSidebar — app-wide right rail for the AI assistant. * Mirrors the left sidebar behavior: slides in/out with a toggle button. * Lives in the (app) layout so it persists across all pages. * * The conversation itself is `AskLeoThreadBody`, shared with the floating * window (`components/ask-leo-window.tsx`); this file owns only the docked * shell and its header. */ import * as React from "react" import { useLocation } from "react-router" import { useProduct } from "@/contexts/product-context" import { productSlug } from "@/stores/app-store" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { useSidebar } from "@/components/ui/sidebar" import { ASK_LEO_PANEL_WIDTH_DEFAULT, ASK_LEO_PANEL_WIDTH_KEY, NestedSecondaryPanelShell, SECONDARY_PANEL_WIDTH_MAX, SECONDARY_PANEL_WIDTH_MIN, clampSecondaryPanelWidth, } from "@/components/templates/nested-secondary-panel-shell" import { ResizableHandle, ResizablePanel, } from "@/components/ui/resizable" import { useShellRailWidth } from "@exxatdesignux/ui/hooks/use-shell-rail-width" import { AskLeoLauncher } from "@exxatdesignux/ui/components/shell/ask-leo-launcher" import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom" import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-context" import { AskLeoIdentity, AskLeoNewChatButton, AskLeoThreadBody, } from "@/components/ask-leo-thread-body" import { AskLeoViewToggle } from "@/components/ask-leo-view-toggle" import { useLeoAmbience } from "@/components/leo-ambience-context" import { LeoIcon } from "@/components/ui/leo-icon" import { focusAskLeoLauncher } from "@/lib/ask-leo-focus" function isLeoLandingPath(pathname: string, leoHref: string) { return pathname === leoHref || pathname.startsWith(`${leoHref}/`) } /** * Right-rail Ask Leo — same shell as {@link NestedSecondaryPanelShell} / library secondary nav. * Sits beside `SidebarInset` in the app row (not inside the main inset card). */ export function AskLeoSidebar() { const { open, setOpen } = useAskLeo() const { setSettingsOpen } = useLeoAmbience() const { isMobile } = useSidebar() const reflowZoom = useSidebarReflowZoom() const navFlyout = isMobile || reflowZoom const { pathname } = useLocation() const { product } = useProduct() const leoHref = `/${productSlug(product)}/leo` const isOnLeoLanding = isLeoLandingPath(pathname, leoHref) // Owns the persisted width and the panel's size props. See the hook for why a // pixel `defaultSize` alone used to leave the rail wider on every load. const rail = useShellRailWidth({ storageKey: ASK_LEO_PANEL_WIDTH_KEY, defaultWidth: ASK_LEO_PANEL_WIDTH_DEFAULT, minWidth: SECONDARY_PANEL_WIDTH_MIN, maxWidth: SECONDARY_PANEL_WIDTH_MAX, clamp: clampSecondaryPanelWidth, // v3 — drop widths the percentage conversion inflated before the rail // re-asserted its pixel size on mount. version: 3, }) // Closing takes the focused button off the page with it, which drops focus to // the document root. Hand it back to the toggle that reopens the panel. const closePanel = React.useCallback(() => { setOpen(false) focusAskLeoLauncher() }, [setOpen]) // Primary-nav collapse lives in `AskLeoShell` so switching panel → window // restores the hub width (window must not leave the sidebar collapsed). if (isOnLeoLanding) { return null } const overlay: false | "mobile" | "desktop" = navFlyout ? isMobile ? "mobile" : "desktop" : false const dockedResizable = open && overlay === false const shellBody = (