Merge pull request #6563 from menloresearch/feat/web-minor-ui-tweak-login

feat: tweak login UI
This commit is contained in:
Dinh Long Nguyen 2025-09-23 21:09:58 +07:00 committed by GitHub
parent 3f51c35229
commit b322c7649b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 114 additions and 47 deletions

View File

@ -41,7 +41,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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 className
)} )}
{...props} {...props}
@ -229,7 +229,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
'bg-main-view 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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', 'bg-main-view 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] min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className className
)} )}
{...props} {...props}

View File

@ -141,7 +141,7 @@ const LeftPanel = () => {
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, [setLeftPanel]) }, [setLeftPanel, open])
const currentPath = useRouterState({ const currentPath = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
@ -433,6 +433,7 @@ const LeftPanel = () => {
if (menu.title === 'common:authentication') { if (menu.title === 'common:authentication') {
return ( return (
<div key={menu.title}> <div key={menu.title}>
<div className="mx-1 my-2 border-t border-left-panel-fg/5" />
{isAuthenticated ? ( {isAuthenticated ? (
<UserProfileMenu /> <UserProfileMenu />
) : ( ) : (

View File

@ -3,7 +3,7 @@
* Shows available authentication providers in a dropdown menu * 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 { IconLogin, IconBrandGoogleFilled } from '@tabler/icons-react'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
@ -15,14 +15,45 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { useSmallScreen } from '@/hooks/useMediaQuery'
export const AuthLoginButton = () => { export const AuthLoginButton = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { getAllProviders, loginWithProvider } = useAuth() const { getAllProviders, loginWithProvider } = useAuth()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [panelWidth, setPanelWidth] = useState<number>(192)
const dropdownRef = useRef<HTMLButtonElement>(null)
const isSmallScreen = useSmallScreen()
const enabledProviders = getAllProviders() 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) => { const handleProviderLogin = async (providerId: ProviderType) => {
try { try {
setIsLoading(true) setIsLoading(true)
@ -52,6 +83,7 @@ export const AuthLoginButton = () => {
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
ref={dropdownRef}
disabled={isLoading} disabled={isLoading}
className="flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded w-full" className="flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded w-full"
> >
@ -59,7 +91,12 @@ export const AuthLoginButton = () => {
<span className="font-medium text-left-panel-fg/90">{t('common:login')}</span> <span className="font-medium text-left-panel-fg/90">{t('common:login')}</span>
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start" className="w-48"> <DropdownMenuContent
side="top"
align="end"
style={{ width: `${panelWidth}px` }}
alignOffset={isSmallScreen ? -4 : 0}
>
{enabledProviders.map((provider) => { {enabledProviders.map((provider) => {
const IconComponent = getProviderIcon(provider.icon) const IconComponent = getProviderIcon(provider.icon)
return ( return (

View File

@ -3,7 +3,7 @@
* Dropdown menu with user profile and logout options * Dropdown menu with user profile and logout options
*/ */
import { useState } from 'react' import { useState, useRef, useEffect } from 'react'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -13,16 +13,46 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button' import { IconUser, IconLogout } from '@tabler/icons-react'
import { IconUser, IconLogout, IconChevronDown } from '@tabler/icons-react'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useSmallScreen } from '@/hooks/useMediaQuery'
export const UserProfileMenu = () => { export const UserProfileMenu = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { user, isLoading, logout } = useAuth() const { user, isLoading, logout } = useAuth()
const [isLoggingOut, setIsLoggingOut] = useState(false) const [isLoggingOut, setIsLoggingOut] = useState(false)
const [panelWidth, setPanelWidth] = useState<number>(192)
const dropdownRef = useRef<HTMLDivElement>(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 () => { const handleLogout = async () => {
if (isLoggingOut) return if (isLoggingOut) return
@ -54,26 +84,24 @@ export const UserProfileMenu = () => {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <div ref={dropdownRef} className="flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded">
variant="link" <Avatar className="h-[18px] w-[18px]">
size="sm"
className="w-full justify-between gap-2 px-2"
>
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
{user.picture && ( {user.picture && (
<AvatarImage src={user.picture} alt={user.name} /> <AvatarImage src={user.picture} alt={user.name} />
)} )}
<AvatarFallback className="text-xs"> <AvatarFallback className="text-[10px]">
{getInitials(user.name)} {getInitials(user.name)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<span className="truncate text-sm">{user.name}</span> <span className="font-medium text-left-panel-fg/90">{user.name}</span>
</div> </div>
<IconChevronDown size={14} className="text-muted-foreground" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start" className="w-56"> <DropdownMenuContent
side="top"
align="end"
style={{ width: `${panelWidth}px` }}
alignOffset={isSmallScreen ? -4 : 0}
>
<DropdownMenuLabel> <DropdownMenuLabel>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p> <p className="text-sm font-medium leading-none">{user.name}</p>

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useAppState } from './useAppState' import { useAppState } from './useAppState'
import { useMessages } from './useMessages' import { useMessages } from './useMessages'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
@ -25,16 +25,16 @@ export const useThreadScrolling = (
const showScrollToBottomBtn = !isAtBottom && hasScrollbar const showScrollToBottomBtn = !isAtBottom && hasScrollbar
const scrollToBottom = (smooth = false) => { const scrollToBottom = useCallback((smooth = false) => {
if (scrollContainerRef.current) { if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({ scrollContainerRef.current.scrollTo({
top: scrollContainerRef.current.scrollHeight, top: scrollContainerRef.current.scrollHeight,
...(smooth ? { behavior: 'smooth' } : {}), ...(smooth ? { behavior: 'smooth' } : {}),
}) })
} }
} }, [])
const handleScroll = (e: Event) => { const handleScroll = useCallback((e: Event) => {
const target = e.target as HTMLDivElement const target = e.target as HTMLDivElement
const { scrollTop, scrollHeight, clientHeight } = target const { scrollTop, scrollHeight, clientHeight } = target
// Use a small tolerance to better detect when we're at the bottom // Use a small tolerance to better detect when we're at the bottom
@ -53,17 +53,18 @@ export const useThreadScrolling = (
setIsAtBottom(isBottom) setIsAtBottom(isBottom)
setHasScrollbar(hasScroll) setHasScrollbar(hasScroll)
lastScrollTopRef.current = scrollTop lastScrollTopRef.current = scrollTop
} }, [streamingContent])
useEffect(() => { useEffect(() => {
if (scrollContainerRef.current) { const scrollContainer = scrollContainerRef.current
scrollContainerRef.current.addEventListener('scroll', handleScroll) if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll)
return () => return () =>
scrollContainerRef.current?.removeEventListener('scroll', handleScroll) scrollContainer.removeEventListener('scroll', handleScroll)
} }
}, [scrollContainerRef]) }, [handleScroll])
const checkScrollState = () => { const checkScrollState = useCallback(() => {
const scrollContainer = scrollContainerRef.current const scrollContainer = scrollContainerRef.current
if (!scrollContainer) return if (!scrollContainer) return
@ -73,7 +74,7 @@ export const useThreadScrolling = (
setIsAtBottom(isBottom) setIsAtBottom(isBottom)
setHasScrollbar(hasScroll) setHasScrollbar(hasScroll)
} }, [])
// Single useEffect for all auto-scrolling logic // Single useEffect for all auto-scrolling logic
useEffect(() => { useEffect(() => {
@ -120,7 +121,7 @@ export const useThreadScrolling = (
const interval = setInterval(checkScrollState, 100) const interval = setInterval(checkScrollState, 100)
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [streamingContent]) }, [streamingContent, checkScrollState])
// Auto-scroll to bottom when component mounts or thread content changes // Auto-scroll to bottom when component mounts or thread content changes
useEffect(() => { useEffect(() => {
@ -138,7 +139,7 @@ export const useThreadScrolling = (
checkScrollState() checkScrollState()
return return
} }
}, []) }, [checkScrollState, scrollToBottom])
const handleDOMScroll = (e: Event) => { const handleDOMScroll = (e: Event) => {
const target = e.target as HTMLDivElement const target = e.target as HTMLDivElement
@ -182,7 +183,7 @@ export const useThreadScrolling = (
userIntendedPositionRef.current = null userIntendedPositionRef.current = null
wasStreamingRef.current = false wasStreamingRef.current = false
checkScrollState() checkScrollState()
}, [threadId]) }, [threadId, checkScrollState, scrollToBottom])
return useMemo( return useMemo(
() => ({ showScrollToBottomBtn, scrollToBottom, setIsUserScrolling }), () => ({ showScrollToBottomBtn, scrollToBottom, setIsUserScrolling }),

View File

@ -3,7 +3,7 @@
* Initializes the auth service and sets up event listeners * 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 { PlatformFeature } from '@/lib/platform/types'
import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeatures } from '@/lib/platform/const'
import { initializeAuthStore, getAuthStore } from '@/hooks/useAuth' import { initializeAuthStore, getAuthStore } from '@/hooks/useAuth'
@ -28,7 +28,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
PlatformFeatures[PlatformFeature.AUTHENTICATION] PlatformFeatures[PlatformFeature.AUTHENTICATION]
// Fetch user data when user logs in // Fetch user data when user logs in
const fetchUserData = async () => { const fetchUserData = useCallback(async () => {
try { try {
const { setThreads } = useThreads.getState() const { setThreads } = useThreads.getState()
const { setMessages } = useMessages.getState() const { setMessages } = useMessages.getState()
@ -47,10 +47,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
} catch (error) { } catch (error) {
console.error('Failed to fetch user data:', error) console.error('Failed to fetch user data:', error)
} }
} }, [serviceHub])
// Reset all app data when user logs out // Reset all app data when user logs out
const resetAppData = () => { const resetAppData = useCallback(() => {
// Clear all threads (including favorites) // Clear all threads (including favorites)
const { clearAllThreads, setCurrentThreadId } = useThreads.getState() const { clearAllThreads, setCurrentThreadId } = useThreads.getState()
clearAllThreads() clearAllThreads()
@ -70,7 +70,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
// Navigate back to home to ensure clean state // Navigate back to home to ensure clean state
navigate({ to: '/', replace: true }) navigate({ to: '/', replace: true })
} }, [navigate])
useEffect(() => { useEffect(() => {
if (!isAuthenticationEnabled) { if (!isAuthenticationEnabled) {
@ -139,7 +139,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
return () => { return () => {
cleanupAuthListener() cleanupAuthListener()
} }
}, [isAuthenticationEnabled, isReady]) }, [isAuthenticationEnabled, isReady, fetchUserData, resetAppData])
return <>{isReady && children}</> return <>{isReady && children}</>
} }