+
+ {IS_MACOS && (
+
+
+ setSearchTerm(e.target.value)}
+ />
+ {searchTerm && (
+
+ )}
+
+ )}
+
+
+ {favoritedThreads.length > 0 && (
+ <>
+
+
+ {t('common:favorites')}
+
+
+
+
+
+
+
+ {
+ unstarAllThreads()
+ toast.success(
+ t('common:toast.allThreadsUnfavorited.title'),
+ {
+ id: 'unfav-all-threads',
+ description: t(
+ 'common:toast.allThreadsUnfavorited.description'
+ ),
+ }
+ )
+ }}
+ >
+
+ {t('common:unstarAll')}
+
+
+
+
+
+
+
+ {favoritedThreads.length === 0 && (
+
+ {t('chat.status.empty', { ns: 'chat' })}
+
+ )}
+
+ >
+ )}
+
+ {unFavoritedThreads.length > 0 && (
-
- {t('common:favorites')}
+
+ {t('common:recents')}
-
-
-
-
-
- {
- unstarAllThreads()
- toast.success(t('common:toast.allThreadsUnfavorited.title'), {
- id: 'unfav-all-threads',
- description: t('common:toast.allThreadsUnfavorited.description'),
- })
- }}
- >
-
- {t('common:unstarAll')}
-
-
-
+
-
-
- {favoritedThreads.length === 0 && (
-
- {t('chat.status.empty', { ns: 'chat' })}
-
- )}
-
- >
- )}
+ )}
- {unFavoritedThreads.length > 0 && (
-
-
- {t('common:recents')}
-
-
-
-
-
- )}
-
- {filteredThreads.length === 0 && searchTerm.length > 0 && (
-
-
-
-
- {t('common:noResultsFound')}
-
-
-
- {t('common:noResultsFoundDesc')}
-
-
- )}
-
- {Object.keys(threads).length === 0 && !searchTerm && (
- <>
+ {filteredThreads.length === 0 && searchTerm.length > 0 && (
-
+
- {t('common:noThreadsYet')}
+ {t('common:noResultsFound')}
- {t('common:noThreadsYetDesc')}
+ {t('common:noResultsFoundDesc')}
- >
- )}
+ )}
-
-
+ {Object.keys(threads).length === 0 && !searchTerm && (
+ <>
+
+
+
+
+ {t('common:noThreadsYet')}
+
+
+
+ {t('common:noThreadsYetDesc')}
+
+
+ >
+ )}
+
+
+
+
-
+
{mainMenus.map((menu) => {
const isActive =
currentPath.includes(route.settings.index) &&
@@ -324,6 +451,7 @@ const LeftPanel = () => {
isSmallScreen && setLeftPanel(false)}
data-test-id={`menu-${menu.title}`}
className={cn(
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
@@ -342,8 +470,8 @@ const LeftPanel = () => {
-
-
+
+ >
)
}
diff --git a/web-app/src/containers/ProvidersMenu.tsx b/web-app/src/containers/ProvidersMenu.tsx
deleted file mode 100644
index 7da1d2f83..000000000
--- a/web-app/src/containers/ProvidersMenu.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { route } from '@/constants/routes'
-import { useModelProvider } from '@/hooks/useModelProvider'
-import { cn, getProviderTitle } from '@/lib/utils'
-import { useNavigate, useMatches, Link } from '@tanstack/react-router'
-import { IconArrowLeft, IconCirclePlus } from '@tabler/icons-react'
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@/components/ui/dialog'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { useCallback, useState } from 'react'
-import { openAIProviderSettings } from '@/mock/data'
-import ProvidersAvatar from '@/containers/ProvidersAvatar'
-import cloneDeep from 'lodash/cloneDeep'
-import { toast } from 'sonner'
-import { useTranslation } from '@/i18n/react-i18next-compat'
-
-const ProvidersMenu = ({
- stepSetupRemoteProvider,
-}: {
- stepSetupRemoteProvider: boolean
-}) => {
- const { providers, addProvider } = useModelProvider()
- const navigate = useNavigate()
- const matches = useMatches()
- const [name, setName] = useState('')
- const { t } = useTranslation()
-
- const createProvider = useCallback(() => {
- if (providers.some((e) => e.provider === name)) {
- toast.error(t('providerAlreadyExists', { name }))
- return
- }
- const newProvider = {
- provider: name,
- active: true,
- models: [],
- settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
- api_key: '',
- base_url: 'https://api.openai.com/v1',
- }
- addProvider(newProvider)
- setTimeout(() => {
- navigate({
- to: route.settings.providers,
- params: {
- providerName: name,
- },
- })
- }, 0)
- }, [providers, name, addProvider, t, navigate])
-
- return (
-
-
-
-
- {t('common:back')}
-
-
-
- {providers.map((provider, index) => {
- const isActive = matches.some(
- (match) =>
- match.routeId === '/settings/providers/$providerName' &&
- 'providerName' in match.params &&
- match.params.providerName === provider.provider
- )
-
- return (
-
-
- navigate({
- to: route.settings.providers,
- params: {
- providerName: provider.provider,
- },
- ...(stepSetupRemoteProvider
- ? { search: { step: 'setup_remote_provider' } }
- : {}),
- })
- }
- >
-
-
- {getProviderTitle(provider.provider)}
-
-
-
- )
- })}
-
-
-
-
- )
-}
-
-export default ProvidersMenu
diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx
index 5d5e3ef5b..8aea3b501 100644
--- a/web-app/src/containers/SettingsMenu.tsx
+++ b/web-app/src/containers/SettingsMenu.tsx
@@ -1,20 +1,56 @@
-import { Link, useMatches } from '@tanstack/react-router'
+import { Link } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useTranslation } from '@/i18n/react-i18next-compat'
-import { useModelProvider } from '@/hooks/useModelProvider'
+import { useState, useEffect } from 'react'
+import {
+ IconChevronDown,
+ IconChevronRight,
+ IconMenu2,
+ IconX,
+} from '@tabler/icons-react'
+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'
const SettingsMenu = () => {
const { t } = useTranslation()
- const { providers } = useModelProvider()
- const { experimentalFeatures } = useGeneralSetting()
- const firstItemProvider =
- providers.length > 0 ? providers[0].provider : 'llama.cpp'
+ const [expandedProviders, setExpandedProviders] = useState(false)
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
const matches = useMatches()
- const isActive = matches.some(
+ 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)
+ const activeProviders = providers.filter((provider) => provider.active)
+
+ // Check if current route has a providerName parameter and expand providers submenu
+ useEffect(() => {
+ const hasProviderName = matches.some(
+ (match) =>
+ match.routeId === '/settings/providers/$providerName' &&
+ 'providerName' in match.params
+ )
+ const isProvidersRoute = matches.some(
+ (match) => match.routeId === '/settings/providers/'
+ )
+ if (hasProviderName || isProvidersRoute) {
+ setExpandedProviders(true)
+ }
+ }, [matches])
+
+ // Check if we're in the setup remote provider step
+ const stepSetupRemoteProvider = matches.some(
(match) =>
- match.routeId === '/settings/providers/$providerName' &&
- 'providerName' in match.params
+ match.search &&
+ typeof match.search === 'object' &&
+ 'step' in match.search &&
+ match.search.step === 'setup_remote_provider'
)
const menuSettings = [
@@ -30,6 +66,11 @@ const SettingsMenu = () => {
title: 'common:privacy',
route: route.settings.privacy,
},
+ {
+ title: 'common:modelProviders',
+ route: route.settings.model_providers,
+ hasSubMenu: activeProviders.length > 0,
+ },
{
title: 'common:keyboardShortcuts',
route: route.settings.shortcuts,
@@ -61,52 +102,113 @@ const SettingsMenu = () => {
},
]
+ const toggleProvidersExpansion = () => {
+ setExpandedProviders(!expandedProviders)
+ }
+
+ const toggleMenu = () => {
+ setIsMenuOpen(!isMenuOpen)
+ }
+
return (
-
-
- {menuSettings.map((menu, index) => {
- // Render the menu item
- const menuItem = (
-
-
{t(menu.title)}
-
- )
+ <>
+
+
+
+ {menuSettings.map((menu) => (
+
+
+
+ {t(menu.title)}
+ {menu.hasSubMenu && (
+
+ )}
+
+
- if (index === 2) {
- return (
-
-
{menuItem}
+ {/* Sub-menu for model providers */}
+ {menu.hasSubMenu && expandedProviders && (
+
+ {activeProviders.map((provider) => {
+ const isActive = matches.some(
+ (match) =>
+ match.routeId === '/settings/providers/$providerName' &&
+ 'providerName' in match.params &&
+ match.params.providerName === provider.provider
+ )
- {/* Model Providers Link with default parameter */}
- {isActive ? (
-
- {t('common:modelProviders')}
-
- ) : (
-
-
- {t('common:modelProviders')}
-
-
- )}
-
- )
- }
-
- // For other menu items, just render them normally
- return menuItem
- })}
+ return (
+
+
+ navigate({
+ to: route.settings.providers,
+ params: {
+ providerName: provider.provider,
+ },
+ ...(stepSetupRemoteProvider
+ ? { search: { step: 'setup_remote_provider' } }
+ : {}),
+ })
+ }
+ >
+
+
+ {getProviderTitle(provider.provider)}
+
+
+
+ )
+ })}
+
+ )}
+
+ ))}
+
-
+ >
)
}
diff --git a/web-app/src/containers/ThreadList.tsx b/web-app/src/containers/ThreadList.tsx
index e4ed8aa93..bd3825eee 100644
--- a/web-app/src/containers/ThreadList.tsx
+++ b/web-app/src/containers/ThreadList.tsx
@@ -20,8 +20,10 @@ import {
IconStar,
} from '@tabler/icons-react'
import { useThreads } from '@/hooks/useThreads'
+import { useLeftPanel } from '@/hooks/useLeftPanel'
import { cn } from '@/lib/utils'
import { route } from '@/constants/routes'
+import { useSmallScreen } from '@/hooks/useMediaQuery'
import {
DropdownMenu,
@@ -55,6 +57,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
isDragging,
} = useSortable({ id: thread.id, disabled: true })
+ const isSmallScreen = useSmallScreen()
+ const { setLeftPanel } = useLeftPanel()
+
const style = {
transform: CSS.Transform.toString(transform),
transition,
@@ -75,7 +80,11 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
const handleClick = () => {
if (!isDragging) {
- navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
+ // Only close panel and navigate if the thread is not already active
+ if (!isActive) {
+ if (isSmallScreen) setLeftPanel(false)
+ navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
+ }
}
}
@@ -85,7 +94,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
return (thread.title || '').replace(/
]*>|<\/span>/g, '')
}, [thread.title])
- const [title, setTitle] = useState(plainTitleForRename || t('common:newThread'))
+ const [title, setTitle] = useState(
+ plainTitleForRename || t('common:newThread')
+ )
return (
{
setOpenDropdown(false)
toast.success(t('common:toast.renameThread.title'), {
id: 'rename-thread',
- description: t('common:toast.renameThread.description', { title }),
+ description: t(
+ 'common:toast.renameThread.description',
+ { title }
+ ),
})
}}
>
@@ -231,7 +245,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
setOpenDropdown(false)
toast.success(t('common:toast.deleteThread.title'), {
id: 'delete-thread',
- description: t('common:toast.deleteThread.description'),
+ description: t(
+ 'common:toast.deleteThread.description'
+ ),
})
setTimeout(() => {
navigate({ to: route.home })
diff --git a/web-app/src/containers/dialogs/AddEditAssistant.tsx b/web-app/src/containers/dialogs/AddEditAssistant.tsx
index 7a1242027..f4e327e35 100644
--- a/web-app/src/containers/dialogs/AddEditAssistant.tsx
+++ b/web-app/src/containers/dialogs/AddEditAssistant.tsx
@@ -378,73 +378,27 @@ export default function AddEditAssistant({
{paramsKeys.map((key, index) => (
-
-
- handleParameterChange(index, e.target.value, 'key')
- }
- placeholder={t('assistants:key')}
- className="w-24"
- />
+
+
+
+ handleParameterChange(index, e.target.value, 'key')
+ }
+ placeholder={t('assistants:key')}
+ className="w-full sm:w-24"
+ />
-
-
-
-
-
-
-
-
-
- handleParameterChange(index, 'string', 'type')
- }
- >
- {t('assistants:stringValue')}
-
-
- handleParameterChange(index, 'number', 'type')
- }
- >
- {t('assistants:numberValue')}
-
-
- handleParameterChange(index, 'boolean', 'type')
- }
- >
- {t('assistants:booleanValue')}
-
-
- handleParameterChange(index, 'json', 'type')
- }
- >
- {t('assistants:jsonValue')}
-
-
-
-
- {paramsTypes[index] === 'boolean' ? (
-
+
@@ -454,48 +408,98 @@ export default function AddEditAssistant({
/>
-
+
- handleParameterChange(index, true, 'value')
+ handleParameterChange(index, 'string', 'type')
}
>
- {t('assistants:trueValue')}
+ {t('assistants:stringValue')}
- handleParameterChange(index, false, 'value')
+ handleParameterChange(index, 'number', 'type')
}
>
- {t('assistants:falseValue')}
+ {t('assistants:numberValue')}
+
+
+ handleParameterChange(index, 'boolean', 'type')
+ }
+ >
+ {t('assistants:booleanValue')}
+
+
+ handleParameterChange(index, 'json', 'type')
+ }
+ >
+ {t('assistants:jsonValue')}
- ) : paramsTypes[index] === 'json' ? (
-
- handleParameterChange(index, e.target.value, 'value')
- }
- placeholder={t('assistants:jsonValuePlaceholder')}
- className="flex-1"
- />
- ) : (
-
- handleParameterChange(index, e.target.value, 'value')
- }
- type={paramsTypes[index] === 'number' ? 'number' : 'text'}
- placeholder={t('assistants:value')}
- className="flex-1"
- />
- )}
+ {paramsTypes[index] === 'boolean' ? (
+
+
+
+
+
+
+
+
+
+ handleParameterChange(index, true, 'value')
+ }
+ >
+ {t('assistants:trueValue')}
+
+
+ handleParameterChange(index, false, 'value')
+ }
+ >
+ {t('assistants:falseValue')}
+
+
+
+ ) : paramsTypes[index] === 'json' ? (
+
+ handleParameterChange(index, e.target.value, 'value')
+ }
+ placeholder={t('assistants:jsonValuePlaceholder')}
+ className="sm:flex-1 h-[36px] w-full"
+ />
+ ) : (
+
+ handleParameterChange(index, e.target.value, 'value')
+ }
+ type={paramsTypes[index] === 'number' ? 'number' : 'text'}
+ placeholder={t('assistants:value')}
+ className="sm:flex-1 h-[36px] w-full"
+ />
+ )}
+
handleRemoveParameter(index)}
diff --git a/web-app/src/containers/dialogs/CortexFailureDialog.tsx b/web-app/src/containers/dialogs/CortexFailureDialog.tsx
index b28281f54..48d08569d 100644
--- a/web-app/src/containers/dialogs/CortexFailureDialog.tsx
+++ b/web-app/src/containers/dialogs/CortexFailureDialog.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { listen } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/core'
-import { t } from 'i18next'
+
import {
Dialog,
DialogContent,
@@ -11,8 +11,10 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
+import { useTranslation } from '@/i18n'
export function CortexFailureDialog() {
+ const { t } = useTranslation()
const [showDialog, setShowDialog] = useState(false)
useEffect(() => {
@@ -52,15 +54,10 @@ export function CortexFailureDialog() {