diff --git a/frontends/ui/src/features/layout/components/ChatArea.spec.tsx b/frontends/ui/src/features/layout/components/ChatArea.spec.tsx index a57ab328..bf4de7fd 100644 --- a/frontends/ui/src/features/layout/components/ChatArea.spec.tsx +++ b/frontends/ui/src/features/layout/components/ChatArea.spec.tsx @@ -6,6 +6,14 @@ import userEvent from '@testing-library/user-event' import { vi, describe, test, expect, beforeEach } from 'vitest' import { ChatArea } from './ChatArea' +vi.mock('../store', () => ({ + useLayoutStore: vi.fn((selector: (s: { newUiEnabled: boolean }) => unknown) => + selector({ newUiEnabled: false }) + ), +})) + +import { useLayoutStore } from '../store' + // Mock the chat store const mockRespondToPrompt = vi.fn() const mockDismissErrorCard = vi.fn() @@ -41,6 +49,9 @@ import { useChatStore } from '@/features/chat' describe('ChatArea', () => { beforeEach(() => { vi.clearAllMocks() + vi.mocked(useLayoutStore).mockImplementation((selector) => + selector({ newUiEnabled: false } as never) + ) }) test('renders welcome state when not authenticated', () => { @@ -58,6 +69,18 @@ describe('ChatArea', () => { expect(screen.getByText(/AI-powered research companion/i)).toBeInTheDocument() }) + test('renders new UI welcome copy when newUiEnabled is true', () => { + vi.mocked(useLayoutStore).mockImplementation((selector) => + selector({ newUiEnabled: true } as never) + ) + + render() + + expect(screen.getByText('Welcome to AI-Q')).toBeInTheDocument() + expect(screen.getByText('Deep Research')).toBeInTheDocument() + expect(screen.getByText(/on-premise data/i)).toBeInTheDocument() + }) + test('calls onSignIn when sign in button clicked', async () => { const user = userEvent.setup() const onSignIn = vi.fn() diff --git a/frontends/ui/src/features/layout/components/ChatArea.tsx b/frontends/ui/src/features/layout/components/ChatArea.tsx index 0f05a605..b7815a10 100644 --- a/frontends/ui/src/features/layout/components/ChatArea.tsx +++ b/frontends/ui/src/features/layout/components/ChatArea.tsx @@ -22,6 +22,7 @@ import { Document, Lock } from '@/adapters/ui/icons' import { useChatStore, AgentPrompt, AgentResponse, ErrorBanner, FileUploadBanner, DeepResearchBanner, UserMessage, ChatThinking } from '@/features/chat' import type { ChatMessage } from '@/features/chat' import { StarfieldAnimation } from '@/shared/components/StarfieldAnimation' +import { useLayoutStore } from '../store' interface ChatAreaProps { /** Whether the user is authenticated */ @@ -339,6 +340,8 @@ interface WelcomeStateProps { } const WelcomeState: FC = ({ isAuthenticated = false, onSignIn }) => { + const newUiEnabled = useLayoutStore((s) => s.newUiEnabled) + if (!isAuthenticated) { // Logged out state - prompt to sign in return ( @@ -393,10 +396,17 @@ const WelcomeState: FC = ({ isAuthenticated = false, onSignIn Welcome to AI-Q - - Your AI-powered research companion for exploring technical documentation, market analysis, - and more. - + {newUiEnabled ? ( + + Your AI-powered Deep Research companion for exploring technical + documentation, market analysis, and insights from your on-premise data, web sources, and local files. + + ) : ( + + Your AI-powered research companion for exploring technical documentation, market analysis, + and more. + + )} diff --git a/frontends/ui/src/features/layout/components/SettingsPanel.spec.tsx b/frontends/ui/src/features/layout/components/SettingsPanel.spec.tsx index 714770a5..651e0177 100644 --- a/frontends/ui/src/features/layout/components/SettingsPanel.spec.tsx +++ b/frontends/ui/src/features/layout/components/SettingsPanel.spec.tsx @@ -10,6 +10,7 @@ import { SettingsPanel } from './SettingsPanel' const mockCloseRightPanel = vi.fn() const mockOpenRightPanel = vi.fn() const mockSetTheme = vi.fn() +const mockSetNewUiEnabled = vi.fn() vi.mock('../store', () => ({ useLayoutStore: vi.fn(() => ({ @@ -18,6 +19,8 @@ vi.mock('../store', () => ({ openRightPanel: mockOpenRightPanel, theme: 'system', setTheme: mockSetTheme, + newUiEnabled: false, + setNewUiEnabled: mockSetNewUiEnabled, })), })) @@ -33,6 +36,8 @@ describe('SettingsPanel', () => { openRightPanel: mockOpenRightPanel, theme: 'system', setTheme: mockSetTheme, + newUiEnabled: false, + setNewUiEnabled: mockSetNewUiEnabled, }) }) @@ -56,6 +61,8 @@ describe('SettingsPanel', () => { openRightPanel: mockOpenRightPanel, theme: 'dark', setTheme: mockSetTheme, + newUiEnabled: false, + setNewUiEnabled: mockSetNewUiEnabled, }) render() @@ -75,6 +82,16 @@ describe('SettingsPanel', () => { expect(mockSetTheme).toHaveBeenCalledWith('dark') }) + test('calls setNewUiEnabled when New UI switch is toggled', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByTestId('nv-switch-track')) + + expect(mockSetNewUiEnabled).toHaveBeenCalledWith(true) + }) + test('does not render when panel is closed', () => { vi.mocked(useLayoutStore).mockReturnValue({ rightPanel: null, @@ -82,6 +99,8 @@ describe('SettingsPanel', () => { openRightPanel: mockOpenRightPanel, theme: 'system', setTheme: mockSetTheme, + newUiEnabled: false, + setNewUiEnabled: mockSetNewUiEnabled, }) render() diff --git a/frontends/ui/src/features/layout/components/SettingsPanel.tsx b/frontends/ui/src/features/layout/components/SettingsPanel.tsx index 0acca238..dd495e8b 100644 --- a/frontends/ui/src/features/layout/components/SettingsPanel.tsx +++ b/frontends/ui/src/features/layout/components/SettingsPanel.tsx @@ -11,7 +11,7 @@ 'use client' import { type FC, useCallback } from 'react' -import { Flex, Text, SidePanel, Select } from '@/adapters/ui' +import { Flex, Text, SidePanel, Select, Switch } from '@/adapters/ui' import { Settings } from '@/adapters/ui/icons' import { useLayoutStore } from '../store' import type { ThemeMode } from '../types' @@ -21,7 +21,15 @@ import type { ThemeMode } from '../types' * Opens from the right side of the screen. */ export const SettingsPanel: FC = () => { - const { rightPanel, closeRightPanel, openRightPanel, theme, setTheme } = useLayoutStore() + const { + rightPanel, + closeRightPanel, + openRightPanel, + theme, + setTheme, + newUiEnabled, + setNewUiEnabled, + } = useLayoutStore() const isOpen = rightPanel === 'settings' @@ -79,6 +87,17 @@ export const SettingsPanel: FC = () => { { children: 'Dark', value: 'dark' }, ]} /> + + New UI + + ) diff --git a/frontends/ui/src/features/layout/store.spec.ts b/frontends/ui/src/features/layout/store.spec.ts index d2c68edc..b4d8ce37 100644 --- a/frontends/ui/src/features/layout/store.spec.ts +++ b/frontends/ui/src/features/layout/store.spec.ts @@ -13,6 +13,7 @@ describe('useLayoutStore', () => { researchPanelTab: 'plan', dataSourcesPanelTab: 'connections', theme: 'system', + newUiEnabled: false, }) }) @@ -24,6 +25,7 @@ describe('useLayoutStore', () => { expect(state.rightPanel).toBeNull() expect(state.researchPanelTab).toBe('plan') expect(state.dataSourcesPanelTab).toBe('connections') + expect(state.newUiEnabled).toBe(false) }) }) @@ -175,4 +177,20 @@ describe('useLayoutStore', () => { expect(useLayoutStore.getState().theme).toBe('system') }) }) + + describe('setNewUiEnabled', () => { + test('enables new UI', () => { + useLayoutStore.getState().setNewUiEnabled(true) + + expect(useLayoutStore.getState().newUiEnabled).toBe(true) + }) + + test('disables new UI', () => { + useLayoutStore.setState({ newUiEnabled: true }) + + useLayoutStore.getState().setNewUiEnabled(false) + + expect(useLayoutStore.getState().newUiEnabled).toBe(false) + }) + }) }) diff --git a/frontends/ui/src/features/layout/store.ts b/frontends/ui/src/features/layout/store.ts index 7cbc9778..f0ef48ed 100644 --- a/frontends/ui/src/features/layout/store.ts +++ b/frontends/ui/src/features/layout/store.ts @@ -28,6 +28,7 @@ const initialState: LayoutState = { dataSourcesPanelTab: 'connections', enabledDataSourceIds: [], // Start empty, populated when data sources are fetched theme: 'system', + newUiEnabled: false, availableDataSources: null, knowledgeLayerAvailable: false, // Default to false until API confirms availability dataSourcesLoading: false, @@ -82,6 +83,9 @@ export const useLayoutStore = create()( setTheme: (theme: ThemeMode) => set({ theme }, false, 'setTheme'), + setNewUiEnabled: (enabled: boolean) => + set({ newUiEnabled: enabled }, false, 'setNewUiEnabled'), + fetchDataSources: async (authToken?: string) => { set({ dataSourcesLoading: true, dataSourcesError: null }, false, 'fetchDataSources/start') diff --git a/frontends/ui/src/features/layout/types.ts b/frontends/ui/src/features/layout/types.ts index a2ee55b4..1291039c 100644 --- a/frontends/ui/src/features/layout/types.ts +++ b/frontends/ui/src/features/layout/types.ts @@ -35,6 +35,8 @@ export interface LayoutState { enabledDataSourceIds: string[] /** Current theme mode */ theme: ThemeMode + /** Whether the new UI experience is enabled */ + newUiEnabled: boolean /** Dynamic data sources from API (null = not loaded yet) */ availableDataSources: DataSourceFromAPI[] | null /** Whether the knowledge layer (file upload) is available */ @@ -73,6 +75,8 @@ export interface LayoutActions { setEnabledDataSources: (ids: string[]) => void /** Set the theme mode */ setTheme: (theme: ThemeMode) => void + /** Enable or disable the new UI experience */ + setNewUiEnabled: (enabled: boolean) => void /** Fetch data sources from API. Only web_search is enabled by default */ fetchDataSources: (authToken?: string) => Promise /** Disable all non-web sources (keep only web_search enabled) */