import { Card, CardItem } from '@/containers/Card' import HeaderPage from '@/containers/HeaderPage' import ProvidersMenu from '@/containers/ProvidersMenu' 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, importModel, startModel, stopAllModels, stopModel, } from '@/services/models' import { createFileRoute, Link, useNavigate, useParams, useSearch, } from '@tanstack/react-router' import { useTranslation } from '@/i18n/react-i18next-compat' import Capabilities from '@/containers/Capabilities' import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting' import { RenderMarkdown } from '@/containers/RenderMarkdown' import { DialogEditModel } from '@/containers/dialogs/EditModel' import { DialogAddModel } from '@/containers/dialogs/AddModel' import { ModelSetting } from '@/containers/ModelSetting' import { DialogDeleteModel } from '@/containers/dialogs/DeleteModel' import Joyride, { CallBackProps, STATUS } from 'react-joyride' import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { route } from '@/constants/routes' import DeleteProvider from '@/containers/dialogs/DeleteProvider' import { updateSettings, fetchModelsFromProvider } from '@/services/providers' import { Button } from '@/components/ui/button' import { IconFolderPlus, IconLoader, IconRefresh } from '@tabler/icons-react' import { getProviders } from '@/services/providers' import { toast } from 'sonner' import { ActiveModel } from '@/types/models' import { useEffect, useState } from 'react' import { predefinedProviders } from '@/mock/data' import { isProd } from '@/lib/version' // as route.threadsDetail export const Route = createFileRoute('/settings/providers/$providerName')({ component: ProviderDetail, validateSearch: (search: Record): { step?: string } => { // validate and parse the search params into a typed state return { step: String(search?.step), } }, }) function ProviderDetail() { const { t } = useTranslation() const steps = [ { target: '.first-step-setup-remote-provider', title: t('providers:joyride.chooseProviderTitle'), disableBeacon: true, content: t('providers:joyride.chooseProviderContent'), }, { target: '.second-step-setup-remote-provider', title: t('providers:joyride.getApiKeyTitle'), disableBeacon: true, content: t('providers:joyride.getApiKeyContent'), }, { target: '.third-step-setup-remote-provider', title: t('providers:joyride.insertApiKeyTitle'), disableBeacon: true, content: t('providers:joyride.insertApiKeyContent'), }, ] const { step } = useSearch({ from: Route.id }) const [activeModels, setActiveModels] = useState([]) const [loadingModels, setLoadingModels] = useState([]) const [refreshingModels, setRefreshingModels] = useState(false) const { providerName } = useParams({ from: Route.id }) const { getProviderByName, setProviders, updateProvider } = useModelProvider() const provider = getProviderByName(providerName) const isSetup = step === 'setup_remote_provider' const navigate = useNavigate() useEffect(() => { // Initial data fetch getActiveModels().then(setActiveModels) // Set up interval for real-time updates const intervalId = setInterval(() => { getActiveModels().then(setActiveModels) }, 5000) return () => clearInterval(intervalId) }, [setActiveModels]) const handleJoyrideCallback = (data: CallBackProps) => { const { status } = data if (status === STATUS.FINISHED) { navigate({ to: route.home, }) } } const handleRefreshModels = async () => { if (!provider || !provider.base_url) { toast.error(t('providers:models'), { description: t('providers:refreshModelsError'), }) return } setRefreshingModels(true) try { const modelIds = await fetchModelsFromProvider(provider) // Create new models from the fetched IDs const newModels: Model[] = modelIds.map((id) => ({ id, model: id, name: id, capabilities: ['completion'], // Default capability version: '1.0', })) // Filter out models that already exist const existingModelIds = provider.models.map((m) => m.id) const modelsToAdd = newModels.filter( (model) => !existingModelIds.includes(model.id) ) if (modelsToAdd.length > 0) { // Update the provider with new models const updatedModels = [...provider.models, ...modelsToAdd] updateProvider(providerName, { ...provider, models: updatedModels, }) toast.success(t('providers:models'), { description: t('providers:refreshModelsSuccess', { count: modelsToAdd.length, provider: provider.provider, }), }) } else { toast.success(t('providers:models'), { description: t('providers:noNewModels'), }) } } catch (error) { console.error( t('providers:refreshModelsFailed', { provider: provider.provider }), error ) toast.error(t('providers:models'), { description: t('providers:refreshModelsFailed', { provider: provider.provider, }), }) } finally { setRefreshingModels(false) } } const handleStartModel = (modelId: string) => { // Add model to loading state setLoadingModels((prev) => [...prev, modelId]) if (provider) startModel(provider, modelId) .then(() => { setActiveModels((prevModels) => [ ...prevModels, { id: modelId } as ActiveModel, ]) }) .catch((error) => { console.error('Error starting model:', error) }) .finally(() => { // Remove model from loading state setLoadingModels((prev) => prev.filter((id) => id !== modelId)) }) } const handleStopModel = (modelId: string) => { stopModel(modelId) .then(() => { setActiveModels((prevModels) => prevModels.filter((model) => model.id !== modelId) ) }) .catch((error) => { console.error('Error stopping model:', error) }) } return ( <>

{t('common:settings')}

{getProviderTitle(providerName)}

{ if (provider) { updateProvider(providerName, { ...provider, active: e }) } }} />
{/* Settings */} {provider?.settings.map((setting, settingIndex) => { // Use the DynamicController component const actionComponent = (
{ if (provider) { const newSettings = [...provider.settings] // Handle different value types by forcing the type // Use type assertion to bypass type checking ;( newSettings[settingIndex].controller_props as { value: string | boolean | number } ).value = newValue // Create update object with updated settings const updateObj: Partial = { settings: newSettings, } // Check if this is an API key or base URL setting and update the corresponding top-level field const settingKey = setting.key if ( settingKey === 'api-key' && typeof newValue === 'string' ) { updateObj.api_key = newValue } else if ( settingKey === 'base-url' && typeof newValue === 'string' ) { updateObj.base_url = newValue } updateSettings( providerName, updateObj.settings ?? [] ) updateProvider(providerName, { ...provider, ...updateObj, }) stopAllModels() } }} />
) return ( { return ( ) }, p: ({ ...props }) => (

), }} /> } actions={actionComponent} /> ) })} {/* Models */}

{t('providers:models')}

{provider && provider.provider !== 'llama.cpp' && ( <> {!predefinedProviders.some( (p) => p.provider === provider.provider ) && ( )} )} {provider && provider.provider === 'llama.cpp' && ( )}
} > {provider?.models.length ? ( provider?.models.map((model, modelIndex) => { const capabilities = model.capabilities || [] return (

{model.id}

{!isProd && ( )}
} actions={
{!isProd && ( )} {model.settings && ( )} {provider && provider.provider === 'llama.cpp' && (
{activeModels.some( (activeModel) => activeModel.id === model.id ) ? ( ) : ( )}
)}
} /> ) }) ) : (
{t('providers:noModelFound')}

{t('providers:noModelFoundDesc')}   {t('common:hub')}

)}
) }