import { Link, useRouterState, useNavigate } from '@tanstack/react-router' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import { IconLayoutSidebar, IconDots, IconCirclePlus, IconSettings, IconStar, IconFolderPlus, IconMessage, IconApps, IconX, IconSearch, IconClipboardSmile, IconFolder, IconPencil, IconTrash, } from '@tabler/icons-react' import { route } from '@/constants/routes' import ThreadList from './ThreadList' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' import { AuthLoginButton } from '@/containers/auth/AuthLoginButton' import { UserProfileMenu } from '@/containers/auth/UserProfileMenu' import { useAuth } from '@/hooks/useAuth' import { useThreads } from '@/hooks/useThreads' import { useThreadManagement } from '@/hooks/useThreadManagement' 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 { DeleteAllThreadsDialog } from '@/containers/dialogs' import AddProjectDialog from '@/containers/dialogs/AddProjectDialog' import { DeleteProjectDialog } from '@/containers/dialogs/DeleteProjectDialog' const mainMenus = [ { title: 'common:newChat', icon: IconCirclePlus, route: route.home, isEnabled: true, }, { title: 'common:projects.title', icon: IconFolderPlus, route: route.project, isEnabled: true, }, ] const secondaryMenus = [ { title: 'common:assistants', icon: IconClipboardSmile, route: route.assistant, isEnabled: PlatformFeatures[PlatformFeature.ASSISTANTS], }, { title: 'common:hub', icon: IconApps, route: route.hub.index, isEnabled: PlatformFeatures[PlatformFeature.MODEL_HUB], }, { title: 'common:settings', icon: IconSettings, route: route.settings.general, isEnabled: true, }, ] const LeftPanel = () => { const open = useLeftPanel((state) => state.open) const setLeftPanel = useLeftPanel((state) => state.setLeftPanel) const { t } = useTranslation() const navigate = useNavigate() const [searchTerm, setSearchTerm] = useState('') const { isAuthenticated } = useAuth() 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 && open) { setLeftPanel(false) } else if (!open) { 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, open]) const currentPath = useRouterState({ select: (state) => state.location.pathname, }) const deleteAllThreads = useThreads((state) => state.deleteAllThreads) const unstarAllThreads = useThreads((state) => state.unstarAllThreads) const getFilteredThreads = useThreads((state) => state.getFilteredThreads) const threads = useThreads((state) => state.threads) const { folders, addFolder, updateFolder, deleteFolder, getFolderById } = useThreadManagement() // Project dialog states const [projectDialogOpen, setProjectDialogOpen] = useState(false) const [editingProjectKey, setEditingProjectKey] = useState( null ) const [deleteProjectConfirmOpen, setDeleteProjectConfirmOpen] = useState(false) const [deletingProjectId, setDeletingProjectId] = useState( null ) const filteredThreads = useMemo(() => { return getFilteredThreads(searchTerm) // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFilteredThreads, searchTerm, threads]) const filteredProjects = useMemo(() => { if (!searchTerm) return folders return folders.filter((folder) => folder.name.toLowerCase().includes(searchTerm.toLowerCase()) ) }, [folders, searchTerm]) // 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 && !t.metadata?.project) }, [filteredThreads]) // Project handlers const handleProjectDelete = (id: string) => { setDeletingProjectId(id) setDeleteProjectConfirmOpen(true) } const confirmProjectDelete = () => { if (deletingProjectId) { deleteFolder(deletingProjectId) setDeleteProjectConfirmOpen(false) setDeletingProjectId(null) } } const handleProjectSave = (name: string) => { if (editingProjectKey) { updateFolder(editingProjectKey, name) } else { const newProject = addFolder(name) // Navigate to the newly created project navigate({ to: '/project/$projectId', params: { projectId: newProject.id }, }) } setProjectDialogOpen(false) setEditingProjectKey(null) } // 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]) return ( <> {/* Backdrop overlay for small screens */} {isSmallScreen && open && !IS_IOS && !IS_ANDROID && (
{ // 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) }} /> )} {/* Project Dialogs */} ) } export default LeftPanel