✨enhancement: setting responsive (#5615)
* ✨feat: setting responsive * 🧹cleanup: feeback PR * 🧹cleanup: unused className * 🧹cleanup: unused props
This commit is contained in:
parent
5918c9cd6f
commit
662879bb5d
@ -5,6 +5,7 @@ export const route = {
|
||||
assistant: '/assistant',
|
||||
settings: {
|
||||
index: '/settings',
|
||||
model_providers: '/settings/providers',
|
||||
providers: '/settings/providers/$providerName',
|
||||
general: '/settings/general',
|
||||
appearance: '/settings/appearance',
|
||||
|
||||
@ -10,43 +10,63 @@ type CardProps = {
|
||||
type CardItemProps = {
|
||||
title?: string | ReactNode
|
||||
description?: string | ReactNode
|
||||
descriptionOutside?: string | ReactNode
|
||||
align?: 'start' | 'center' | 'end'
|
||||
actions?: ReactNode
|
||||
column?: boolean
|
||||
className?: string
|
||||
classNameWrapperAction?: string
|
||||
}
|
||||
|
||||
export function CardItem({
|
||||
title,
|
||||
description,
|
||||
descriptionOutside,
|
||||
className,
|
||||
classNameWrapperAction,
|
||||
align = 'center',
|
||||
column,
|
||||
actions,
|
||||
}: CardItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between mt-2 first:mt-0 border-b border-main-view-fg/5 pb-3 last:border-none last:pb-0 gap-8',
|
||||
className,
|
||||
align === 'start' && 'items-start',
|
||||
align === 'center' && 'items-center',
|
||||
align === 'end' && 'items-end',
|
||||
column && 'flex-col gap-y-0 items-start'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h1 className="font-medium">{title}</h1>
|
||||
{description && (
|
||||
<span className="text-main-view-fg/70 leading-normal">
|
||||
{description}
|
||||
</span>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between mt-2 first:mt-0 border-b border-main-view-fg/5 pb-3 last:border-none last:pb-0 gap-8',
|
||||
descriptionOutside && 'border-0',
|
||||
align === 'start' && 'items-start',
|
||||
align === 'center' && 'items-center',
|
||||
align === 'end' && 'items-end',
|
||||
column && 'flex-col gap-y-0 items-start',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h1 className="font-medium">{title}</h1>
|
||||
{description && (
|
||||
<span className="text-main-view-fg/70 leading-normal">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0',
|
||||
classNameWrapperAction,
|
||||
column && 'w-full'
|
||||
)}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className={cn('shrink-0', column && 'w-full')}>{actions}</div>
|
||||
{descriptionOutside && (
|
||||
<span className="text-main-view-fg/70 leading-normal">
|
||||
{descriptionOutside}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,27 +9,31 @@ export function ChatWidthSwitcher() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:gap-4">
|
||||
<button
|
||||
className={cn(
|
||||
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer',
|
||||
'w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2 pb-2 cursor-pointer ',
|
||||
chatWidth === 'compact' && 'border-accent'
|
||||
)}
|
||||
onClick={() => setChatWidth('compact')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">{t('common:compactWidth')}</span>
|
||||
<span className="font-medium text-xs font-sans">
|
||||
{t('common:compactWidth')}
|
||||
</span>
|
||||
{chatWidth === 'compact' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto p-2">
|
||||
<div className="flex flex-col px-10 gap-2 mt-2">
|
||||
<div className="flex flex-col px-6 gap-2 mt-2">
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-sm flex items-center truncate">
|
||||
<span className="text-main-view-fg/50 line-clamp-1">
|
||||
{t('common:placeholder.chatInput')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -42,7 +46,9 @@ export function ChatWidthSwitcher() {
|
||||
onClick={() => setChatWidth('full')}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
||||
<span className="font-medium text-xs font-sans">{t('common:fullWidth')}</span>
|
||||
<span className="font-medium text-xs font-sans">
|
||||
{t('common:fullWidth')}
|
||||
</span>
|
||||
{chatWidth === 'full' && (
|
||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||
)}
|
||||
@ -52,8 +58,10 @@ export function ChatWidthSwitcher() {
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
|
||||
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
|
||||
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-sm flex items-center">
|
||||
<span className="text-main-view-fg/50">
|
||||
{t('common:placeholder.chatInput')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<div className="w-44 py-2 border-r border-main-view-fg/5 pb-10 overflow-y-auto">
|
||||
<Link to={route.settings.general}>
|
||||
<div className="flex items-center gap-0.5 ml-3 mb-4 mt-1">
|
||||
<IconArrowLeft size={16} className="text-main-view-fg/70" />
|
||||
<span className="text-main-view-fg/80">{t('common:back')}</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="first-step-setup-remote-provider">
|
||||
{providers.map((provider, index) => {
|
||||
const isActive = matches.some(
|
||||
(match) =>
|
||||
match.routeId === '/settings/providers/$providerName' &&
|
||||
'providerName' in match.params &&
|
||||
match.params.providerName === provider.provider
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={index} className="flex flex-col px-2 my-1.5 ">
|
||||
<div
|
||||
className={cn(
|
||||
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
|
||||
isActive && 'bg-main-view-fg/5',
|
||||
// hidden for llama.cpp provider for setup remote provider
|
||||
provider.provider === 'llama.cpp' &&
|
||||
stepSetupRemoteProvider &&
|
||||
'hidden'
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: provider.provider,
|
||||
},
|
||||
...(stepSetupRemoteProvider
|
||||
? { search: { step: 'setup_remote_provider' } }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<ProvidersAvatar provider={provider} />
|
||||
<div className="truncate">
|
||||
<span>{getProviderTitle(provider.provider)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<div className="flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80">
|
||||
<IconCirclePlus size={18} />
|
||||
<span>{t('provider:addProvider')}</span>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('provider:addOpenAIProvider')}</DialogTitle>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-2"
|
||||
placeholder={t('provider:enterNameForProvider')}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent key from being captured by parent components
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button disabled={!name} onClick={createProvider}>
|
||||
{t('common:create')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProvidersMenu
|
||||
@ -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 (
|
||||
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
||||
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
||||
{menuSettings.map((menu, index) => {
|
||||
// Render the menu item
|
||||
const menuItem = (
|
||||
<Link
|
||||
key={menu.title}
|
||||
to={menu.route}
|
||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
|
||||
>
|
||||
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||
</Link>
|
||||
)
|
||||
<>
|
||||
<button
|
||||
className="fixed top-4 right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20"
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle settings menu"
|
||||
>
|
||||
{isMenuOpen ? (
|
||||
<IconX size={18} className="text-main-view-fg relative z-20" />
|
||||
) : (
|
||||
<IconMenu2 size={18} className="text-main-view-fg relative z-20" />
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5 bg-main-view',
|
||||
'sm:flex',
|
||||
isMenuOpen
|
||||
? 'flex fixed sm:hidden top-0 z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg'
|
||||
: 'hidden'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
||||
{menuSettings.map((menu) => (
|
||||
<div key={menu.title}>
|
||||
<Link
|
||||
to={menu.route}
|
||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||
{menu.hasSubMenu && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleProvidersExpansion()
|
||||
}}
|
||||
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||
>
|
||||
{expandedProviders ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
if (index === 2) {
|
||||
return (
|
||||
<div key={menu.title}>
|
||||
<span className="mb-1 block">{menuItem}</span>
|
||||
{/* Sub-menu for model providers */}
|
||||
{menu.hasSubMenu && expandedProviders && (
|
||||
<div className="ml-2 mt-1 space-y-1 first-step-setup-remote-provider">
|
||||
{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 ? (
|
||||
<div className="block px-2 mt-1 gap-1.5 py-1 w-full rounded bg-main-view-fg/5 cursor-pointer">
|
||||
<span>{t('common:modelProviders')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
key="common.modelProviders"
|
||||
to={route.settings.providers}
|
||||
params={{ providerName: firstItemProvider }}
|
||||
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded"
|
||||
>
|
||||
<span className="text-main-view-fg/80">
|
||||
{t('common:modelProviders')}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// For other menu items, just render them normally
|
||||
return menuItem
|
||||
})}
|
||||
return (
|
||||
<div key={provider.provider}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex px-2 items-center gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5 text-main-view-fg/80',
|
||||
isActive && 'bg-main-view-fg/5',
|
||||
// hidden for llama.cpp provider for setup remote provider
|
||||
provider.provider === 'llama.cpp' &&
|
||||
stepSetupRemoteProvider &&
|
||||
'hidden'
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: provider.provider,
|
||||
},
|
||||
...(stepSetupRemoteProvider
|
||||
? { search: { step: 'setup_remote_provider' } }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<ProvidersAvatar provider={provider} />
|
||||
<div className="truncate">
|
||||
<span>{getProviderTitle(provider.provider)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ function Appareances() {
|
||||
<HeaderPage>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||
<SettingsMenu />
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||
@ -55,26 +55,31 @@ function Appareances() {
|
||||
<CardItem
|
||||
title={t('settings:appearance.windowBackground')}
|
||||
description={t('settings:appearance.windowBackgroundDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={<ColorPickerAppBgColor />}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:appearance.appMainView')}
|
||||
description={t('settings:appearance.appMainViewDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={<ColorPickerAppMainView />}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:appearance.primary')}
|
||||
description={t('settings:appearance.primaryDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={<ColorPickerAppPrimaryColor />}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:appearance.accent')}
|
||||
description={t('settings:appearance.accentDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={<ColorPickerAppAccentColor />}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:appearance.destructive')}
|
||||
description={t('settings:appearance.destructiveDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={<ColorPickerAppDestructiveColor />}
|
||||
/>
|
||||
<CardItem
|
||||
|
||||
@ -205,7 +205,7 @@ function General() {
|
||||
<HeaderPage>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||
<SettingsMenu />
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||
@ -222,6 +222,7 @@ function General() {
|
||||
<CardItem
|
||||
title={t('settings:general.checkForUpdates')}
|
||||
description={t('settings:general.checkForUpdatesDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="link"
|
||||
@ -265,6 +266,7 @@ function General() {
|
||||
ns: 'settings',
|
||||
})}
|
||||
align="start"
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
@ -273,13 +275,15 @@ function General() {
|
||||
})}
|
||||
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span
|
||||
title={janDataFolder}
|
||||
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
|
||||
>
|
||||
{janDataFolder}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1 ">
|
||||
<div className="">
|
||||
<span
|
||||
title={janDataFolder}
|
||||
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80 line-clamp-1 w-fit"
|
||||
>
|
||||
{janDataFolder}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
janDataFolder && copyToClipboard(janDataFolder)
|
||||
@ -349,6 +353,7 @@ function General() {
|
||||
ns: 'settings',
|
||||
})}
|
||||
description={t('settings:dataFolder.appLogsDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
||||
@ -229,9 +229,11 @@ function LocalAPIServer() {
|
||||
title={t('settings:localApiServer.apiKey')}
|
||||
description={t('settings:localApiServer.apiKeyDesc')}
|
||||
className={cn(
|
||||
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||
isServerRunning && 'opacity-50 pointer-events-none',
|
||||
isApiKeyEmpty && showApiKeyError && 'pb-6'
|
||||
)}
|
||||
classNameWrapperAction="w-full sm:w-auto"
|
||||
actions={
|
||||
<ApiKeyInput
|
||||
showError={showApiKeyError}
|
||||
@ -243,8 +245,10 @@ function LocalAPIServer() {
|
||||
title={t('settings:localApiServer.trustedHosts')}
|
||||
description={t('settings:localApiServer.trustedHostsDesc')}
|
||||
className={cn(
|
||||
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||
isServerRunning && 'opacity-50 pointer-events-none'
|
||||
)}
|
||||
classNameWrapperAction="w-full sm:w-auto"
|
||||
actions={<TrustedHostsInput />}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@ -320,7 +320,7 @@ function MCPServers() {
|
||||
</h1>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
descriptionOutside={
|
||||
<div className="text-sm text-main-view-fg/70">
|
||||
<div>
|
||||
{t('mcp-servers:command')}: {config.command}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { Card, CardItem } from '@/containers/Card'
|
||||
import HeaderPage from '@/containers/HeaderPage'
|
||||
import ProvidersMenu from '@/containers/ProvidersMenu'
|
||||
import SettingsMenu from '@/containers/SettingsMenu'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { cn, getProviderTitle } from '@/lib/utils'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import {
|
||||
getActiveModels,
|
||||
@ -228,23 +227,13 @@ function ProviderDetail() {
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex">
|
||||
<ProvidersMenu stepSetupRemoteProvider={isSetup} />
|
||||
</div>
|
||||
<SettingsMenu />
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="font-medium text-base">
|
||||
{getProviderTitle(providerName)}
|
||||
</h1>
|
||||
<Switch
|
||||
checked={provider?.active}
|
||||
onCheckedChange={(e) => {
|
||||
if (provider) {
|
||||
updateProvider(providerName, { ...provider, active: e })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -461,7 +450,12 @@ function ProviderDetail() {
|
||||
key={modelIndex}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="font-medium">{model.id}</h1>
|
||||
<h1
|
||||
className="font-medium line-clamp-1"
|
||||
title={model.id}
|
||||
>
|
||||
{model.id}
|
||||
</h1>
|
||||
<Capabilities capabilities={capabilities} />
|
||||
</div>
|
||||
}
|
||||
|
||||
187
web-app/src/routes/settings/providers/index.tsx
Normal file
187
web-app/src/routes/settings/providers/index.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import SettingsMenu from '@/containers/SettingsMenu'
|
||||
import HeaderPage from '@/containers/HeaderPage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardItem } from '@/containers/Card'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
|
||||
import { getProviderTitle } from '@/lib/utils'
|
||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { openAIProviderSettings } from '@/mock/data'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.settings.model_providers as any)({
|
||||
component: ModelProviders,
|
||||
})
|
||||
|
||||
function ModelProviders() {
|
||||
const { t } = useTranslation()
|
||||
const { providers, addProvider, updateProvider } = useModelProvider()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState('')
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col h-full">
|
||||
<HeaderPage>
|
||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||
</HeaderPage>
|
||||
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||
<SettingsMenu />
|
||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
||||
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
|
||||
{/* Model Providers */}
|
||||
<Card
|
||||
header={
|
||||
<div className="flex items-center justify-between w-full mb-6">
|
||||
<span className="text-main-view-fg font-medium text-base">
|
||||
{t('common:modelProviders')}
|
||||
</span>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<div className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1.5 py-1 gap-1 -mr-2">
|
||||
<IconCirclePlus size={16} />
|
||||
<span>{t('provider:addProvider')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('provider:addOpenAIProvider')}
|
||||
</DialogTitle>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-2"
|
||||
placeholder={t('provider:enterNameForProvider')}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent key from being captured by parent components
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button disabled={!name} onClick={createProvider}>
|
||||
{t('common:create')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{providers.map((provider, index) => (
|
||||
<CardItem
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<ProvidersAvatar provider={provider} />
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{getProviderTitle(provider.provider)}
|
||||
</h3>
|
||||
<p className="text-xs text-main-view-fg/70">
|
||||
{provider.models.length} Models
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
{provider.active && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 bg-transparent hover:bg-main-view-fg/10 border-none shadow-none"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: provider.provider,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconSettings
|
||||
className="text-main-view-fg/60"
|
||||
size={16}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<Switch
|
||||
checked={provider.active}
|
||||
onCheckedChange={(e) => {
|
||||
updateProvider(provider.provider, {
|
||||
...provider,
|
||||
active: e,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user