diff --git a/web/src/app/home/components/models-dialog/ModelsDialog.tsx b/web/src/app/home/components/models-dialog/ModelsDialog.tsx index d1c3e47e..16c6663d 100644 --- a/web/src/app/home/components/models-dialog/ModelsDialog.tsx +++ b/web/src/app/home/components/models-dialog/ModelsDialog.tsx @@ -295,7 +295,7 @@ export default function ModelsDialog({ async function handleScanModels( providerUuid: string, - modelType: ModelType, + modelType?: ModelType, ): Promise { try { const resp = await httpClient.scanProviderModels(providerUuid, modelType); @@ -319,19 +319,26 @@ export default function ModelsDialog({ setIsSubmitting(true); try { for (const item of models) { - if (modelType === 'llm') { + const effectiveType = item.model.type || modelType; + if (effectiveType === 'llm') { await httpClient.createProviderLLMModel({ name: item.model.name, provider_uuid: providerUuid, abilities: item.abilities, extra_args: {}, } as never); - } else { + } else if (effectiveType === 'embedding') { await httpClient.createProviderEmbeddingModel({ name: item.model.name, provider_uuid: providerUuid, extra_args: {}, } as never); + } else { + await httpClient.createProviderRerankModel({ + name: item.model.name, + provider_uuid: providerUuid, + extra_args: {}, + } as never); } } setAddModelPopoverOpen(null); diff --git a/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx index df3aea26..c596037a 100644 --- a/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx +++ b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx @@ -73,10 +73,13 @@ export default function ProviderForm({ >([]); useEffect(() => { - loadRequesters(); - if (providerId) { - loadProvider(providerId); + async function init() { + await loadRequesters(); + if (providerId) { + await loadProvider(providerId); + } } + init(); }, [providerId]); async function loadRequesters() { diff --git a/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx index c9dab2cd..b4de04dc 100644 --- a/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx +++ b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx @@ -8,7 +8,6 @@ import { Wrench, Check, RefreshCw, - Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -33,6 +32,8 @@ import ExtraArgsEditor from './ExtraArgsEditor'; interface AddModelPopoverProps { isOpen: boolean; + initialMode?: 'manual' | 'scan'; + trigger?: React.ReactNode; onOpen: () => void; onClose: () => void; onAddModel: ( @@ -41,7 +42,7 @@ interface AddModelPopoverProps { abilities: string[], extraArgs: ExtraArg[], ) => Promise; - onScanModels: (modelType: ModelType) => Promise; + onScanModels: (modelType?: ModelType) => Promise; onAddScannedModels: ( modelType: ModelType, models: SelectedScannedModel[], @@ -60,6 +61,8 @@ interface AddModelPopoverProps { export default function AddModelPopover({ isOpen, + initialMode = 'manual', + trigger, onOpen, onClose, onAddModel, @@ -80,9 +83,7 @@ export default function AddModelPopover({ const [abilities, setAbilities] = useState([]); const [extraArgs, setExtraArgs] = useState([]); const [scanLoading, setScanLoading] = useState(false); - const [scannedModels, setScannedModels] = useState( - [], - ); + const [scannedModels, setScannedModels] = useState([]); const [selectedScannedModels, setSelectedScannedModels] = useState< Record >({}); @@ -92,7 +93,7 @@ export default function AddModelPopover({ const wasOpen = prevIsOpenRef.current; if (isOpen && !wasOpen) { setTab('llm'); - setMode('manual'); + setMode(initialMode); setName(''); setAbilities([]); setExtraArgs([]); @@ -101,8 +102,12 @@ export default function AddModelPopover({ setSelectedScannedModels({}); setScanQuery(''); onResetTestResult(); + if (initialMode === 'scan') { + handleScan(); + } } prevIsOpenRef.current = isOpen; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, onResetTestResult]); useEffect(() => { @@ -122,9 +127,8 @@ export default function AddModelPopover({ const handleScan = async () => { setScanLoading(true); try { - const result = await onScanModels(tab); + const result = await onScanModels(trigger ? undefined : tab); - // Enrich abilities from debug.response.data (e.g. features.tools.function_calling) const debugData = ( result.debug?.response as { data?: Record[] } )?.data; @@ -143,9 +147,9 @@ export default function AddModelPopover({ | undefined; const tools = features?.tools as Record | undefined; if (tools?.function_calling === true) { - const abilities = new Set(model.abilities || []); - abilities.add('func_call'); - model.abilities = [...abilities]; + const nextAbilities = new Set(model.abilities || []); + nextAbilities.add('func_call'); + model.abilities = [...nextAbilities]; } } } @@ -247,305 +251,321 @@ export default function AddModelPopover({ onOpenChange={(open) => (open ? onOpen() : onClose())} > - + {trigger || ( + + )} e.stopPropagation()} - onTouchMove={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} > - setTab(v as ModelType)}> - - - - {t('models.chat')} - - - - {t('models.embedding')} - - - - {t('models.rerank')} - - + setTab(v as ModelType)} + className="flex flex-col min-h-0 flex-1" + > +
+ {!(trigger && initialMode === 'scan') && ( + + + + {t('models.chat')} + + + + {t('models.embedding')} + + + + {t('models.rerank')} + + + )} +
- setMode(v as 'manual' | 'scan')} - > - - {t('models.manualAdd')} - {t('models.scanAdd')} - +
+ setMode(v as 'manual' | 'scan')} + > + {!trigger && ( + + + {t('models.manualAdd')} + + {t('models.scanAdd')} + + )} - -
-
- - setName(e.target.value)} - /> -
- - {tab === 'llm' && ( + +
- -
-
- - toggleAbility('vision', checked as boolean) - } - /> - -
-
- - toggleAbility('func_call', checked as boolean) - } - /> - + + setName(e.target.value)} + /> +
+ + {tab === 'llm' && ( +
+ +
+
+ + toggleAbility('vision', checked as boolean) + } + /> + +
+
+ + toggleAbility('func_call', checked as boolean) + } + /> + +
+ )} + + +
+ +
+
+ + + + {scanLoading ? ( +
+ + + {t('models.scanModels')}... + +
+ ) : ( + <> +
+ setScanQuery(e.target.value)} + disabled={scannedModels.length === 0} + /> + {selectableModels.length > 0 && ( +
+ + +
+ )} +
+ +
e.stopPropagation()} + > +
+ {filteredScannedModels.length === 0 ? ( +

+ {scannedModels.length === 0 + ? t('models.noScannedModels') + : t('models.noScannedModelsMatch')} +

+ ) : ( + filteredScannedModels.map((model) => { + const isSelected = Boolean( + selectedScannedModels[model.id], + ); + const selectedAbilities = + selectedScannedModels[model.id]?.abilities || []; + return ( +
+
+ + toggleScannedModel( + model, + checked as boolean, + ) + } + /> +
+
+ {model.name} +
+
+ {model.already_added + ? t('models.alreadyAdded') + : model.type === 'llm' + ? t('models.chat') + : model.type === 'embedding' + ? t('models.embedding') + : t('models.rerank')} +
+
+
+ + {model.type === 'llm' && + isSelected && + !model.already_added && ( +
+
+ + toggleScannedModelAbility( + model.id, + 'vision', + checked as boolean, + ) + } + /> + +
+
+ + toggleScannedModelAbility( + model.id, + 'func_call', + checked as boolean, + ) + } + /> + +
+
+ )} +
+ ); + }) + )} +
+
+ )} -
-
- - - -
- {t('models.scanModelsHint')} -
- -
- - -
- -
- - setScanQuery(e.target.value)} - disabled={scannedModels.length === 0} - /> - {selectableModels.length > 0 && ( -
- - -
- )} -
- -
e.stopPropagation()} - > -
- {filteredScannedModels.length === 0 ? ( -

- {scannedModels.length === 0 - ? t('models.noScannedModels') - : t('models.noScannedModelsMatch')} -

- ) : ( - filteredScannedModels.map((model) => { - const isSelected = Boolean( - selectedScannedModels[model.id], - ); - const selectedAbilities = - selectedScannedModels[model.id]?.abilities || []; - return ( -
-
- - toggleScannedModel(model, checked as boolean) - } - /> -
-
- {model.name} -
-
- {model.already_added - ? t('models.alreadyAdded') - : model.type === 'llm' - ? t('models.chat') - : model.type === 'embedding' - ? t('models.embedding') - : t('models.rerank')} -
-
-
- - {tab === 'llm' && - isSelected && - !model.already_added && ( -
-
- - toggleScannedModelAbility( - model.id, - 'vision', - checked as boolean, - ) - } - /> - -
-
- - toggleScannedModelAbility( - model.id, - 'func_call', - checked as boolean, - ) - } - /> - -
-
- )} -
- ); - }) - )} -
-
-
- + + +
diff --git a/web/src/app/home/components/models-dialog/components/ProviderCard.tsx b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx index 4ccaed56..029f5e6f 100644 --- a/web/src/app/home/components/models-dialog/components/ProviderCard.tsx +++ b/web/src/app/home/components/models-dialog/components/ProviderCard.tsx @@ -6,6 +6,7 @@ import { Trash2, Settings, LogIn, + Radar, } from 'lucide-react'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; import { ModelProvider } from '@/app/infra/entities/api'; @@ -60,7 +61,7 @@ interface ProviderCardProps { abilities: string[], extraArgs: ExtraArg[], ) => Promise; - onScanModels: (modelType: ModelType) => Promise; + onScanModels: (modelType?: ModelType) => Promise; onAddScannedModels: ( modelType: ModelType, models: SelectedScannedModel[], @@ -130,6 +131,7 @@ export default function ProviderCard({ const { t } = useTranslation(); const [deleteProviderConfirmOpen, setDeleteProviderConfirmOpen] = useState(false); + const [addModelMode, setAddModelMode] = useState<'manual' | 'scan'>('manual'); const canDelete = !isLangBotModels && @@ -310,19 +312,75 @@ export default function ProviderCard({
)} {!isLangBotModels && ( - +
+ { + e.stopPropagation(); + setAddModelMode('manual'); + }} + > + + {t('models.addModel')} + + } + onOpen={() => { + setAddModelMode('manual'); + onOpenAddModel(); + }} + onClose={onCloseAddModel} + onAddModel={onAddModel} + onScanModels={onScanModels} + onAddScannedModels={onAddScannedModels} + onTestModel={onTestModel} + isSubmitting={isSubmitting} + isTesting={isTesting} + testResult={testResult} + onResetTestResult={onResetTestResult} + /> + { + e.stopPropagation(); + setAddModelMode('scan'); + }} + > + + + } + onOpen={() => { + setAddModelMode('scan'); + onOpenAddModel(); + }} + onClose={onCloseAddModel} + onAddModel={onAddModel} + onScanModels={onScanModels} + onAddScannedModels={onAddScannedModels} + onTestModel={onTestModel} + isSubmitting={isSubmitting} + isTesting={isTesting} + testResult={testResult} + onResetTestResult={onResetTestResult} + /> +
)}
diff --git a/web/src/app/home/components/models-dialog/types.ts b/web/src/app/home/components/models-dialog/types.ts index 1fa6d784..d2ecb7f1 100644 --- a/web/src/app/home/components/models-dialog/types.ts +++ b/web/src/app/home/components/models-dialog/types.ts @@ -90,7 +90,7 @@ export interface ProviderCardProps { abilities: string[], extraArgs: ExtraArg[], ) => Promise; - onScanModels: (modelType: ModelType) => Promise; + onScanModels: (modelType?: ModelType) => Promise; onAddScannedModels: ( modelType: ModelType, models: SelectedScannedModel[],