import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import CopilotChatView, { CopilotChatViewProps, WelcomeScreenProps, } from "./CopilotChatView"; import { CopilotChatConfigurationProvider, useCopilotChatConfiguration, } from "../../providers/CopilotChatConfigurationProvider"; import CopilotChatToggleButton from "./CopilotChatToggleButton"; import { cn } from "../../lib/utils"; import { CopilotModalHeader } from "./CopilotModalHeader"; import { renderSlot, SlotValue } from "../../lib/slots"; const DEFAULT_SIDEBAR_WIDTH = 480; const SIDEBAR_TRANSITION_MS = 260; export type CopilotSidebarViewProps = CopilotChatViewProps & { header?: SlotValue; toggleButton?: SlotValue; width?: number | string; defaultOpen?: boolean; }; export function CopilotSidebarView({ header, toggleButton, width, defaultOpen = true, ...props }: CopilotSidebarViewProps) { return ( ); } function CopilotSidebarViewInternal({ header, toggleButton, width, ...props }: Omit) { const configuration = useCopilotChatConfiguration(); const isSidebarOpen = configuration?.isModalOpen ?? false; const sidebarRef = useRef(null); const [sidebarWidth, setSidebarWidth] = useState( width ?? DEFAULT_SIDEBAR_WIDTH, ); // Helper to convert width to CSS value const widthToCss = (w: number | string): string => { return typeof w === "number" ? `${w}px` : w; }; // Helper to extract numeric value for body margin (only works with px values) const widthToMargin = (w: number | string): string => { if (typeof w === "number") { return `${w}px`; } // For string values, use as-is (assumes valid CSS unit) return w; }; useEffect(() => { // If width is explicitly provided, don't measure if (width !== undefined) { return; } if (typeof window === "undefined") { return; } const element = sidebarRef.current; if (!element) { return; } const updateWidth = () => { const rect = element.getBoundingClientRect(); if (rect.width > 0) { setSidebarWidth(rect.width); } }; updateWidth(); if (typeof ResizeObserver !== "undefined") { const observer = new ResizeObserver(() => updateWidth()); observer.observe(element); return () => observer.disconnect(); } window.addEventListener("resize", updateWidth); return () => window.removeEventListener("resize", updateWidth); }, [width]); // Manage body margin for sidebar docking (desktop only). // useLayoutEffect runs before paint, so defaultOpen={true} never causes a // visible layout shift — the margin is already applied on the first frame. const hasMounted = useRef(false); useLayoutEffect(() => { if ( typeof window === "undefined" || typeof window.matchMedia !== "function" ) return; if (!window.matchMedia("(min-width: 768px)").matches) return; if (isSidebarOpen) { if (hasMounted.current) { document.body.style.transition = `margin-inline-end ${SIDEBAR_TRANSITION_MS}ms ease`; } document.body.style.marginInlineEnd = widthToMargin(sidebarWidth); } else if (hasMounted.current) { document.body.style.transition = `margin-inline-end ${SIDEBAR_TRANSITION_MS}ms ease`; document.body.style.marginInlineEnd = ""; } hasMounted.current = true; return () => { document.body.style.marginInlineEnd = ""; document.body.style.transition = ""; }; }, [isSidebarOpen, sidebarWidth]); const headerElement = renderSlot(header, CopilotModalHeader, {}); const toggleButtonElement = renderSlot( toggleButton, CopilotChatToggleButton, {}, ); return ( <> {toggleButtonElement} ); } CopilotSidebarView.displayName = "CopilotSidebarView"; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace CopilotSidebarView { /** * Sidebar-specific welcome screen layout: * - Suggestions at the top * - Welcome message in the middle * - Input fixed at the bottom (like normal chat) */ export const WelcomeScreen: React.FC = ({ welcomeMessage, input, suggestionView, className, children, ...props }) => { // Render the welcomeMessage slot internally const BoundWelcomeMessage = renderSlot( welcomeMessage, CopilotChatView.WelcomeMessage, {}, ); if (children) { return (
{children({ welcomeMessage: BoundWelcomeMessage, input, suggestionView, className, ...props, })}
); } return (
{/* Welcome message - centered vertically */}
{BoundWelcomeMessage}
{/* Suggestions and input at bottom */}
{/* Suggestions above input */}
{suggestionView}
{input}
); }; } export default CopilotSidebarView;