Skip to content
Merged
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
5 changes: 2 additions & 3 deletions web-app/src/containers/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
const { prompt, setPrompt } = usePrompt()
const { currentThreadId } = useThreads()
const { t } = useTranslation()
const { spellCheckChatInput, experimentalFeatures } = useGeneralSetting()
const { spellCheckChatInput } = useGeneralSetting()

const maxRows = 10

Expand Down Expand Up @@ -86,9 +86,9 @@
const servers = await getConnectedServers()
setConnectedServers(servers)
} catch (error) {
console.error('Failed to get connected servers:', error)
setConnectedServers([])
}

Check warning on line 91 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

89-91 lines are not covered with tests
}

checkConnectedServers()
Expand All @@ -110,12 +110,12 @@
setHasMmproj(hasLocalMmproj)
} else {
// For non-llamacpp providers, only check vision capability
setHasMmproj(true)
}

Check warning on line 114 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

113-114 lines are not covered with tests
} catch (error) {
console.error('Error checking mmproj:', error)
setHasMmproj(false)
}

Check warning on line 118 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

116-118 lines are not covered with tests
}
}

Expand All @@ -131,8 +131,8 @@
return
}
if (!prompt.trim() && uploadedFiles.length === 0) {
return
}

Check warning on line 135 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

134-135 lines are not covered with tests
setMessage('')
sendMessage(
prompt,
Expand Down Expand Up @@ -173,8 +173,8 @@

useEffect(() => {
if (tooltipToolsAvailable && dropdownToolsAvailable) {
setTooltipToolsAvailable(false)
}

Check warning on line 177 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

176-177 lines are not covered with tests
}, [dropdownToolsAvailable, tooltipToolsAvailable])

// Focus when thread changes
Expand All @@ -196,39 +196,39 @@

const stopStreaming = useCallback(
(threadId: string) => {
abortControllers[threadId]?.abort()
cancelToolCall?.()
},

Check warning on line 201 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

199-201 lines are not covered with tests
[abortControllers, cancelToolCall]
)

const fileInputRef = useRef<HTMLInputElement>(null)

const handleAttachmentClick = () => {
fileInputRef.current?.click()
}

Check warning on line 209 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

208-209 lines are not covered with tests

const handleRemoveFile = (indexToRemove: number) => {
setUploadedFiles((prev) =>
prev.filter((_, index) => index !== indexToRemove)
)
}

Check warning on line 215 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

212-215 lines are not covered with tests

const getFileTypeFromExtension = (fileName: string): string => {
const extension = fileName.toLowerCase().split('.').pop()
switch (extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg'
case 'png':
return 'image/png'
default:
return ''
}
}

Check warning on line 228 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

218-228 lines are not covered with tests

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files

Check warning on line 231 in web-app/src/containers/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

231 line is not covered with tests

if (files && files.length > 0) {
const maxSize = 10 * 1024 * 1024 // 10MB in bytes
Expand Down Expand Up @@ -586,8 +586,7 @@
</TooltipProvider>
)}

{experimentalFeatures &&
selectedModel?.capabilities?.includes('tools') &&
{selectedModel?.capabilities?.includes('tools') &&
hasActiveMCPServers && (
<TooltipProvider>
<Tooltip
Expand Down
15 changes: 4 additions & 11 deletions web-app/src/containers/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import { useMatches, useNavigate } from '@tanstack/react-router'
import { cn } from '@/lib/utils'

import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useModelProvider } from '@/hooks/useModelProvider'
import { getProviderTitle } from '@/lib/utils'
import ProvidersAvatar from '@/containers/ProvidersAvatar'
Expand All @@ -23,7 +22,6 @@ const SettingsMenu = () => {
const matches = useMatches()
const navigate = useNavigate()

const { experimentalFeatures } = useGeneralSetting()
const { providers } = useModelProvider()

// Filter providers that have active API keys (or are llama.cpp which doesn't need one)
Expand Down Expand Up @@ -79,15 +77,10 @@ const SettingsMenu = () => {
title: 'common:hardware',
route: route.settings.hardware,
},
// Only show MCP Servers when experimental features are enabled
...(experimentalFeatures
? [
{
title: 'common:mcp-servers',
route: route.settings.mcp_servers,
},
]
: []),
{
title: 'common:mcp-servers',
route: route.settings.mcp_servers,
},
{
title: 'common:local_api_server',
route: route.settings.local_api_server,
Expand Down
114 changes: 54 additions & 60 deletions web-app/src/containers/__tests__/SettingsMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SettingsMenu from '../SettingsMenu'
import { useNavigate, useMatches } from '@tanstack/react-router'
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useAppState } from '@/hooks/useAppState'

// Mock dependencies
vi.mock('@tanstack/react-router', () => ({
Expand All @@ -25,9 +24,7 @@ vi.mock('@/i18n/react-i18next-compat', () => ({
}))

vi.mock('@/hooks/useGeneralSetting', () => ({
useGeneralSetting: vi.fn(() => ({
experimentalFeatures: false,
})),
useGeneralSetting: vi.fn(() => ({})),
}))

vi.mock('@/hooks/useModelProvider', () => ({
Expand Down Expand Up @@ -71,14 +68,14 @@ describe('SettingsMenu', () => {

beforeEach(() => {
vi.clearAllMocks()

vi.mocked(useNavigate).mockReturnValue(mockNavigate)
vi.mocked(useMatches).mockReturnValue(mockMatches)
})

it('renders all menu items', () => {
render(<SettingsMenu />)

expect(screen.getByText('common:general')).toBeInTheDocument()
expect(screen.getByText('common:appearance')).toBeInTheDocument()
expect(screen.getByText('common:privacy')).toBeInTheDocument()
Expand All @@ -88,29 +85,14 @@ describe('SettingsMenu', () => {
expect(screen.getByText('common:local_api_server')).toBeInTheDocument()
expect(screen.getByText('common:https_proxy')).toBeInTheDocument()
expect(screen.getByText('common:extensions')).toBeInTheDocument()
})

it('does not show MCP Servers when experimental features disabled', () => {
render(<SettingsMenu />)

expect(screen.queryByText('common:mcp-servers')).not.toBeInTheDocument()
})

it('shows MCP Servers when experimental features enabled', () => {
vi.mocked(useGeneralSetting).mockReturnValue({
experimentalFeatures: true,
})

render(<SettingsMenu />)

expect(screen.getByText('common:mcp-servers')).toBeInTheDocument()
})

it('shows provider expansion chevron when providers are active', () => {
render(<SettingsMenu />)

const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
expect(chevron).toBeInTheDocument()
Expand All @@ -119,14 +101,14 @@ describe('SettingsMenu', () => {
it('expands providers submenu when chevron is clicked', async () => {
const user = userEvent.setup()
render(<SettingsMenu />)

const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
if (!chevron) throw new Error('Chevron button not found')
await user.click(chevron)

expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument()
expect(screen.getByTestId('provider-avatar-llama.cpp')).toBeInTheDocument()
})
Expand All @@ -138,52 +120,56 @@ describe('SettingsMenu', () => {
params: { providerName: 'openai' },
},
])

render(<SettingsMenu />)

expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument()
expect(screen.getByTestId('provider-avatar-llama.cpp')).toBeInTheDocument()
})

it('highlights active provider in submenu', async () => {
const user = userEvent.setup()

vi.mocked(useMatches).mockReturnValue([
{
routeId: '/settings/providers/$providerName',
params: { providerName: 'openai' },
},
])

render(<SettingsMenu />)

// First expand the providers submenu
const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
if (chevron) await user.click(chevron)

const openaiProvider = screen.getByTestId('provider-avatar-openai').closest('div')

const openaiProvider = screen
.getByTestId('provider-avatar-openai')
.closest('div')
expect(openaiProvider).toBeInTheDocument()
})

it('navigates to provider when provider is clicked', async () => {
const user = userEvent.setup()
render(<SettingsMenu />)

// First expand the providers
const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
if (!chevron) throw new Error('Chevron button not found')
await user.click(chevron)

// Then click on a provider
const openaiProvider = screen.getByTestId('provider-avatar-openai').closest('div')
const openaiProvider = screen
.getByTestId('provider-avatar-openai')
.closest('div')
await user.click(openaiProvider!)

expect(mockNavigate).toHaveBeenCalledWith({
to: '/settings/providers/$providerName',
params: { providerName: 'openai' },
Expand All @@ -192,18 +178,22 @@ describe('SettingsMenu', () => {

it('shows mobile menu toggle button', () => {
render(<SettingsMenu />)

const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' })

const menuToggle = screen.getByRole('button', {
name: 'Toggle settings menu',
})
expect(menuToggle).toBeInTheDocument()
})

it('opens mobile menu when toggle is clicked', async () => {
const user = userEvent.setup()
render(<SettingsMenu />)

const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' })

const menuToggle = screen.getByRole('button', {
name: 'Toggle settings menu',
})
await user.click(menuToggle)

// Menu should now be visible
const menu = screen.getByText('common:general').closest('div')
expect(menu).toHaveClass('flex')
Expand All @@ -212,38 +202,40 @@ describe('SettingsMenu', () => {
it('closes mobile menu when X is clicked', async () => {
const user = userEvent.setup()
render(<SettingsMenu />)

// Open menu first
const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' })
const menuToggle = screen.getByRole('button', {
name: 'Toggle settings menu',
})
await user.click(menuToggle)

// Then close it
await user.click(menuToggle)

// Just verify the toggle button is still there after clicking twice
expect(menuToggle).toBeInTheDocument()
})

it('hides llamacpp provider during setup remote provider step', async () => {
const user = userEvent.setup()

vi.mocked(useMatches).mockReturnValue([
{
routeId: '/settings/providers/',
params: {},
search: { step: 'setup_remote_provider' },
},
])

render(<SettingsMenu />)

// First expand the providers submenu
const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
if (chevron) await user.click(chevron)

// llamacpp provider div should have hidden class
const llamacppElement = screen.getByTestId('provider-avatar-llama.cpp')
expect(llamacppElement.parentElement).toHaveClass('hidden')
Expand All @@ -253,7 +245,7 @@ describe('SettingsMenu', () => {

it('filters out inactive providers from submenu', async () => {
const user = userEvent.setup()

vi.mocked(useModelProvider).mockReturnValue({
providers: [
{
Expand All @@ -268,17 +260,19 @@ describe('SettingsMenu', () => {
},
],
})

render(<SettingsMenu />)

// Expand providers
const chevronButtons = screen.getAllByRole('button')
const chevron = chevronButtons.find(button =>
const chevron = chevronButtons.find((button) =>
button.querySelector('svg.tabler-icon-chevron-right')
)
if (chevron) await user.click(chevron)

expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument()
expect(screen.queryByTestId('provider-avatar-anthropic')).not.toBeInTheDocument()
expect(
screen.queryByTestId('provider-avatar-anthropic')
).not.toBeInTheDocument()
})
})
})
Loading
Loading