Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontends/ui/src/adapters/ui/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const Copy = createIcon('copy-generic')
export const Download = createIcon('download')
export const Upload = createIcon('upload')
export const Share = createIcon('share')
export const OpenExternal = createIcon('open-external')
export const Refresh = createIcon('refresh')

// ---------------------------------------------------------------------------
Expand Down
22 changes: 8 additions & 14 deletions frontends/ui/src/features/layout/components/AppBar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import { AppBar } from './AppBar'
const mockToggleSessionsPanel = vi.fn()
const mockOpenRightPanel = vi.fn()
const mockCloseRightPanel = vi.fn()
const mockSetTheme = vi.fn()

vi.mock('../store', () => ({
useLayoutStore: () => ({
toggleSessionsPanel: mockToggleSessionsPanel,
rightPanel: null,
openRightPanel: mockOpenRightPanel,
closeRightPanel: mockCloseRightPanel,
theme: 'system',
setTheme: mockSetTheme,
}),
}))

Expand Down Expand Up @@ -66,7 +69,7 @@ describe('AppBar', () => {
expect(screen.getByRole('button', { name: /create new session/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /toggle sessions sidebar/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /add data sources/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /open settings/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /open documentation/i })).not.toBeDisabled()
})

test('enables action buttons when authenticated', () => {
Expand All @@ -75,7 +78,7 @@ describe('AppBar', () => {
expect(screen.getByRole('button', { name: /create new session/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /toggle sessions sidebar/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /add data sources/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /open settings/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /open documentation/i })).not.toBeDisabled()
})

test('calls onNewSession when logo button clicked', async () => {
Expand Down Expand Up @@ -117,16 +120,6 @@ describe('AppBar', () => {
expect(mockOpenRightPanel).toHaveBeenCalledWith('data-sources')
})

test('opens settings panel when Settings clicked', async () => {
const user = userEvent.setup()

render(<AppBar isAuthenticated={true} />)

await user.click(screen.getByRole('button', { name: /open settings/i }))

expect(mockOpenRightPanel).toHaveBeenCalledWith('settings')
})

test('renders Docs button that opens in new tab', () => {
render(<AppBar />)

Expand Down Expand Up @@ -168,8 +161,9 @@ describe('AppBar', () => {
})
await user.click(avatarButton)

// Popover should show "Default User" and info message
// Popover should show "Default User", theme control, and info message
expect(screen.getByText('Default User')).toBeInTheDocument()
expect(screen.getByRole('radiogroup', { name: /theme/i })).toBeInTheDocument()
expect(screen.getByText('Authentication Not Configured')).toBeInTheDocument()
})

Expand All @@ -193,7 +187,7 @@ describe('AppBar', () => {
expect(screen.getByRole('button', { name: /create new session/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /toggle sessions sidebar/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /add data sources/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /open settings/i })).not.toBeDisabled()
expect(screen.getByRole('button', { name: /open documentation/i })).not.toBeDisabled()
})

test('shows session title when auth is disabled', () => {
Expand Down
126 changes: 96 additions & 30 deletions frontends/ui/src/features/layout/components/AppBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* AppBar Component
*
* Top navigation bar with menu toggle, logo, session title,
* and action buttons (Add Sources, Settings, Docs, User Avatar).
* and action buttons (Add Sources, Docs, User Avatar).
*
* Shows different states based on authentication:
* - Auth disabled: Default User avatar with info tooltip (no sign in/out)
Expand All @@ -15,10 +15,11 @@

'use client'

import { type FC, useCallback, useState } from 'react'
import { type CSSProperties, type FC, useCallback, useState } from 'react'
import { Flex, Text, Button, Logo, Avatar, Popover, Divider } from '@/adapters/ui'
import { Menu, Globe, Settings, Book, Lock, Logout, ChevronRight, Info } from '@/adapters/ui/icons'
import { Menu, Globe, Book, Lock, Logout, OpenExternal, Info, Moon, Sun } from '@/adapters/ui/icons'
import { useLayoutStore } from '../store'
import type { ThemeMode } from '../types'

interface AppBarProps {
/** Current session title to display */
Expand All @@ -43,6 +44,17 @@ interface AppBarProps {
onSignOut?: () => void
}

/** Transparent popover shell with outline only (no KUI fill/shadow). */
const USER_MENU_POPOVER_CLASS =
'!bg-transparent !p-0 !shadow-none rounded-[var(--radius-md)] border border-base text-primary'

const USER_MENU_POPOVER_STYLE: CSSProperties = {
backgroundColor: 'transparent',
boxShadow: 'none',
padding: 0,
marginTop: 4,
}

/**
* Main navigation bar at the top of the application.
* Controls sidebar toggles and navigation actions.
Expand Down Expand Up @@ -74,15 +86,6 @@ export const AppBar: FC<AppBarProps> = ({
}
}, [rightPanel, openRightPanel, closeRightPanel, isAuthenticated])

const handleSettingsClick = useCallback(() => {
if (!isAuthenticated) return
if (rightPanel === 'settings') {
closeRightPanel()
} else {
openRightPanel('settings')
}
}, [rightPanel, openRightPanel, closeRightPanel, isAuthenticated])

const handleDocsClick = useCallback(() => {
window.open('https://github.com/NVIDIA-AI-Blueprints/aiq', '_blank')
}, [])
Expand Down Expand Up @@ -164,21 +167,6 @@ export const AppBar: FC<AppBarProps> = ({
<Text kind="label/regular/md">Data Sources</Text>
</Flex>
</Button>

<Button
kind="tertiary"
size="small"
onClick={handleSettingsClick}
disabled={!isAuthenticated}
aria-label="Open settings"
title="Open settings"
>
<Flex align="center" gap="1">
<Settings className="h-4 w-4" />
<Text kind="label/regular/md">Settings</Text>
</Flex>
</Button>

<Button
kind="tertiary"
size="small"
Expand All @@ -188,8 +176,8 @@ export const AppBar: FC<AppBarProps> = ({
>
<Flex align="center" gap="1">
<Book className="h-4 w-4" />
<Text kind="label/regular/md">Docs</Text>
<ChevronRight className="h-3 w-3 -rotate-45" />
<Text kind="label/regular/md">Documentation</Text>
<OpenExternal className="h-4 w-4" />
</Flex>
</Button>

Expand All @@ -200,6 +188,8 @@ export const AppBar: FC<AppBarProps> = ({
onOpenChange={setIsUserMenuOpen}
side="bottom"
align="end"
className={USER_MENU_POPOVER_CLASS}
style={USER_MENU_POPOVER_STYLE}
slotContent={<AuthDisabledContent />}
>
<Button
Expand All @@ -218,6 +208,8 @@ export const AppBar: FC<AppBarProps> = ({
onOpenChange={setIsUserMenuOpen}
side="bottom"
align="end"
className={USER_MENU_POPOVER_CLASS}
style={USER_MENU_POPOVER_STYLE}
slotContent={<UserDropdownContent user={user} onSignOut={handleSignOut} />}
>
<Button
Expand Down Expand Up @@ -267,6 +259,77 @@ interface UserDropdownContentProps {
onSignOut?: () => void
}

const APPEARANCE_SEGMENTS: { mode: ThemeMode; label: string }[] = [
{ mode: 'system', label: 'System' },
{ mode: 'dark', label: 'Dark' },
{ mode: 'light', label: 'Light' },
]

const AppearanceThemeControl: FC = () => {
const { theme, setTheme } = useLayoutStore()

return (
<Flex direction="col" gap="2">
<Text kind="label/regular/sm" className="text-subtle">
Appearance
</Text>
<Flex
align="center"
gap="1"
className="p-1"
role="radiogroup"
aria-label="Theme"
style={{
background: 'var(--color-component-track-background, #FFFFFF33)',
borderRadius: 'var(--radius-lg)',
}}
>
{APPEARANCE_SEGMENTS.map(({ mode, label }) => {
const selected = theme === mode
return (
<Button
key={mode}
type="button"
role="radio"
aria-checked={selected}
aria-label={`${label} theme`}
kind="tertiary"
size="small"
onClick={() => setTheme(mode)}
className={`h-auto min-h-9 flex-1 rounded-[var(--radius-md)] border-0 px-2 py-1.5 shadow-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus,#76b900)] ${
selected ? '!bg-black !text-white hover:!bg-black' : 'bg-transparent hover:bg-white/10'
}`}
>
<Flex align="center" justify="center" gap="1" className="w-full">
{mode === 'dark' ? (
<Moon
className={`h-4 w-4 shrink-0 ${selected ? '!text-white' : 'text-primary'}`}
width={16}
height={16}
/>
) : null}
{mode === 'light' ? (
<Sun
className={`h-4 w-4 shrink-0 ${selected ? '!text-white' : 'text-primary'}`}
width={16}
height={16}
/>
) : null}
<Text
kind={selected ? 'label/semibold/sm' : 'label/regular/sm'}
className={selected ? 'text-white' : 'text-primary'}
>
{label}
</Text>
</Flex>
</Button>
)
})}
</Flex>
</Flex>
)
}

const UserDropdownContent: FC<UserDropdownContentProps> = ({ user, onSignOut }) => {
return (
<Flex direction="col" gap="3" className="min-w-[240px] p-4">
Expand All @@ -291,6 +354,7 @@ const UserDropdownContent: FC<UserDropdownContentProps> = ({ user, onSignOut })

<Divider />

<AppearanceThemeControl />
{/* Sign out button */}
<Button
kind="secondary"
Expand Down Expand Up @@ -328,8 +392,10 @@ const AuthDisabledContent: FC = () => {

<Divider />

<AppearanceThemeControl />

{/* Info message */}
<Flex align="center" gap="2" className="rounded bg-[var(--background-color-surface-raised)] p-3">
<Flex align="center" gap="2" className="rounded border border-base p-3">
<Info className="h-4 w-4 shrink-0 text-[var(--text-color-subtle)]" />
<Text kind="body/regular/sm" className="text-subtle">
Authentication Not Configured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,6 @@ vi.mock('./DataSourcesPanel', () => ({
DataSourcesPanel: () => <div data-testid="data-sources-panel">Data Sources Panel</div>,
}))

vi.mock('./SettingsPanel', () => ({
SettingsPanel: () => <div data-testid="settings-panel">Settings Panel</div>,
}))

import { useChatStore } from '@/features/chat'
import { useLayoutStore } from '../store'

Expand All @@ -119,7 +115,6 @@ describe('MainLayout', () => {
expect(screen.getByTestId('input-area')).toBeInTheDocument()
expect(screen.getByTestId('research-panel')).toBeInTheDocument()
expect(screen.getByTestId('data-sources-panel')).toBeInTheDocument()
expect(screen.getByTestId('settings-panel')).toBeInTheDocument()
})

test('passes session title to AppBar', () => {
Expand Down
6 changes: 1 addition & 5 deletions frontends/ui/src/features/layout/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* - SessionsPanel (left, overlay)
* - ChatArea + InputArea (center, responsive width)
* - ResearchPanel (right, pushes content - takes 60% when open)
* - DataSourcesPanel / SettingsPanel (right, overlay)
* - DataSourcesPanel (right, overlay)
*
* Handles auth state to show different UI for logged-in vs logged-out users.
*/
Expand All @@ -25,7 +25,6 @@ import { ChatArea } from './ChatArea'
import { InputArea } from './InputArea'
import { ResearchPanel } from './ResearchPanel'
import { DataSourcesPanel } from './DataSourcesPanel'
import { SettingsPanel } from './SettingsPanel'
import { useChatStore, useDeepResearch, NoSourcesBanner } from '@/features/chat'
import { hasActiveDeepResearchJob } from '@/features/chat/lib/session-activity'
import { useLayoutStore } from '../store'
Expand Down Expand Up @@ -193,9 +192,6 @@ export const MainLayout: FC<MainLayoutProps> = ({

{/* Data Sources Panel (Right) - Overlay */}
<DataSourcesPanel />

{/* Settings Panel (Right) - Overlay */}
<SettingsPanel />
</Flex>
)
}
Loading