/* eslint-disable @typescript-eslint/no-explicit-any */ import { Card, CardItem } from '@/containers/Card' import HeaderPage from '@/containers/HeaderPage' import SettingsMenu from '@/containers/SettingsMenu' import { useModelProvider } from '@/hooks/useModelProvider' import { cn, getProviderTitle, getModelDisplayName } from '@/lib/utils' import { createFileRoute, Link, 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 { ImportVisionModelDialog } from '@/containers/dialogs/ImportVisionModelDialog' import { ModelSetting } from '@/containers/ModelSetting' import { DialogDeleteModel } from '@/containers/dialogs/DeleteModel' import { FavoriteModelAction } from '@/containers/FavoriteModelAction' import Joyride, { CallBackProps, STATUS } from 'react-joyride' import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { route } from '@/constants/routes' import DeleteProvider from '@/containers/dialogs/DeleteProvider' import { useServiceHub } from '@/hooks/useServiceHub' import { localStorageKey } from '@/constants/localStorage' import { Button } from '@/components/ui/button' import { IconFolderPlus, IconLoader, IconRefresh, IconUpload, } from '@tabler/icons-react' import { toast } from 'sonner' import { useCallback, useEffect, useState } from 'react' import { predefinedProviders } from '@/consts/providers' import { useModelLoad } from '@/hooks/useModelLoad' import { useLlamacppDevices } from '@/hooks/useLlamacppDevices' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' import { useBackendUpdater } from '@/hooks/useBackendUpdater' import { basenameNoExt } from '@/lib/utils' // 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 serviceHub = useServiceHub() const { setModelLoadError } = useModelLoad() 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 [isCheckingBackendUpdate, setIsCheckingBackendUpdate] = useState(false) const [isInstallingBackend, setIsInstallingBackend] = useState(false) const [importingModel, setImportingModel] = useState(null) const { checkForUpdate: checkForBackendUpdate, installBackend } = useBackendUpdater() const { providerName } = useParams({ from: Route.id }) const { getProviderByName, setProviders, updateProvider } = useModelProvider() const provider = getProviderByName(providerName) const isSetup = step === 'setup_remote_provider' // Check if llamacpp provider needs backend configuration const needsBackendConfig = provider?.provider === 'llamacpp' && provider.settings?.some( (setting) => setting.key === 'version_backend' && (setting.controller_props.value === 'none' || setting.controller_props.value === '' || !setting.controller_props.value) ) const handleModelImportSuccess = async (importedModelName?: string) => { if (importedModelName) { setImportingModel(importedModelName) } try { // Refresh the provider to update the models list await serviceHub.providers().getProviders().then(setProviders) // If a model was imported and it might have vision capabilities, check and update if (importedModelName && providerName === 'llamacpp') { try { const mmprojExists = await serviceHub .models() .checkMmprojExists(importedModelName) if (mmprojExists) { // Get the updated provider after refresh const { getProviderByName, updateProvider: updateProviderState } = useModelProvider.getState() const llamacppProvider = getProviderByName('llamacpp') if (llamacppProvider) { const modelIndex = llamacppProvider.models.findIndex( (m: Model) => m.id === importedModelName ) if (modelIndex !== -1) { const model = llamacppProvider.models[modelIndex] const capabilities = model.capabilities || [] // Add 'vision' capability if not already present AND if user hasn't manually configured capabilities // Check if model has a custom capabilities config flag const hasUserConfiguredCapabilities = (model as any)._userConfiguredCapabilities === true if ( !capabilities.includes('vision') && !hasUserConfiguredCapabilities ) { const updatedModels = [...llamacppProvider.models] updatedModels[modelIndex] = { ...model, capabilities: [...capabilities, 'vision'], // Mark this as auto-detected, not user-configured _autoDetectedVision: true, } as any updateProviderState('llamacpp', { models: updatedModels }) console.log( `Vision capability added to model after provider refresh: ${importedModelName}` ) } } } } } catch (error) { console.error('Error checking mmproj existence after import:', error) } } } finally { // The importing state will be cleared by the useEffect when model appears in list } } useEffect(() => { // Initial data fetch serviceHub .models() .getActiveModels() .then((models) => setActiveModels(models || [])) // Set up interval for real-time updates const intervalId = setInterval(() => { serviceHub .models() .getActiveModels() .then((models) => setActiveModels(models || [])) }, 5000) return () => clearInterval(intervalId) }, [serviceHub, setActiveModels]) // Clear importing state when model appears in the provider's model list useEffect(() => { if (importingModel && provider?.models) { const modelExists = provider.models.some( (model) => model.id === importingModel ) if (modelExists) { setImportingModel(null) } } }, [importingModel, provider?.models]) // Fallback: Clear importing state after 10 seconds to prevent infinite loading useEffect(() => { if (importingModel) { const timeoutId = setTimeout(() => { setImportingModel(null) }, 10000) // 10 seconds fallback return () => clearTimeout(timeoutId) } }, [importingModel]) // Auto-refresh provider settings to get updated backend configuration const refreshSettings = useCallback(async () => { if (!provider) return try { // Refresh providers to get updated settings from the extension const updatedProviders = await serviceHub.providers().getProviders() setProviders(updatedProviders) } catch (error) { console.error('Failed to refresh settings:', error) } }, [provider, serviceHub, setProviders]) // Auto-refresh settings when provider changes or when llamacpp needs backend config useEffect(() => { if (provider && needsBackendConfig) { // Auto-refresh every 3 seconds when backend is being configured const intervalId = setInterval(refreshSettings, 3000) return () => clearInterval(intervalId) } }, [provider, needsBackendConfig, refreshSettings]) // Note: settingsChanged event is now handled globally in GlobalEventHandler // This ensures all screens receive the event intermediately const handleJoyrideCallback = (data: CallBackProps) => { const { status } = data if (status === STATUS.FINISHED) { localStorage.setItem(localStorageKey.setupCompleted, 'true') } } const handleRefreshModels = async () => { if (!provider || !provider.base_url) { toast.error(t('providers:models'), { description: t('providers:refreshModelsError'), }) return } setRefreshingModels(true) try { const modelIds = await serviceHub .providers() .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 = async (modelId: string) => { // Add model to loading state setLoadingModels((prev) => [...prev, modelId]) if (provider) { try { // Start the model with plan result await serviceHub.models().startModel(provider, modelId) // Refresh active models after starting serviceHub .models() .getActiveModels() .then((models) => setActiveModels(models || [])) } catch (error) { setModelLoadError(error as ErrorObject) } finally { // Remove model from loading state setLoadingModels((prev) => prev.filter((id) => id !== modelId)) } } } const handleStopModel = (modelId: string) => { // Original: stopModel(modelId).then(() => { setActiveModels((prevModels) => prevModels.filter((model) => model !== modelId)) }) serviceHub .models() .stopModel(modelId) .then(() => { // Refresh active models after stopping serviceHub .models() .getActiveModels() .then((models) => setActiveModels(models || [])) }) .catch((error) => { console.error('Error stopping model:', error) }) } const handleCheckForBackendUpdate = useCallback(async () => { if (provider?.provider !== 'llamacpp') return setIsCheckingBackendUpdate(true) try { const update = await checkForBackendUpdate(true) if (!update) { toast.info(t('settings:noBackendUpdateAvailable')) } // If update is available, the BackendUpdater dialog will automatically show } catch (error) { console.error('Failed to check for backend updates:', error) toast.error(t('settings:backendUpdateError')) } finally { setIsCheckingBackendUpdate(false) } }, [provider, checkForBackendUpdate, t]) const handleInstallBackendFromFile = useCallback(async () => { if (provider?.provider !== 'llamacpp') return setIsInstallingBackend(true) try { // Open file dialog with filter for .tar.gz and .zip files const selectedFile = await serviceHub.dialog().open({ multiple: false, directory: false, filters: [ { name: 'Backend Archives', extensions: ['tar.gz', 'zip', 'gz'], }, ], }) if (selectedFile && typeof selectedFile === 'string') { // Process the file path: replace spaces with dashes and convert to lowercase // Install the backend using the llamacpp extension await installBackend(selectedFile) // Extract filename from the selected file path and replace spaces with dashes const fileName = basenameNoExt(selectedFile).replace(/\s+/g, "-") toast.success(t('settings:backendInstallSuccess'), { description: `Llamacpp ${fileName} installed`, }) // Refresh settings to update backend configuration await refreshSettings() } } catch (error) { console.error('Failed to install backend from file:', error) toast.error(t('settings:backendInstallError'), { description: error instanceof Error ? error.message : 'Unknown error occurred', }) } finally { setIsInstallingBackend(false) } }, [provider, serviceHub, refreshSettings, t, installBackend]) // Check if model provider settings are enabled for this platform if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) { return (

{t('common:settings')}

{t('common:notAvailable')}

Provider settings are not available on the web platform.

) } return ( <>

{t('common:settings')}

{getProviderTitle(providerName)}

{/* Settings */} {provider?.settings.map((setting, settingIndex) => { // Use the DynamicController component const actionComponent = (
{needsBackendConfig && setting.key === 'version_backend' ? (
loading
) : ( { 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 } // Reset device setting to empty when backend version changes if (settingKey === 'version_backend') { const deviceSettingIndex = newSettings.findIndex( (s) => s.key === 'device' ) if (deviceSettingIndex !== -1) { ( newSettings[deviceSettingIndex] .controller_props as { value: string } ).value = '' } // Reset llamacpp device activations when backend version changes if (providerName === 'llamacpp') { // Refresh devices to update activation status from provider settings const { fetchDevices } = useLlamacppDevices.getState() fetchDevices() } } serviceHub .providers() .updateSettings( providerName, updateObj.settings ?? [] ) updateProvider(providerName, { ...provider, ...updateObj, }) serviceHub.models().stopAllModels() } }} /> )}
) return ( { return ( ) }, p: ({ ...props }) => (

), }} /> {setting.key === 'version_backend' && setting.controller_props?.recommended && (

{setting.controller_props.recommended ?.split('/') .pop() || setting.controller_props.recommended} is the recommended backend.
)} {setting.key === 'version_backend' && provider?.provider === 'llamacpp' && (
)} } actions={actionComponent} /> ) })}
{/* Models */}

{t('providers:models')}

{provider && provider.provider !== 'llamacpp' && ( <> {!predefinedProviders.some( (p) => p.provider === provider.provider ) && ( )} )} {provider && provider.provider === 'llamacpp' && (
{t('providers:import')}
} /> )}
} > {provider?.models.length ? ( provider?.models.map((model, modelIndex) => { const capabilities = model.capabilities || [] return (

{getModelDisplayName(model)}

} actions={
{model.settings && ( )} {((provider && !predefinedProviders.some( (p) => p.provider === provider.provider )) || (provider && predefinedProviders.some( (p) => p.provider === provider.provider ) && Boolean(provider.api_key?.length))) && ( )} {provider && provider.provider === 'llamacpp' && (
{activeModels.some( (activeModel) => activeModel === model.id ) ? ( ) : ( )}
)}
} /> ) }) ) : (
{t('providers:noModelFound')}

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

)} {/* Show importing skeleton first if there's one */} {importingModel && (
Importing...

{importingModel}

} /> )}
) }