feat: allow user import model include mmproj file
This commit is contained in:
parent
88fb1acc0a
commit
1b035fd2f1
@ -105,7 +105,9 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
||||
try {
|
||||
// Only check mmproj for llamacpp provider
|
||||
if (selectedProvider === 'llamacpp') {
|
||||
const hasLocalMmproj = await serviceHub.models().checkMmprojExists(selectedModel.id)
|
||||
const hasLocalMmproj = await serviceHub
|
||||
.models()
|
||||
.checkMmprojExists(selectedModel.id)
|
||||
setHasMmproj(hasLocalMmproj)
|
||||
}
|
||||
// For non-llamacpp providers, only check vision capability
|
||||
|
||||
@ -139,7 +139,7 @@ const DropdownModelProvider = ({
|
||||
[getProviderByName, updateProvider, serviceHub]
|
||||
)
|
||||
|
||||
// Initialize model provider only once
|
||||
// Initialize model provider - avoid race conditions with manual selections
|
||||
useEffect(() => {
|
||||
const initializeModel = async () => {
|
||||
// Auto select model when existing thread is passed
|
||||
@ -150,11 +150,13 @@ const DropdownModelProvider = ({
|
||||
}
|
||||
// Check mmproj existence for llamacpp models
|
||||
if (model?.provider === 'llamacpp') {
|
||||
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
model.id as string,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
await serviceHub
|
||||
.models()
|
||||
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
model.id as string,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
// Also check vision capability
|
||||
await checkAndUpdateModelVisionCapability(model.id as string)
|
||||
}
|
||||
@ -164,11 +166,13 @@ const DropdownModelProvider = ({
|
||||
if (lastUsed && checkModelExists(lastUsed.provider, lastUsed.model)) {
|
||||
selectModelProvider(lastUsed.provider, lastUsed.model)
|
||||
if (lastUsed.provider === 'llamacpp') {
|
||||
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
lastUsed.model,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
await serviceHub
|
||||
.models()
|
||||
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
lastUsed.model,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
// Also check vision capability
|
||||
await checkAndUpdateModelVisionCapability(lastUsed.model)
|
||||
}
|
||||
@ -186,19 +190,28 @@ const DropdownModelProvider = ({
|
||||
}
|
||||
selectModelProvider('', '')
|
||||
}
|
||||
} else if (PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] && !selectedModel) {
|
||||
// For web-only builds, always auto-select the first model from jan provider if none is selected
|
||||
const janProvider = providers.find(
|
||||
(p) => p.provider === 'jan' && p.active && p.models.length > 0
|
||||
)
|
||||
if (janProvider && janProvider.models.length > 0) {
|
||||
const firstModel = janProvider.models[0]
|
||||
selectModelProvider(janProvider.provider, firstModel.id)
|
||||
} else {
|
||||
// Get current state for web auto-selection check
|
||||
const currentState = { selectedModel, selectedProvider }
|
||||
if (
|
||||
PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] &&
|
||||
!currentState.selectedModel &&
|
||||
!currentState.selectedProvider
|
||||
) {
|
||||
// For web-only builds, auto-select the first model from jan provider only if nothing is selected
|
||||
const janProvider = providers.find(
|
||||
(p) => p.provider === 'jan' && p.active && p.models.length > 0
|
||||
)
|
||||
if (janProvider && janProvider.models.length > 0) {
|
||||
const firstModel = janProvider.models[0]
|
||||
selectModelProvider(janProvider.provider, firstModel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeModel()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
model,
|
||||
selectModelProvider,
|
||||
@ -210,7 +223,7 @@ const DropdownModelProvider = ({
|
||||
getProviderByName,
|
||||
checkAndUpdateModelVisionCapability,
|
||||
serviceHub,
|
||||
selectedModel,
|
||||
// selectedModel and selectedProvider intentionally excluded to prevent race conditions
|
||||
])
|
||||
|
||||
// Update display model when selection changes
|
||||
@ -376,11 +389,13 @@ const DropdownModelProvider = ({
|
||||
|
||||
// Check mmproj existence for llamacpp models
|
||||
if (searchableModel.provider.provider === 'llamacpp') {
|
||||
await serviceHub.models().checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
searchableModel.model.id,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
await serviceHub
|
||||
.models()
|
||||
.checkMmprojExistsAndUpdateOffloadMMprojSetting(
|
||||
searchableModel.model.id,
|
||||
updateProvider,
|
||||
getProviderByName
|
||||
)
|
||||
// Also check vision capability
|
||||
await checkAndUpdateModelVisionCapability(searchableModel.model.id)
|
||||
}
|
||||
@ -572,7 +587,9 @@ const DropdownModelProvider = ({
|
||||
{getProviderTitle(providerInfo.provider)}
|
||||
</span>
|
||||
</div>
|
||||
{PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS] && (
|
||||
{PlatformFeatures[
|
||||
PlatformFeature.MODEL_PROVIDER_SETTINGS
|
||||
] && (
|
||||
<div
|
||||
className="size-6 cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||
onClick={(e) => {
|
||||
|
||||
327
web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
Normal file
327
web-app/src/containers/dialogs/ImportVisionModelDialog.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useServiceHub } from '@/hooks/useServiceHub'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
IconFileImport,
|
||||
IconLoader2,
|
||||
IconEye,
|
||||
IconCheck,
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
type ImportVisionModelDialogProps = {
|
||||
provider: ModelProvider
|
||||
trigger?: React.ReactNode
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const ImportVisionModelDialog = ({
|
||||
provider,
|
||||
trigger,
|
||||
onSuccess,
|
||||
}: ImportVisionModelDialogProps) => {
|
||||
const serviceHub = useServiceHub()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [isVisionModel, setIsVisionModel] = useState(false)
|
||||
const [modelFile, setModelFile] = useState<string | null>(null)
|
||||
const [mmProjFile, setMmProjFile] = useState<string | null>(null)
|
||||
const [modelName, setModelName] = useState('')
|
||||
|
||||
const handleFileSelect = async (type: 'model' | 'mmproj') => {
|
||||
const selectedFile = await serviceHub.dialog().open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [
|
||||
{
|
||||
name: type === 'model' ? 'GGUF Files' : 'MMPROJ Files',
|
||||
extensions: type === 'model' ? ['gguf'] : ['gguf'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (selectedFile && typeof selectedFile === 'string') {
|
||||
const fileName = selectedFile.split(/[\\/]/).pop() || ''
|
||||
|
||||
if (type === 'model') {
|
||||
setModelFile(selectedFile)
|
||||
// Auto-generate model name from GGUF file
|
||||
const sanitizedName = fileName
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/\.(gguf|GGUF)$/, '')
|
||||
.replace(/[^a-zA-Z0-9/_.-]/g, '') // Remove any characters not allowed in model IDs
|
||||
setModelName(sanitizedName)
|
||||
} else {
|
||||
setMmProjFile(selectedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!modelFile) {
|
||||
toast.error('Please select a model file')
|
||||
return
|
||||
}
|
||||
|
||||
if (isVisionModel && !mmProjFile) {
|
||||
toast.error('Please select both model and MMPROJ files for vision models')
|
||||
return
|
||||
}
|
||||
|
||||
if (!modelName) {
|
||||
toast.error('Unable to determine model name from file')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if model already exists
|
||||
const modelExists = provider.models.some(
|
||||
(model) => model.name === modelName
|
||||
)
|
||||
|
||||
if (modelExists) {
|
||||
toast.error('Model already exists', {
|
||||
description: `${modelName} already imported`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setImporting(true)
|
||||
|
||||
try {
|
||||
if (isVisionModel && mmProjFile) {
|
||||
// Import vision model with both files - let backend calculate SHA256 and sizes
|
||||
await serviceHub.models().pullModel(
|
||||
modelName,
|
||||
modelFile,
|
||||
undefined, // modelSha256 - calculated by backend
|
||||
undefined, // modelSize - calculated by backend
|
||||
mmProjFile // mmprojPath
|
||||
// mmprojSha256 and mmprojSize omitted - calculated by backend
|
||||
)
|
||||
} else {
|
||||
// Import regular model - let backend calculate SHA256 and size
|
||||
await serviceHub.models().pullModel(modelName, modelFile)
|
||||
}
|
||||
|
||||
toast.success('Model imported successfully', {
|
||||
description: `${modelName} has been imported`,
|
||||
})
|
||||
|
||||
// Reset form and close dialog
|
||||
resetForm()
|
||||
setOpen(false)
|
||||
onSuccess?.()
|
||||
} catch (error) {
|
||||
console.error('Import model error:', error)
|
||||
toast.error('Failed to import model', {
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
})
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setModelFile(null)
|
||||
setMmProjFile(null)
|
||||
setModelName('')
|
||||
setIsVisionModel(false)
|
||||
}
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (!importing) {
|
||||
setOpen(newOpen)
|
||||
if (!newOpen) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Import Model
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Import a GGUF model file to add it to your collection. Enable vision
|
||||
support for models that work with images.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Vision Model Toggle Card */}
|
||||
<div className="border border-main-view-fg/10 rounded-lg p-4 space-y-3 bg-main-view-fg/5">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<IconEye size={20} className="text-accent" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-main-view-fg">
|
||||
Vision Model Support
|
||||
</h3>
|
||||
<p className="text-sm text-main-view-fg/70">
|
||||
Enable if your model supports image understanding (requires
|
||||
MMPROJ file)
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="vision-model"
|
||||
checked={isVisionModel}
|
||||
onCheckedChange={setIsVisionModel}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Name Preview */}
|
||||
{modelName && (
|
||||
<div className="bg-main-view-fg/5 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-main-view-fg/80">
|
||||
Model will be saved as:
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-base font-mono mt-1 text-main-view-fg">
|
||||
{modelName}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Selection Area */}
|
||||
<div className="space-y-4">
|
||||
{/* Model File Selection */}
|
||||
<div className="border border-main-view-fg/10 rounded-lg p-4 space-y-3 bg-main-view-fg/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-main-view-fg">
|
||||
Model File (GGUF)
|
||||
</h3>
|
||||
<span className="text-xs bg-main-view-fg/10 text-main-view-fg/70 px-2 py-1 rounded">
|
||||
Required
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{modelFile ? (
|
||||
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconCheck size={16} className="text-accent" />
|
||||
<span className="text-sm font-medium text-main-view-fg">
|
||||
{modelFile.split(/[\\/]/).pop()}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => handleFileSelect('model')}
|
||||
disabled={importing}
|
||||
className="text-accent hover:text-accent/80"
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
onClick={() => handleFileSelect('model')}
|
||||
disabled={importing}
|
||||
className="w-full h-12 border border-dashed border-main-view-fg/10 bg-main-view"
|
||||
>
|
||||
Select GGUF File
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* MMPROJ File Selection - only show if vision model is enabled */}
|
||||
{isVisionModel && (
|
||||
<div className="border border-main-view-fg/10 rounded-lg p-4 space-y-3 bg-main-view-fg/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-main-view-fg">MMPROJ File</h3>
|
||||
<span className="text-xs bg-accent/10 text-accent px-2 py-1 rounded">
|
||||
Required for Vision
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{mmProjFile ? (
|
||||
<div className="bg-accent/10 border border-accent/20 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconCheck size={16} className="text-accent" />
|
||||
<span className="text-sm font-medium text-main-view-fg">
|
||||
{mmProjFile.split(/[\\/]/).pop()}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => handleFileSelect('mmproj')}
|
||||
disabled={importing}
|
||||
className="text-accent hover:text-accent/80"
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
onClick={() => handleFileSelect('mmproj')}
|
||||
disabled={importing}
|
||||
className="w-full h-12 border border-dashed border-main-view-fg/10 bg-main-view"
|
||||
>
|
||||
<IconFileImport size={18} className="mr-2" />
|
||||
Select MMPROJ File
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 pt-4">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={importing}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={
|
||||
importing ||
|
||||
!modelFile ||
|
||||
!modelName ||
|
||||
(isVisionModel && !mmProjFile)
|
||||
}
|
||||
className="flex-1"
|
||||
>
|
||||
{importing && <IconLoader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{importing ? (
|
||||
'Importing...'
|
||||
) : (
|
||||
<>Import {isVisionModel ? 'Vision ' : ''}Model</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -15,6 +15,7 @@ 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'
|
||||
@ -73,7 +74,6 @@ function ProviderDetail() {
|
||||
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)
|
||||
@ -90,67 +90,24 @@ function ProviderDetail() {
|
||||
!setting.controller_props.value)
|
||||
)
|
||||
|
||||
const handleImportModel = async () => {
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
setImportingModel(true)
|
||||
const selectedFile = await serviceHub.dialog().open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
})
|
||||
// If the dialog returns a file path, extract just the file name
|
||||
const fileName =
|
||||
typeof selectedFile === 'string'
|
||||
? selectedFile.split(/[\\/]/).pop()?.replace(/\s/g, '-')
|
||||
: undefined
|
||||
|
||||
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 serviceHub.models().pullModel(fileName, typeof selectedFile === 'string' ? selectedFile : selectedFile?.[0])
|
||||
// Refresh the provider to update the models list
|
||||
await serviceHub.providers().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)
|
||||
}
|
||||
const handleModelImportSuccess = async () => {
|
||||
// Refresh the provider to update the models list
|
||||
await serviceHub.providers().getProviders().then(setProviders)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Initial data fetch
|
||||
serviceHub.models().getActiveModels().then((models) => setActiveModels(models || []))
|
||||
serviceHub
|
||||
.models()
|
||||
.getActiveModels()
|
||||
.then((models) => setActiveModels(models || []))
|
||||
|
||||
// Set up interval for real-time updates
|
||||
const intervalId = setInterval(() => {
|
||||
serviceHub.models().getActiveModels().then((models) => setActiveModels(models || []))
|
||||
serviceHub
|
||||
.models()
|
||||
.getActiveModels()
|
||||
.then((models) => setActiveModels(models || []))
|
||||
}, 5000)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
@ -199,7 +156,9 @@ function ProviderDetail() {
|
||||
|
||||
setRefreshingModels(true)
|
||||
try {
|
||||
const modelIds = await serviceHub.providers().fetchModelsFromProvider(provider)
|
||||
const modelIds = await serviceHub
|
||||
.providers()
|
||||
.fetchModelsFromProvider(provider)
|
||||
|
||||
// Create new models from the fetched IDs
|
||||
const newModels: Model[] = modelIds.map((id) => ({
|
||||
@ -255,10 +214,15 @@ function ProviderDetail() {
|
||||
setLoadingModels((prev) => [...prev, modelId])
|
||||
if (provider)
|
||||
// Original: startModel(provider, modelId).then(() => { setActiveModels((prevModels) => [...prevModels, modelId]) })
|
||||
serviceHub.models().startModel(provider, modelId)
|
||||
serviceHub
|
||||
.models()
|
||||
.startModel(provider, modelId)
|
||||
.then(() => {
|
||||
// Refresh active models after starting
|
||||
serviceHub.models().getActiveModels().then((models) => setActiveModels(models || []))
|
||||
serviceHub
|
||||
.models()
|
||||
.getActiveModels()
|
||||
.then((models) => setActiveModels(models || []))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error starting model:', error)
|
||||
@ -276,10 +240,15 @@ function ProviderDetail() {
|
||||
|
||||
const handleStopModel = (modelId: string) => {
|
||||
// Original: stopModel(modelId).then(() => { setActiveModels((prevModels) => prevModels.filter((model) => model !== modelId)) })
|
||||
serviceHub.models().stopModel(modelId)
|
||||
serviceHub
|
||||
.models()
|
||||
.stopModel(modelId)
|
||||
.then(() => {
|
||||
// Refresh active models after stopping
|
||||
serviceHub.models().getActiveModels().then((models) => setActiveModels(models || []))
|
||||
serviceHub
|
||||
.models()
|
||||
.getActiveModels()
|
||||
.then((models) => setActiveModels(models || []))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error stopping model:', error)
|
||||
@ -434,10 +403,12 @@ function ProviderDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
serviceHub.providers().updateSettings(
|
||||
providerName,
|
||||
updateObj.settings ?? []
|
||||
)
|
||||
serviceHub
|
||||
.providers()
|
||||
.updateSettings(
|
||||
providerName,
|
||||
updateObj.settings ?? []
|
||||
)
|
||||
updateProvider(providerName, {
|
||||
...provider,
|
||||
...updateObj,
|
||||
@ -553,32 +524,28 @@ function ProviderDetail() {
|
||||
</>
|
||||
)}
|
||||
{provider && provider.provider === 'llamacpp' && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
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">
|
||||
{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">
|
||||
{importingModel
|
||||
? 'Importing...'
|
||||
: t('providers:import')}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
<ImportVisionModelDialog
|
||||
provider={provider}
|
||||
onSuccess={handleModelImportSuccess}
|
||||
trigger={
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline !outline-none focus:outline-none active:outline-none"
|
||||
asChild
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<span className="text-main-view-fg/70">
|
||||
{t('providers:import')}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user