From 699545a19687ddaf4363e344333e60f77020a3d2 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 19 Apr 2026 20:47:51 +0800 Subject: [PATCH] fix(web): fix models dialog provider type select and split add/scan popovers 1. Fix provider type select showing blank when editing: await loadRequesters() before loadProvider() to ensure options are populated before setting the selected value. 2. Split 'Add Model' into two separate entries: a '+ Add Model' button for manual add and a Radar icon button for scan. Each opens its own popover with only one layer of tabs (model type for manual, no tabs for scan since types are auto-detected). 3. Fix popover position: side='bottom' instead of 'left'. 4. Fix popover scroll: model type tabs stay fixed at top, content area scrolls independently when it overflows. 5. Scan mode now fetches all model types at once (no modelType filter), and routes each scanned model to the correct API based on its own type field. --- .../components/models-dialog/ModelsDialog.tsx | 13 +- .../component/provider-form/ProviderForm.tsx | 9 +- .../components/AddModelPopover.tsx | 588 +++++++++--------- .../models-dialog/components/ProviderCard.tsx | 86 ++- .../home/components/models-dialog/types.ts | 2 +- 5 files changed, 393 insertions(+), 305 deletions(-) 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[],