+
-
-
{t('common:placeholder.chatInput')}
+
+
+ {t('common:placeholder.chatInput')}
+
@@ -42,7 +46,9 @@ export function ChatWidthSwitcher() {
onClick={() => setChatWidth('full')}
>
-
{t('common:fullWidth')}
+
+ {t('common:fullWidth')}
+
{chatWidth === 'full' && (
)}
@@ -52,8 +58,10 @@ export function ChatWidthSwitcher() {
-
-
{t('common:placeholder.chatInput')}
+
+
+ {t('common:placeholder.chatInput')}
+
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/routeTree.gen.ts b/web-app/src/routeTree.gen.ts
index 52782cb2e..bbd3db391 100644
--- a/web-app/src/routeTree.gen.ts
+++ b/web-app/src/routeTree.gen.ts
@@ -27,6 +27,7 @@ import { Route as SettingsGeneralImport } from './routes/settings/general'
import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
+import { Route as SettingsProvidersIndexImport } from './routes/settings/providers/index'
import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
// Create/Update Routes
@@ -127,6 +128,12 @@ const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({
getParentRoute: () => rootRoute,
} as any)
+const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({
+ id: '/settings/providers/',
+ path: '/settings/providers/',
+ getParentRoute: () => rootRoute,
+} as any)
+
const SettingsProvidersProviderNameRoute =
SettingsProvidersProviderNameImport.update({
id: '/settings/providers/$providerName',
@@ -257,6 +264,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsProvidersProviderNameImport
parentRoute: typeof rootRoute
}
+ '/settings/providers/': {
+ id: '/settings/providers/'
+ path: '/settings/providers'
+ fullPath: '/settings/providers'
+ preLoaderRoute: typeof SettingsProvidersIndexImport
+ parentRoute: typeof rootRoute
+ }
}
}
@@ -280,6 +294,7 @@ export interface FileRoutesByFullPath {
'/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
+ '/settings/providers': typeof SettingsProvidersIndexRoute
}
export interface FileRoutesByTo {
@@ -300,6 +315,7 @@ export interface FileRoutesByTo {
'/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
+ '/settings/providers': typeof SettingsProvidersIndexRoute
}
export interface FileRoutesById {
@@ -321,6 +337,7 @@ export interface FileRoutesById {
'/settings/shortcuts': typeof SettingsShortcutsRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
+ '/settings/providers/': typeof SettingsProvidersIndexRoute
}
export interface FileRouteTypes {
@@ -343,6 +360,7 @@ export interface FileRouteTypes {
| '/settings/shortcuts'
| '/threads/$threadId'
| '/settings/providers/$providerName'
+ | '/settings/providers'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -362,6 +380,7 @@ export interface FileRouteTypes {
| '/settings/shortcuts'
| '/threads/$threadId'
| '/settings/providers/$providerName'
+ | '/settings/providers'
id:
| '__root__'
| '/'
@@ -381,6 +400,7 @@ export interface FileRouteTypes {
| '/settings/shortcuts'
| '/threads/$threadId'
| '/settings/providers/$providerName'
+ | '/settings/providers/'
fileRoutesById: FileRoutesById
}
@@ -402,6 +422,7 @@ export interface RootRouteChildren {
SettingsShortcutsRoute: typeof SettingsShortcutsRoute
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
+ SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
@@ -422,6 +443,7 @@ const rootRouteChildren: RootRouteChildren = {
SettingsShortcutsRoute: SettingsShortcutsRoute,
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
+ SettingsProvidersIndexRoute: SettingsProvidersIndexRoute,
}
export const routeTree = rootRoute
@@ -450,7 +472,8 @@ export const routeTree = rootRoute
"/settings/privacy",
"/settings/shortcuts",
"/threads/$threadId",
- "/settings/providers/$providerName"
+ "/settings/providers/$providerName",
+ "/settings/providers/"
]
},
"/": {
@@ -503,6 +526,9 @@ export const routeTree = rootRoute
},
"/settings/providers/$providerName": {
"filePath": "settings/providers/$providerName.tsx"
+ },
+ "/settings/providers/": {
+ "filePath": "settings/providers/index.tsx"
}
}
}
diff --git a/web-app/src/routes/settings/appearance.tsx b/web-app/src/routes/settings/appearance.tsx
index df0da98fa..3cba3eed5 100644
--- a/web-app/src/routes/settings/appearance.tsx
+++ b/web-app/src/routes/settings/appearance.tsx
@@ -35,7 +35,7 @@ function Appareances() {
{t('common:settings')}
-
+
@@ -55,26 +55,31 @@ function Appareances() {
}
/>
}
/>
}
/>
}
/>
}
/>
{t('common:settings')}
-
+
@@ -222,6 +222,7 @@ function General() {
@@ -273,13 +275,15 @@ function General() {
})}
-
-
- {janDataFolder}
-
+
+
+
+ {janDataFolder}
+
+