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"
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({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
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
)}
{...props}

View File

@ -141,7 +141,7 @@ const LeftPanel = () => {
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 (
<div key={menu.title}>
<div className="mx-1 my-2 border-t border-left-panel-fg/5" />
{isAuthenticated ? (
<UserProfileMenu />
) : (

View File

@ -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<number>(192)
const dropdownRef = useRef<HTMLButtonElement>(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 = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
ref={dropdownRef}
disabled={isLoading}
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>
</button>
</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) => {
const IconComponent = getProviderIcon(provider.icon)
return (

View File

@ -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<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 () => {
if (isLoggingOut) return
@ -54,26 +84,24 @@ export const UserProfileMenu = () => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="link"
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 && (
<AvatarImage src={user.picture} alt={user.name} />
)}
<AvatarFallback className="text-xs">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<span className="truncate text-sm">{user.name}</span>
</div>
<IconChevronDown size={14} className="text-muted-foreground" />
</Button>
<div ref={dropdownRef} className="flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded">
<Avatar className="h-[18px] w-[18px]">
{user.picture && (
<AvatarImage src={user.picture} alt={user.name} />
)}
<AvatarFallback className="text-[10px]">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<span className="font-medium text-left-panel-fg/90">{user.name}</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start" className="w-56">
<DropdownMenuContent
side="top"
align="end"
style={{ width: `${panelWidth}px` }}
alignOffset={isSmallScreen ? -4 : 0}
>
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<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 { 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 }),

View File

@ -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}</>
}