// SPDX-License-Identifier: MIT // Copyright contributors to the openassistant project import { Resizable } from 're-resizable'; import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import React from 'react'; // Throttle utility function for resize handlers function useThrottledResizeCallback( callback: ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement, delta: { width: number; height: number } ) => void, delay: number ) { const lastCall = useRef(0); const timeoutRef = useRef(); return useCallback( ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement, delta: { width: number; height: number } ) => { const now = Date.now(); if (now - lastCall.current >= delay) { lastCall.current = now; callback(e, direction, refElement, delta); } else { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout( () => { lastCall.current = Date.now(); callback(e, direction, refElement, delta); }, delay - (now - lastCall.current) ); } }, [callback, delay] ); } // Child-size-driven container: Child determines initial size, user can resize to constrain export function ResizableContainerChildDriven({ children, isHovered, handlePosition = 'bottomRight', throttleMs = 16, onSizeChange, }: { children: JSX.Element; isHovered?: boolean; handlePosition?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft'; throttleMs?: number; onSizeChange?: (width: number, height: number) => void; }) { const [containerSize, setContainerSize] = useState<{ width: number; height: number; } | null>(null); const [forceUpdate, setForceUpdate] = useState(0); const childRef = useRef(null); const resizeObserverRef = useRef(); // Observe child's natural size changes useEffect(() => { if (childRef.current) { resizeObserverRef.current = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; if (width > 0 && height > 0) { setContainerSize({ width, height }); onSizeChange?.(width, height); } } }); resizeObserverRef.current.observe(childRef.current); return () => { resizeObserverRef.current?.disconnect(); }; } }, [onSizeChange]); const handleResize = useCallback( ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement ) => { const newSize = { width: refElement.offsetWidth, height: refElement.offsetHeight, }; setContainerSize(newSize); setForceUpdate((prev) => prev + 1); onSizeChange?.(newSize.width, newSize.height); }, [onSizeChange] ); const throttledResize = useThrottledResizeCallback(handleResize, throttleMs); // Always pass current dimensions if available (not just after user resize) const childProps = useMemo( () => ({ ...(containerSize && { width: containerSize.width, height: containerSize.height, }), ...children.props, }), // eslint-disable-next-line react-hooks/exhaustive-deps [containerSize, children.props, forceUpdate] // Add forceUpdate to dependencies ); const childrenWithProps = useMemo( () => React.cloneElement(children, childProps), [children, childProps] ); return (
) : null, }} >
{childrenWithProps}
); } // Alternative implementation using render prop pattern (more performant) export function ResizablePlotContainerRenderProp({ children, isHovered, handlePosition = 'bottomRight', defaultWidth = 0, defaultHeight = 0, throttleMs = 16, // ~60fps }: { children: (props: { width: number; height: number }) => JSX.Element; defaultWidth?: number; defaultHeight?: number; isHovered?: boolean; handlePosition?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft'; throttleMs?: number; }) { const [width, setWidth] = useState(defaultWidth); const [height, setHeight] = useState(defaultHeight); const handleResize = useCallback( ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement ) => { const newSize = { width: refElement.offsetWidth, height: refElement.offsetHeight, }; setWidth(newSize.width); setHeight(newSize.height); }, [] ); const throttledResize = useThrottledResizeCallback(handleResize, throttleMs); const dimensionProps = useMemo(() => ({ width, height }), [width, height]); return (
) : null, }} > {children(dimensionProps)} ); } export function ResizablePlotContainer({ children, isHovered, handlePosition = 'bottomRight', defaultWidth = 0, defaultHeight = 0, throttleMs = 16, // ~60fps }: { children: JSX.Element; defaultWidth?: number; defaultHeight?: number; isHovered?: boolean; handlePosition?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft'; throttleMs?: number; }) { const [width, setWidth] = useState(defaultWidth); const [height, setHeight] = useState(defaultHeight); const resizableRef = useRef(null); // Get initial dimensions after mount useEffect(() => { if (resizableRef.current && (width === 0 || height === 0)) { const element = resizableRef.current.resizable; if (element) { const rect = element.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { setWidth(rect.width); setHeight(rect.height); } } } }, [width, height]); const handleResize = useCallback( ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement ) => { const newSize = { width: refElement.offsetWidth, height: refElement.offsetHeight, }; setWidth(newSize.width); setHeight(newSize.height); }, [] ); const throttledResize = useThrottledResizeCallback(handleResize, throttleMs); // Memoize the props object to prevent unnecessary re-renders const childProps = useMemo( () => ({ width, height, ...children.props, // Preserve existing props }), [width, height, children.props] ); // Memoize the cloned element to prevent unnecessary re-renders const childrenWithProps = useMemo( () => React.cloneElement(children, childProps), [children, childProps] ); return (
) : null, }} > {childrenWithProps}
); } // Tool call container: Optimized for tool call components with hover-based resize export function ResizableToolCallContainer({ children, defaultWidth = 800, defaultHeight = 400, minWidth = 300, minHeight = 200, maxWidth, maxHeight, throttleMs = 16, onSizeChange, }: { children: JSX.Element; defaultWidth?: number; defaultHeight?: number; minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number; throttleMs?: number; onSizeChange?: (width: number, height: number) => void; }) { const [containerSize, setContainerSize] = useState<{ width: number; height: number; }>({ width: defaultWidth, height: defaultHeight, }); const [isHovered, setIsHovered] = useState(false); const [forceUpdate, setForceUpdate] = useState(0); const handleResize = useCallback( ( e: MouseEvent | TouchEvent, direction: string, refElement: HTMLElement ) => { const newSize = { width: refElement.offsetWidth, height: refElement.offsetHeight, }; setContainerSize(newSize); setForceUpdate((prev) => prev + 1); onSizeChange?.(newSize.width, newSize.height); }, [onSizeChange] ); const throttledResize = useThrottledResizeCallback(handleResize, throttleMs); const childProps = useMemo( () => ({ width: containerSize.width, height: containerSize.height, ...children.props, }), // eslint-disable-next-line react-hooks/exhaustive-deps [containerSize, children.props, forceUpdate] ); const childrenWithProps = useMemo( () => React.cloneElement(children, childProps), [children, childProps] ); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} >
) : undefined, }} style={{ border: isHovered ? '1px solid #e5e7eb' : '1px solid transparent', borderRadius: '4px', transition: 'border-color 0.2s ease', }} > {childrenWithProps} ); }