✨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',
|
assistant: '/assistant',
|
||||||
settings: {
|
settings: {
|
||||||
index: '/settings',
|
index: '/settings',
|
||||||
|
model_providers: '/settings/providers',
|
||||||
providers: '/settings/providers/$providerName',
|
providers: '/settings/providers/$providerName',
|
||||||
general: '/settings/general',
|
general: '/settings/general',
|
||||||
appearance: '/settings/appearance',
|
appearance: '/settings/appearance',
|
||||||
|
|||||||
@ -10,29 +10,35 @@ type CardProps = {
|
|||||||
type CardItemProps = {
|
type CardItemProps = {
|
||||||
title?: string | ReactNode
|
title?: string | ReactNode
|
||||||
description?: string | ReactNode
|
description?: string | ReactNode
|
||||||
|
descriptionOutside?: string | ReactNode
|
||||||
align?: 'start' | 'center' | 'end'
|
align?: 'start' | 'center' | 'end'
|
||||||
actions?: ReactNode
|
actions?: ReactNode
|
||||||
column?: boolean
|
column?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
classNameWrapperAction?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardItem({
|
export function CardItem({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
descriptionOutside,
|
||||||
className,
|
className,
|
||||||
|
classNameWrapperAction,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
column,
|
column,
|
||||||
actions,
|
actions,
|
||||||
}: CardItemProps) {
|
}: CardItemProps) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
'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,
|
descriptionOutside && 'border-0',
|
||||||
align === 'start' && 'items-start',
|
align === 'start' && 'items-start',
|
||||||
align === 'center' && 'items-center',
|
align === 'center' && 'items-center',
|
||||||
align === 'end' && 'items-end',
|
align === 'end' && 'items-end',
|
||||||
column && 'flex-col gap-y-0 items-start'
|
column && 'flex-col gap-y-0 items-start',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@ -44,9 +50,23 @@ export function CardItem({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{actions && (
|
{actions && (
|
||||||
<div className={cn('shrink-0', column && 'w-full')}>{actions}</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'shrink-0',
|
||||||
|
classNameWrapperAction,
|
||||||
|
column && 'w-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{descriptionOutside && (
|
||||||
|
<span className="text-main-view-fg/70 leading-normal">
|
||||||
|
{descriptionOutside}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export function ChatWidthSwitcher() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col sm:flex-row sm:gap-4">
|
||||||
<button
|
<button
|
||||||
className={cn(
|
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 ',
|
||||||
@ -18,18 +18,22 @@ export function ChatWidthSwitcher() {
|
|||||||
onClick={() => setChatWidth('compact')}
|
onClick={() => setChatWidth('compact')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
<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' && (
|
{chatWidth === 'compact' && (
|
||||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
<IconCircleCheckFilled className="size-4 text-accent" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto p-2">
|
<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" />
|
<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">
|
<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">{t('common:placeholder.chatInput')}</span>
|
<span className="text-main-view-fg/50 line-clamp-1">
|
||||||
|
{t('common:placeholder.chatInput')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +46,9 @@ export function ChatWidthSwitcher() {
|
|||||||
onClick={() => setChatWidth('full')}
|
onClick={() => setChatWidth('full')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
|
<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' && (
|
{chatWidth === 'full' && (
|
||||||
<IconCircleCheckFilled className="size-4 text-accent" />
|
<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" />
|
<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">
|
<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>
|
<span className="text-main-view-fg/50">
|
||||||
|
{t('common:placeholder.chatInput')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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,21 +1,57 @@
|
|||||||
import { Link, useMatches } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
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 { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
import { getProviderTitle } from '@/lib/utils'
|
||||||
|
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||||
|
|
||||||
const SettingsMenu = () => {
|
const SettingsMenu = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers } = useModelProvider()
|
const [expandedProviders, setExpandedProviders] = useState(false)
|
||||||
const { experimentalFeatures } = useGeneralSetting()
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const firstItemProvider =
|
|
||||||
providers.length > 0 ? providers[0].provider : 'llama.cpp'
|
|
||||||
const matches = useMatches()
|
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) =>
|
||||||
match.routeId === '/settings/providers/$providerName' &&
|
match.routeId === '/settings/providers/$providerName' &&
|
||||||
'providerName' in match.params
|
'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.search &&
|
||||||
|
typeof match.search === 'object' &&
|
||||||
|
'step' in match.search &&
|
||||||
|
match.search.step === 'setup_remote_provider'
|
||||||
|
)
|
||||||
|
|
||||||
const menuSettings = [
|
const menuSettings = [
|
||||||
{
|
{
|
||||||
@ -30,6 +66,11 @@ const SettingsMenu = () => {
|
|||||||
title: 'common:privacy',
|
title: 'common:privacy',
|
||||||
route: route.settings.privacy,
|
route: route.settings.privacy,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'common:modelProviders',
|
||||||
|
route: route.settings.model_providers,
|
||||||
|
hasSubMenu: activeProviders.length > 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'common:keyboardShortcuts',
|
title: 'common:keyboardShortcuts',
|
||||||
route: route.settings.shortcuts,
|
route: route.settings.shortcuts,
|
||||||
@ -61,52 +102,113 @@ const SettingsMenu = () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const toggleProvidersExpansion = () => {
|
||||||
|
setExpandedProviders(!expandedProviders)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
setIsMenuOpen(!isMenuOpen)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5">
|
<>
|
||||||
|
<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">
|
<div className="flex flex-col gap-1 w-full text-main-view-fg/90 font-medium">
|
||||||
{menuSettings.map((menu, index) => {
|
{menuSettings.map((menu) => (
|
||||||
// Render the menu item
|
<div key={menu.title}>
|
||||||
const menuItem = (
|
|
||||||
<Link
|
<Link
|
||||||
key={menu.title}
|
|
||||||
to={menu.route}
|
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"
|
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>
|
<span className="text-main-view-fg/80">{t(menu.title)}</span>
|
||||||
</Link>
|
{menu.hasSubMenu && (
|
||||||
)
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
if (index === 2) {
|
e.preventDefault()
|
||||||
return (
|
e.stopPropagation()
|
||||||
<div key={menu.title}>
|
toggleProvidersExpansion()
|
||||||
<span className="mb-1 block">{menuItem}</span>
|
}}
|
||||||
|
className="text-main-view-fg/60 hover:text-main-view-fg/80"
|
||||||
{/* 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">
|
{expandedProviders ? (
|
||||||
{t('common:modelProviders')}
|
<IconChevronDown size={16} />
|
||||||
</span>
|
) : (
|
||||||
</Link>
|
<IconChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
</Link>
|
||||||
}
|
|
||||||
|
|
||||||
// For other menu items, just render them normally
|
{/* Sub-menu for model providers */}
|
||||||
return menuItem
|
{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
|
||||||
|
)
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { Route as SettingsGeneralImport } from './routes/settings/general'
|
|||||||
import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
|
import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
|
||||||
import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
|
import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
|
||||||
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
|
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'
|
import { Route as SettingsProvidersProviderNameImport } from './routes/settings/providers/$providerName'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
@ -127,6 +128,12 @@ const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SettingsProvidersIndexRoute = SettingsProvidersIndexImport.update({
|
||||||
|
id: '/settings/providers/',
|
||||||
|
path: '/settings/providers/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const SettingsProvidersProviderNameRoute =
|
const SettingsProvidersProviderNameRoute =
|
||||||
SettingsProvidersProviderNameImport.update({
|
SettingsProvidersProviderNameImport.update({
|
||||||
id: '/settings/providers/$providerName',
|
id: '/settings/providers/$providerName',
|
||||||
@ -257,6 +264,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SettingsProvidersProviderNameImport
|
preLoaderRoute: typeof SettingsProvidersProviderNameImport
|
||||||
parentRoute: typeof rootRoute
|
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
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@ -300,6 +315,7 @@ export interface FileRoutesByTo {
|
|||||||
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@ -321,6 +337,7 @@ export interface FileRoutesById {
|
|||||||
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
'/settings/shortcuts': typeof SettingsShortcutsRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
'/threads/$threadId': typeof ThreadsThreadIdRoute
|
||||||
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
'/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute
|
||||||
|
'/settings/providers/': typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@ -343,6 +360,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@ -362,6 +380,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@ -381,6 +400,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings/shortcuts'
|
| '/settings/shortcuts'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/settings/providers/$providerName'
|
| '/settings/providers/$providerName'
|
||||||
|
| '/settings/providers/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,6 +422,7 @@ export interface RootRouteChildren {
|
|||||||
SettingsShortcutsRoute: typeof SettingsShortcutsRoute
|
SettingsShortcutsRoute: typeof SettingsShortcutsRoute
|
||||||
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
|
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
|
||||||
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
|
SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute
|
||||||
|
SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
@ -422,6 +443,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
SettingsShortcutsRoute: SettingsShortcutsRoute,
|
SettingsShortcutsRoute: SettingsShortcutsRoute,
|
||||||
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
|
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
|
||||||
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
|
SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute,
|
||||||
|
SettingsProvidersIndexRoute: SettingsProvidersIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@ -450,7 +472,8 @@ export const routeTree = rootRoute
|
|||||||
"/settings/privacy",
|
"/settings/privacy",
|
||||||
"/settings/shortcuts",
|
"/settings/shortcuts",
|
||||||
"/threads/$threadId",
|
"/threads/$threadId",
|
||||||
"/settings/providers/$providerName"
|
"/settings/providers/$providerName",
|
||||||
|
"/settings/providers/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@ -503,6 +526,9 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/settings/providers/$providerName": {
|
"/settings/providers/$providerName": {
|
||||||
"filePath": "settings/providers/$providerName.tsx"
|
"filePath": "settings/providers/$providerName.tsx"
|
||||||
|
},
|
||||||
|
"/settings/providers/": {
|
||||||
|
"filePath": "settings/providers/index.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ function Appareances() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<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 flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
@ -55,26 +55,31 @@ function Appareances() {
|
|||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.windowBackground')}
|
title={t('settings:appearance.windowBackground')}
|
||||||
description={t('settings:appearance.windowBackgroundDesc')}
|
description={t('settings:appearance.windowBackgroundDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppBgColor />}
|
actions={<ColorPickerAppBgColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.appMainView')}
|
title={t('settings:appearance.appMainView')}
|
||||||
description={t('settings:appearance.appMainViewDesc')}
|
description={t('settings:appearance.appMainViewDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppMainView />}
|
actions={<ColorPickerAppMainView />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.primary')}
|
title={t('settings:appearance.primary')}
|
||||||
description={t('settings:appearance.primaryDesc')}
|
description={t('settings:appearance.primaryDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppPrimaryColor />}
|
actions={<ColorPickerAppPrimaryColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.accent')}
|
title={t('settings:appearance.accent')}
|
||||||
description={t('settings:appearance.accentDesc')}
|
description={t('settings:appearance.accentDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppAccentColor />}
|
actions={<ColorPickerAppAccentColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:appearance.destructive')}
|
title={t('settings:appearance.destructive')}
|
||||||
description={t('settings:appearance.destructiveDesc')}
|
description={t('settings:appearance.destructiveDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={<ColorPickerAppDestructiveColor />}
|
actions={<ColorPickerAppDestructiveColor />}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
|
|||||||
@ -205,7 +205,7 @@ function General() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full flex-col sm:flex-row">
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<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 flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
@ -222,6 +222,7 @@ function General() {
|
|||||||
<CardItem
|
<CardItem
|
||||||
title={t('settings:general.checkForUpdates')}
|
title={t('settings:general.checkForUpdates')}
|
||||||
description={t('settings:general.checkForUpdatesDesc')}
|
description={t('settings:general.checkForUpdatesDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
@ -265,6 +266,7 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
align="start"
|
align="start"
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
@ -274,12 +276,14 @@ function General() {
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2 mt-1 ">
|
<div className="flex items-center gap-2 mt-1 ">
|
||||||
|
<div className="">
|
||||||
<span
|
<span
|
||||||
title={janDataFolder}
|
title={janDataFolder}
|
||||||
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
|
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}
|
{janDataFolder}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
janDataFolder && copyToClipboard(janDataFolder)
|
janDataFolder && copyToClipboard(janDataFolder)
|
||||||
@ -349,6 +353,7 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
description={t('settings:dataFolder.appLogsDesc')}
|
description={t('settings:dataFolder.appLogsDesc')}
|
||||||
|
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -229,9 +229,11 @@ function LocalAPIServer() {
|
|||||||
title={t('settings:localApiServer.apiKey')}
|
title={t('settings:localApiServer.apiKey')}
|
||||||
description={t('settings:localApiServer.apiKeyDesc')}
|
description={t('settings:localApiServer.apiKeyDesc')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||||
isServerRunning && 'opacity-50 pointer-events-none',
|
isServerRunning && 'opacity-50 pointer-events-none',
|
||||||
isApiKeyEmpty && showApiKeyError && 'pb-6'
|
isApiKeyEmpty && showApiKeyError && 'pb-6'
|
||||||
)}
|
)}
|
||||||
|
classNameWrapperAction="w-full sm:w-auto"
|
||||||
actions={
|
actions={
|
||||||
<ApiKeyInput
|
<ApiKeyInput
|
||||||
showError={showApiKeyError}
|
showError={showApiKeyError}
|
||||||
@ -243,8 +245,10 @@ function LocalAPIServer() {
|
|||||||
title={t('settings:localApiServer.trustedHosts')}
|
title={t('settings:localApiServer.trustedHosts')}
|
||||||
description={t('settings:localApiServer.trustedHostsDesc')}
|
description={t('settings:localApiServer.trustedHostsDesc')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2',
|
||||||
isServerRunning && 'opacity-50 pointer-events-none'
|
isServerRunning && 'opacity-50 pointer-events-none'
|
||||||
)}
|
)}
|
||||||
|
classNameWrapperAction="w-full sm:w-auto"
|
||||||
actions={<TrustedHostsInput />}
|
actions={<TrustedHostsInput />}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -320,7 +320,7 @@ function MCPServers() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
description={
|
descriptionOutside={
|
||||||
<div className="text-sm text-main-view-fg/70">
|
<div className="text-sm text-main-view-fg/70">
|
||||||
<div>
|
<div>
|
||||||
{t('mcp-servers:command')}: {config.command}
|
{t('mcp-servers:command')}: {config.command}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { Card, CardItem } from '@/containers/Card'
|
import { Card, CardItem } from '@/containers/Card'
|
||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import ProvidersMenu from '@/containers/ProvidersMenu'
|
import SettingsMenu from '@/containers/SettingsMenu'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import { cn, getProviderTitle } from '@/lib/utils'
|
import { cn, getProviderTitle } from '@/lib/utils'
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import {
|
import {
|
||||||
getActiveModels,
|
getActiveModels,
|
||||||
@ -228,23 +227,13 @@ function ProviderDetail() {
|
|||||||
<h1 className="font-medium">{t('common:settings')}</h1>
|
<h1 className="font-medium">{t('common:settings')}</h1>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full">
|
||||||
<div className="flex">
|
<SettingsMenu />
|
||||||
<ProvidersMenu stepSetupRemoteProvider={isSetup} />
|
|
||||||
</div>
|
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
|
<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 flex-col justify-between gap-4 gap-y-3 w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="font-medium text-base">
|
<h1 className="font-medium text-base">
|
||||||
{getProviderTitle(providerName)}
|
{getProviderTitle(providerName)}
|
||||||
</h1>
|
</h1>
|
||||||
<Switch
|
|
||||||
checked={provider?.active}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
if (provider) {
|
|
||||||
updateProvider(providerName, { ...provider, active: e })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -461,7 +450,12 @@ function ProviderDetail() {
|
|||||||
key={modelIndex}
|
key={modelIndex}
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<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} />
|
<Capabilities capabilities={capabilities} />
|
||||||
</div>
|
</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