import { Link, useRouterState } from '@tanstack/react-router' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import { IconLayoutSidebar, IconDots, IconCirclePlusFilled, IconSettingsFilled, IconStar, IconMessageFilled, IconAppsFilled, IconX, IconSearch, IconClipboardSmileFilled, } from '@tabler/icons-react' import { route } from '@/constants/routes' import ThreadList from './ThreadList' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useThreads } from '@/hooks/useThreads' import { useTranslation } from '@/i18n/react-i18next-compat' import { useMemo, useState, useEffect, useRef } from 'react' import { toast } from 'sonner' import { DownloadManagement } from '@/containers/DownloadManegement' import { useSmallScreen } from '@/hooks/useMediaQuery' import { useClickOutside } from '@/hooks/useClickOutside' import { useDownloadStore } from '@/hooks/useDownloadStore' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' import { DeleteAllThreadsDialog } from '@/containers/dialogs' const mainMenus = [ { title: 'common:newChat', icon: IconCirclePlusFilled, route: route.home, isEnabled: true, }, { title: 'common:assistants', icon: IconClipboardSmileFilled, route: route.assistant, isEnabled: true, }, { title: 'common:hub', icon: IconAppsFilled, route: route.hub.index, isEnabled: PlatformFeatures[PlatformFeature.MODEL_HUB], }, { title: 'common:settings', icon: IconSettingsFilled, route: route.settings.general, isEnabled: true, }, ] const LeftPanel = () => { const { open, setLeftPanel } = useLeftPanel() const { t } = useTranslation() const [searchTerm, setSearchTerm] = useState('') const isSmallScreen = useSmallScreen() const prevScreenSizeRef = useRef(null) const isInitialMountRef = useRef(true) const panelRef = useRef(null) const searchContainerRef = useRef(null) const searchContainerMacRef = useRef(null) // Determine if we're in a resizable context (large screen with panel open) const isResizableContext = !isSmallScreen && open // Use click outside hook for panel with debugging useClickOutside( () => { if (isSmallScreen && open) { setLeftPanel(false) } }, null, [ panelRef.current, searchContainerRef.current, searchContainerMacRef.current, ] ) // Auto-collapse panel only when window is resized useEffect(() => { const handleResize = () => { const currentIsSmallScreen = window.innerWidth <= 768 // Skip on initial mount if (isInitialMountRef.current) { isInitialMountRef.current = false prevScreenSizeRef.current = currentIsSmallScreen return } // Only trigger if the screen size actually changed if ( prevScreenSizeRef.current !== null && prevScreenSizeRef.current !== currentIsSmallScreen ) { if (currentIsSmallScreen) { setLeftPanel(false) } else { setLeftPanel(true) } prevScreenSizeRef.current = currentIsSmallScreen } } // Add resize listener window.addEventListener('resize', handleResize) // Initialize the previous screen size on mount if (isInitialMountRef.current) { prevScreenSizeRef.current = window.innerWidth <= 768 isInitialMountRef.current = false } return () => { window.removeEventListener('resize', handleResize) } }, [setLeftPanel]) const currentPath = useRouterState({ select: (state) => state.location.pathname, }) const { deleteAllThreads, unstarAllThreads, getFilteredThreads, threads } = useThreads() const filteredThreads = useMemo(() => { return getFilteredThreads(searchTerm) // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFilteredThreads, searchTerm, threads]) // Memoize categorized threads based on filteredThreads const favoritedThreads = useMemo(() => { return filteredThreads.filter((t) => t.isFavorite) }, [filteredThreads]) const unFavoritedThreads = useMemo(() => { return filteredThreads.filter((t) => !t.isFavorite) }, [filteredThreads]) // Disable body scroll when panel is open on small screens useEffect(() => { if (isSmallScreen && open) { document.body.style.overflow = 'hidden' } else { document.body.style.overflow = '' } return () => { document.body.style.overflow = '' } }, [isSmallScreen, open]) const { downloads, localDownloadingModels } = useDownloadStore() return ( <> {/* Backdrop overlay for small screens */} {isSmallScreen && open && (
{ // Don't close if clicking on search container or if currently searching if ( searchContainerRef.current?.contains(e.target as Node) || searchContainerMacRef.current?.contains(e.target as Node) ) { return } setLeftPanel(false) }} /> )} ) } export default LeftPanel