Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 81 additions & 43 deletions web-app/src/routes/settings/providers/$providerName.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,167 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Card, CardItem } from '@/containers/Card'
import HeaderPage from '@/containers/HeaderPage'
import SettingsMenu from '@/containers/SettingsMenu'
import { useModelProvider } from '@/hooks/useModelProvider'
import { cn, getProviderTitle } from '@/lib/utils'
import { open } from '@tauri-apps/plugin-dialog'
import {

Check warning on line 8 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

2-8 lines are not covered with tests
getActiveModels,
pullModel,
startModel,
stopAllModels,
stopModel,
} from '@/services/models'
import {

Check warning on line 15 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

15 line is not covered with tests
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 { DialogAddModel } from '@/containers/dialogs/AddModel'
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 { updateSettings, fetchModelsFromProvider } from '@/services/providers'
import { localStorageKey } from '@/constants/localStorage'
import { Button } from '@/components/ui/button'
import { IconFolderPlus, IconLoader, IconRefresh } from '@tabler/icons-react'
import { getProviders } from '@/services/providers'
import { toast } from 'sonner'
import { useEffect, useState } from 'react'
import { predefinedProviders } from '@/consts/providers'
import { useModelLoad } from '@/hooks/useModelLoad'
import { useLlamacppDevices } from '@/hooks/useLlamacppDevices'

Check warning on line 42 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

21-42 lines are not covered with tests

// as route.threadsDetail
export const Route = createFileRoute('/settings/providers/$providerName')({
component: ProviderDetail,
validateSearch: (search: Record<string, unknown>): { step?: string } => {

Check warning on line 47 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

45-47 lines are not covered with tests
// validate and parse the search params into a typed state
return {
step: String(search?.step),
}
},
})

Check warning on line 53 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

49-53 lines are not covered with tests

function ProviderDetail() {
const { t } = useTranslation()
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<string[]>([])
const [loadingModels, setLoadingModels] = useState<string[]>([])
const [refreshingModels, setRefreshingModels] = useState(false)
const [importingModel, setImportingModel] = useState(false)
const { providerName } = useParams({ from: Route.id })
const { getProviderByName, setProviders, updateProvider } = useModelProvider()
const provider = getProviderByName(providerName)
const isSetup = step === 'setup_remote_provider'

Check warning on line 86 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

55-86 lines are not covered with tests

// 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)
)

Check warning on line 97 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

89-97 lines are not covered with tests

const handleImportModel = async () => {
if (!provider) {
return
}

Check warning on line 102 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

99-102 lines are not covered with tests

setImportingModel(true)
const selectedFile = await open({
multiple: false,
directory: false,
filters: [
{
name: 'GGUF',
extensions: ['gguf'],
},
],
})

Check warning on line 114 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

104-114 lines are not covered with tests
// If the dialog returns a file path, extract just the file name
const fileName =
typeof selectedFile === 'string'
? selectedFile
.split(/[\\/]/)
.pop()
?.replace(/\s/g, '-')
: undefined

Check warning on line 122 in web-app/src/routes/settings/providers/$providerName.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

116-122 lines are not covered with tests

if (selectedFile && fileName) {
// Check if model already exists
const modelExists = provider.models.some(
(model) => model.name === fileName
)

if (modelExists) {
toast.error('Model already exists', {
description: `${fileName} already imported`,
})
setImportingModel(false)
return
}

try {
await pullModel(fileName, selectedFile)
// Refresh the provider to update the models list
await getProviders().then(setProviders)
toast.success(t('providers:import'), {
id: `import-model-${provider.provider}`,
description: t(
'providers:importModelSuccess',
{ provider: fileName }
),
})
} catch (error) {
console.error(
t('providers:importModelError'),
error
)
toast.error(t('providers:importModelError'), {
description: error instanceof Error ? error.message : 'Unknown error occurred',
})
} finally {
setImportingModel(false)
}
} else {
setImportingModel(false)
}
}

useEffect(() => {
// Initial data fetch
getActiveModels().then((models) => setActiveModels(models || []))
Expand Down Expand Up @@ -482,52 +549,23 @@
variant="link"
size="sm"
className="hover:no-underline"
onClick={async () => {
const selectedFile = await open({
multiple: false,
directory: false,
filters: [
{
name: 'GGUF',
extensions: ['gguf'],
},
],
})
// If the dialog returns a file path, extract just the file name
const fileName =
typeof selectedFile === 'string'
? selectedFile.split(/[\\/]/).pop()
: undefined

if (selectedFile && fileName) {
try {
await pullModel(fileName, selectedFile)
} catch (error) {
console.error(
t('providers:importModelError'),
error
)
} finally {
// Refresh the provider to update the models list
getProviders().then(setProviders)
toast.success(t('providers:import'), {
id: `import-model-${provider.provider}`,
description: t(
'providers:importModelSuccess',
{ provider: provider.provider }
),
})
}
}
}}
disabled={importingModel}
onClick={handleImportModel}
>
<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">
<IconFolderPlus
size={18}
className="text-main-view-fg/50"
/>
{importingModel ? (
<IconLoader
size={18}
className="text-main-view-fg/50 animate-spin"
/>
) : (
<IconFolderPlus
size={18}
className="text-main-view-fg/50"
/>
)}
<span className="text-main-view-fg/70">
{t('providers:import')}
{importingModel ? 'Importing...' : t('providers:import')}
</span>
</div>
</Button>
Expand Down
Loading