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) */