diff --git a/web-app/src/components/ui/dropdown-menu.tsx b/web-app/src/components/ui/dropdown-menu.tsx index 15f721e2ed..7a527aaca0 100644 --- a/web-app/src/components/ui/dropdown-menu.tsx +++ b/web-app/src/components/ui/dropdown-menu.tsx @@ -41,7 +41,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'bg-main-view select-none text-main-view-fg border-main-view-fg/5 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', + 'bg-main-view select-none text-main-view-fg border-main-view-fg/5 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[51] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', className )} {...props} @@ -229,7 +229,7 @@ function DropdownMenuSubContent({ { return () => { window.removeEventListener('resize', handleResize) } - }, [setLeftPanel]) + }, [setLeftPanel, open]) const currentPath = useRouterState({ select: (state) => state.location.pathname, @@ -433,6 +433,7 @@ const LeftPanel = () => { if (menu.title === 'common:authentication') { return (
+
{isAuthenticated ? ( ) : ( diff --git a/web-app/src/containers/auth/AuthLoginButton.tsx b/web-app/src/containers/auth/AuthLoginButton.tsx index 2f27bf78d2..d4de67327d 100644 --- a/web-app/src/containers/auth/AuthLoginButton.tsx +++ b/web-app/src/containers/auth/AuthLoginButton.tsx @@ -3,7 +3,7 @@ * Shows available authentication providers in a dropdown menu */ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { IconLogin, IconBrandGoogleFilled } from '@tabler/icons-react' import { useTranslation } from '@/i18n/react-i18next-compat' import { useAuth } from '@/hooks/useAuth' @@ -15,14 +15,45 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { useSmallScreen } from '@/hooks/useMediaQuery' export const AuthLoginButton = () => { const { t } = useTranslation() const { getAllProviders, loginWithProvider } = useAuth() const [isLoading, setIsLoading] = useState(false) + const [panelWidth, setPanelWidth] = useState(192) + const dropdownRef = useRef(null) + const isSmallScreen = useSmallScreen() const enabledProviders = getAllProviders() + useEffect(() => { + const updateWidth = () => { + // Find the left panel element + const leftPanel = document.querySelector('aside[ref]') || + document.querySelector('aside') || + dropdownRef.current?.closest('aside') + if (leftPanel) { + setPanelWidth(leftPanel.getBoundingClientRect().width) + } + } + + updateWidth() + window.addEventListener('resize', updateWidth) + + // Also observe for panel resize + const observer = new ResizeObserver(updateWidth) + const leftPanel = document.querySelector('aside') + if (leftPanel) { + observer.observe(leftPanel) + } + + return () => { + window.removeEventListener('resize', updateWidth) + observer.disconnect() + } + }, []) + const handleProviderLogin = async (providerId: ProviderType) => { try { setIsLoading(true) @@ -52,6 +83,7 @@ export const AuthLoginButton = () => { - + {enabledProviders.map((provider) => { const IconComponent = getProviderIcon(provider.icon) return ( diff --git a/web-app/src/containers/auth/UserProfileMenu.tsx b/web-app/src/containers/auth/UserProfileMenu.tsx index 941bfa2479..e61016bf01 100644 --- a/web-app/src/containers/auth/UserProfileMenu.tsx +++ b/web-app/src/containers/auth/UserProfileMenu.tsx @@ -3,7 +3,7 @@ * Dropdown menu with user profile and logout options */ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { DropdownMenu, DropdownMenuContent, @@ -13,16 +13,46 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { Button } from '@/components/ui/button' -import { IconUser, IconLogout, IconChevronDown } from '@tabler/icons-react' +import { IconUser, IconLogout } from '@tabler/icons-react' import { useTranslation } from '@/i18n/react-i18next-compat' import { useAuth } from '@/hooks/useAuth' import { toast } from 'sonner' +import { useSmallScreen } from '@/hooks/useMediaQuery' export const UserProfileMenu = () => { const { t } = useTranslation() const { user, isLoading, logout } = useAuth() const [isLoggingOut, setIsLoggingOut] = useState(false) + const [panelWidth, setPanelWidth] = useState(192) + const dropdownRef = useRef(null) + const isSmallScreen = useSmallScreen() + + useEffect(() => { + const updateWidth = () => { + // Find the left panel element + const leftPanel = document.querySelector('aside[ref]') || + document.querySelector('aside') || + dropdownRef.current?.closest('aside') + if (leftPanel) { + setPanelWidth(leftPanel.getBoundingClientRect().width) + } + } + + updateWidth() + window.addEventListener('resize', updateWidth) + + // Also observe for panel resize + const observer = new ResizeObserver(updateWidth) + const leftPanel = document.querySelector('aside') + if (leftPanel) { + observer.observe(leftPanel) + } + + return () => { + window.removeEventListener('resize', updateWidth) + observer.disconnect() + } + }, []) const handleLogout = async () => { if (isLoggingOut) return @@ -54,26 +84,24 @@ export const UserProfileMenu = () => { return ( - +
+ + {user.picture && ( + + )} + + {getInitials(user.name)} + + + {user.name} +
- +

{user.name}

diff --git a/web-app/src/hooks/useThreadScrolling.tsx b/web-app/src/hooks/useThreadScrolling.tsx index a60c9a6a29..9dfbeefb79 100644 --- a/web-app/src/hooks/useThreadScrolling.tsx +++ b/web-app/src/hooks/useThreadScrolling.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAppState } from './useAppState' import { useMessages } from './useMessages' import { useShallow } from 'zustand/react/shallow' @@ -25,16 +25,16 @@ export const useThreadScrolling = ( const showScrollToBottomBtn = !isAtBottom && hasScrollbar - const scrollToBottom = (smooth = false) => { + const scrollToBottom = useCallback((smooth = false) => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, ...(smooth ? { behavior: 'smooth' } : {}), }) } - } + }, []) - const handleScroll = (e: Event) => { + const handleScroll = useCallback((e: Event) => { const target = e.target as HTMLDivElement const { scrollTop, scrollHeight, clientHeight } = target // Use a small tolerance to better detect when we're at the bottom @@ -53,17 +53,18 @@ export const useThreadScrolling = ( setIsAtBottom(isBottom) setHasScrollbar(hasScroll) lastScrollTopRef.current = scrollTop - } + }, [streamingContent]) useEffect(() => { - if (scrollContainerRef.current) { - scrollContainerRef.current.addEventListener('scroll', handleScroll) + const scrollContainer = scrollContainerRef.current + if (scrollContainer) { + scrollContainer.addEventListener('scroll', handleScroll) return () => - scrollContainerRef.current?.removeEventListener('scroll', handleScroll) + scrollContainer.removeEventListener('scroll', handleScroll) } - }, [scrollContainerRef]) + }, [handleScroll]) - const checkScrollState = () => { + const checkScrollState = useCallback(() => { const scrollContainer = scrollContainerRef.current if (!scrollContainer) return @@ -73,7 +74,7 @@ export const useThreadScrolling = ( setIsAtBottom(isBottom) setHasScrollbar(hasScroll) - } + }, []) // Single useEffect for all auto-scrolling logic useEffect(() => { @@ -120,7 +121,7 @@ export const useThreadScrolling = ( const interval = setInterval(checkScrollState, 100) return () => clearInterval(interval) } - }, [streamingContent]) + }, [streamingContent, checkScrollState]) // Auto-scroll to bottom when component mounts or thread content changes useEffect(() => { @@ -138,7 +139,7 @@ export const useThreadScrolling = ( checkScrollState() return } - }, []) + }, [checkScrollState, scrollToBottom]) const handleDOMScroll = (e: Event) => { const target = e.target as HTMLDivElement @@ -182,7 +183,7 @@ export const useThreadScrolling = ( userIntendedPositionRef.current = null wasStreamingRef.current = false checkScrollState() - }, [threadId]) + }, [threadId, checkScrollState, scrollToBottom]) return useMemo( () => ({ showScrollToBottomBtn, scrollToBottom, setIsUserScrolling }), diff --git a/web-app/src/providers/AuthProvider.tsx b/web-app/src/providers/AuthProvider.tsx index bfd0e279c2..a62ea4fdd2 100644 --- a/web-app/src/providers/AuthProvider.tsx +++ b/web-app/src/providers/AuthProvider.tsx @@ -3,7 +3,7 @@ * Initializes the auth service and sets up event listeners */ -import { useEffect, useState, ReactNode } from 'react' +import { useCallback, useEffect, useState, ReactNode } from 'react' import { PlatformFeature } from '@/lib/platform/types' import { PlatformFeatures } from '@/lib/platform/const' import { initializeAuthStore, getAuthStore } from '@/hooks/useAuth' @@ -28,7 +28,7 @@ export function AuthProvider({ children }: AuthProviderProps) { PlatformFeatures[PlatformFeature.AUTHENTICATION] // Fetch user data when user logs in - const fetchUserData = async () => { + const fetchUserData = useCallback(async () => { try { const { setThreads } = useThreads.getState() const { setMessages } = useMessages.getState() @@ -47,10 +47,10 @@ export function AuthProvider({ children }: AuthProviderProps) { } catch (error) { console.error('Failed to fetch user data:', error) } - } + }, [serviceHub]) // Reset all app data when user logs out - const resetAppData = () => { + const resetAppData = useCallback(() => { // Clear all threads (including favorites) const { clearAllThreads, setCurrentThreadId } = useThreads.getState() clearAllThreads() @@ -70,7 +70,7 @@ export function AuthProvider({ children }: AuthProviderProps) { // Navigate back to home to ensure clean state navigate({ to: '/', replace: true }) - } + }, [navigate]) useEffect(() => { if (!isAuthenticationEnabled) { @@ -139,7 +139,7 @@ export function AuthProvider({ children }: AuthProviderProps) { return () => { cleanupAuthListener() } - }, [isAuthenticationEnabled, isReady]) + }, [isAuthenticationEnabled, isReady, fetchUserData, resetAppData]) return <>{isReady && children} }