Merge pull request #6563 from menloresearch/feat/web-minor-ui-tweak-login
feat: tweak login UI
This commit is contained in:
parent
3f51c35229
commit
b322c7649b
@ -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}
|
||||
|
||||
@ -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 />
|
||||
) : (
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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}</>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user