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.
This commit is contained in:
Junyan Qin
2026-04-19 20:47:51 +08:00
parent f0061817ea
commit 699545a196
5 changed files with 393 additions and 305 deletions

View File

@@ -295,7 +295,7 @@ export default function ModelsDialog({
async function handleScanModels( async function handleScanModels(
providerUuid: string, providerUuid: string,
modelType: ModelType, modelType?: ModelType,
): Promise<ScanModelsResult> { ): Promise<ScanModelsResult> {
try { try {
const resp = await httpClient.scanProviderModels(providerUuid, modelType); const resp = await httpClient.scanProviderModels(providerUuid, modelType);
@@ -319,19 +319,26 @@ export default function ModelsDialog({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
for (const item of models) { for (const item of models) {
if (modelType === 'llm') { const effectiveType = item.model.type || modelType;
if (effectiveType === 'llm') {
await httpClient.createProviderLLMModel({ await httpClient.createProviderLLMModel({
name: item.model.name, name: item.model.name,
provider_uuid: providerUuid, provider_uuid: providerUuid,
abilities: item.abilities, abilities: item.abilities,
extra_args: {}, extra_args: {},
} as never); } as never);
} else { } else if (effectiveType === 'embedding') {
await httpClient.createProviderEmbeddingModel({ await httpClient.createProviderEmbeddingModel({
name: item.model.name, name: item.model.name,
provider_uuid: providerUuid, provider_uuid: providerUuid,
extra_args: {}, extra_args: {},
} as never); } as never);
} else {
await httpClient.createProviderRerankModel({
name: item.model.name,
provider_uuid: providerUuid,
extra_args: {},
} as never);
} }
} }
setAddModelPopoverOpen(null); setAddModelPopoverOpen(null);

View File

@@ -73,10 +73,13 @@ export default function ProviderForm({
>([]); >([]);
useEffect(() => { useEffect(() => {
loadRequesters(); async function init() {
if (providerId) { await loadRequesters();
loadProvider(providerId); if (providerId) {
await loadProvider(providerId);
}
} }
init();
}, [providerId]); }, [providerId]);
async function loadRequesters() { async function loadRequesters() {

View File

@@ -8,7 +8,6 @@ import {
Wrench, Wrench,
Check, Check,
RefreshCw, RefreshCw,
Search,
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -33,6 +32,8 @@ import ExtraArgsEditor from './ExtraArgsEditor';
interface AddModelPopoverProps { interface AddModelPopoverProps {
isOpen: boolean; isOpen: boolean;
initialMode?: 'manual' | 'scan';
trigger?: React.ReactNode;
onOpen: () => void; onOpen: () => void;
onClose: () => void; onClose: () => void;
onAddModel: ( onAddModel: (
@@ -41,7 +42,7 @@ interface AddModelPopoverProps {
abilities: string[], abilities: string[],
extraArgs: ExtraArg[], extraArgs: ExtraArg[],
) => Promise<void>; ) => Promise<void>;
onScanModels: (modelType: ModelType) => Promise<ScanModelsResult>; onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
onAddScannedModels: ( onAddScannedModels: (
modelType: ModelType, modelType: ModelType,
models: SelectedScannedModel[], models: SelectedScannedModel[],
@@ -60,6 +61,8 @@ interface AddModelPopoverProps {
export default function AddModelPopover({ export default function AddModelPopover({
isOpen, isOpen,
initialMode = 'manual',
trigger,
onOpen, onOpen,
onClose, onClose,
onAddModel, onAddModel,
@@ -80,9 +83,7 @@ export default function AddModelPopover({
const [abilities, setAbilities] = useState<string[]>([]); const [abilities, setAbilities] = useState<string[]>([]);
const [extraArgs, setExtraArgs] = useState<ExtraArg[]>([]); const [extraArgs, setExtraArgs] = useState<ExtraArg[]>([]);
const [scanLoading, setScanLoading] = useState(false); const [scanLoading, setScanLoading] = useState(false);
const [scannedModels, setScannedModels] = useState<ScannedProviderModel[]>( const [scannedModels, setScannedModels] = useState<ScannedProviderModel[]>([]);
[],
);
const [selectedScannedModels, setSelectedScannedModels] = useState< const [selectedScannedModels, setSelectedScannedModels] = useState<
Record<string, SelectedScannedModel> Record<string, SelectedScannedModel>
>({}); >({});
@@ -92,7 +93,7 @@ export default function AddModelPopover({
const wasOpen = prevIsOpenRef.current; const wasOpen = prevIsOpenRef.current;
if (isOpen && !wasOpen) { if (isOpen && !wasOpen) {
setTab('llm'); setTab('llm');
setMode('manual'); setMode(initialMode);
setName(''); setName('');
setAbilities([]); setAbilities([]);
setExtraArgs([]); setExtraArgs([]);
@@ -101,8 +102,12 @@ export default function AddModelPopover({
setSelectedScannedModels({}); setSelectedScannedModels({});
setScanQuery(''); setScanQuery('');
onResetTestResult(); onResetTestResult();
if (initialMode === 'scan') {
handleScan();
}
} }
prevIsOpenRef.current = isOpen; prevIsOpenRef.current = isOpen;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, onResetTestResult]); }, [isOpen, onResetTestResult]);
useEffect(() => { useEffect(() => {
@@ -122,9 +127,8 @@ export default function AddModelPopover({
const handleScan = async () => { const handleScan = async () => {
setScanLoading(true); setScanLoading(true);
try { 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 = ( const debugData = (
result.debug?.response as { data?: Record<string, unknown>[] } result.debug?.response as { data?: Record<string, unknown>[] }
)?.data; )?.data;
@@ -143,9 +147,9 @@ export default function AddModelPopover({
| undefined; | undefined;
const tools = features?.tools as Record<string, unknown> | undefined; const tools = features?.tools as Record<string, unknown> | undefined;
if (tools?.function_calling === true) { if (tools?.function_calling === true) {
const abilities = new Set(model.abilities || []); const nextAbilities = new Set(model.abilities || []);
abilities.add('func_call'); nextAbilities.add('func_call');
model.abilities = [...abilities]; model.abilities = [...nextAbilities];
} }
} }
} }
@@ -247,305 +251,321 @@ export default function AddModelPopover({
onOpenChange={(open) => (open ? onOpen() : onClose())} onOpenChange={(open) => (open ? onOpen() : onClose())}
> >
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button {trigger || (
variant="ghost" <Button
size="sm" variant="ghost"
className="h-6 text-xs" size="sm"
onClick={(e) => e.stopPropagation()} className="h-6 text-xs"
> onClick={(e) => e.stopPropagation()}
<Plus className="h-3 w-3 mr-1" /> >
{t('models.addModel')} <Plus className="h-3 w-3 mr-1" />
</Button> {t('models.addModel')}
</Button>
)}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-[min(24rem,calc(100vw-2rem))] max-h-[70vh] overflow-y-auto overscroll-none focus:outline-none focus-visible:outline-none focus-visible:ring-0" className="w-[min(24rem,calc(100vw-2rem))] max-h-[calc(100vh-8rem)] flex flex-col overflow-hidden"
style={{
maxHeight: 'min(70vh, var(--radix-popover-content-available-height))',
}}
align="end" align="end"
side="left" side="bottom"
sideOffset={8} sideOffset={8}
collisionPadding={16} collisionPadding={16}
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Tabs value={tab} onValueChange={(v) => setTab(v as ModelType)}> <Tabs
<TabsList className="grid w-full grid-cols-3"> value={tab}
<TabsTrigger value="llm"> onValueChange={(v) => setTab(v as ModelType)}
<MessageSquareText className="h-4 w-4 mr-1" /> className="flex flex-col min-h-0 flex-1"
{t('models.chat')} >
</TabsTrigger> <div className="flex-shrink-0">
<TabsTrigger value="embedding"> {!(trigger && initialMode === 'scan') && (
<Cpu className="h-4 w-4 mr-1" /> <TabsList className="grid w-full grid-cols-3">
{t('models.embedding')} <TabsTrigger value="llm">
</TabsTrigger> <MessageSquareText className="h-4 w-4 mr-1" />
<TabsTrigger value="rerank"> {t('models.chat')}
<ArrowUpDown className="h-4 w-4 mr-1" /> </TabsTrigger>
{t('models.rerank')} <TabsTrigger value="embedding">
</TabsTrigger> <Cpu className="h-4 w-4 mr-1" />
</TabsList> {t('models.embedding')}
</TabsTrigger>
<TabsTrigger value="rerank">
<ArrowUpDown className="h-4 w-4 mr-1" />
{t('models.rerank')}
</TabsTrigger>
</TabsList>
)}
</div>
<Tabs <div className="overflow-y-auto flex-1 min-h-0">
value={mode} <Tabs
onValueChange={(v) => setMode(v as 'manual' | 'scan')} value={mode}
> onValueChange={(v) => setMode(v as 'manual' | 'scan')}
<TabsList className="grid w-full grid-cols-2 mt-3"> >
<TabsTrigger value="manual">{t('models.manualAdd')}</TabsTrigger> {!trigger && (
<TabsTrigger value="scan">{t('models.scanAdd')}</TabsTrigger> <TabsList className="grid w-full grid-cols-2 mt-3">
</TabsList> <TabsTrigger value="manual">
{t('models.manualAdd')}
</TabsTrigger>
<TabsTrigger value="scan">{t('models.scanAdd')}</TabsTrigger>
</TabsList>
)}
<TabsContent value="manual" className="mt-3"> <TabsContent value="manual" className="mt-3">
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2">
<Label>{t('models.modelName')}</Label>
<Input
placeholder={t('models.modelName')}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
{tab === 'llm' && (
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('models.abilities')}</Label> <Label>{t('models.modelName')}</Label>
<div className="flex gap-4"> <Input
<div className="flex items-center gap-2"> placeholder={t('models.modelName')}
<Checkbox value={name}
id="add-vision" onChange={(e) => setName(e.target.value)}
checked={abilities.includes('vision')} />
onCheckedChange={(checked) => </div>
toggleAbility('vision', checked as boolean)
} {tab === 'llm' && (
/> <div className="space-y-2">
<Label htmlFor="add-vision" className="text-sm"> <Label>{t('models.abilities')}</Label>
<Eye className="h-3 w-3 inline mr-1" /> <div className="flex gap-4">
{t('models.visionAbility')} <div className="flex items-center gap-2">
</Label> <Checkbox
</div> id="add-vision"
<div className="flex items-center gap-2"> checked={abilities.includes('vision')}
<Checkbox onCheckedChange={(checked) =>
id="add-func-call" toggleAbility('vision', checked as boolean)
checked={abilities.includes('func_call')} }
onCheckedChange={(checked) => />
toggleAbility('func_call', checked as boolean) <Label htmlFor="add-vision" className="text-sm">
} <Eye className="h-3 w-3 inline mr-1" />
/> {t('models.visionAbility')}
<Label htmlFor="add-func-call" className="text-sm"> </Label>
<Wrench className="h-3 w-3 inline mr-1" /> </div>
{t('models.functionCallAbility')} <div className="flex items-center gap-2">
</Label> <Checkbox
id="add-func-call"
checked={abilities.includes('func_call')}
onCheckedChange={(checked) =>
toggleAbility('func_call', checked as boolean)
}
/>
<Label htmlFor="add-func-call" className="text-sm">
<Wrench className="h-3 w-3 inline mr-1" />
{t('models.functionCallAbility')}
</Label>
</div>
</div> </div>
</div> </div>
)}
<ExtraArgsEditor
args={extraArgs}
onChange={setExtraArgs}
modelType={tab}
/>
<div className="flex gap-2">
<Button
className="flex-1"
size="sm"
onClick={handleAdd}
disabled={isSubmitting || isTesting}
>
{isSubmitting ? t('common.saving') : t('common.add')}
</Button>
<Button
className="flex-1"
size="sm"
variant="outline"
onClick={handleTest}
disabled={isSubmitting || isTesting}
>
{isTesting ? (
t('common.loading')
) : testResult?.success ? (
<>
<Check className="h-4 w-4 mr-1 text-green-500" />
{(testResult.duration / 1000).toFixed(1)}s
</>
) : (
t('common.test')
)}
</Button>
</div> </div>
</div>
</TabsContent>
<TabsContent value="scan" className="space-y-2 mt-0 pt-0">
{scanLoading ? (
<div className="flex items-center justify-center py-4">
<RefreshCw className="h-4 w-4 mr-2 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{t('models.scanModels')}...
</span>
</div>
) : (
<>
<div className="space-y-2">
<Input
placeholder={t('models.searchScannedModels')}
value={scanQuery}
onChange={(e) => setScanQuery(e.target.value)}
disabled={scannedModels.length === 0}
/>
{selectableModels.length > 0 && (
<div className="flex items-center gap-2 pt-1">
<Checkbox
id="scan-select-all"
checked={allSelected}
onCheckedChange={toggleSelectAll}
/>
<Label
htmlFor="scan-select-all"
className="text-sm font-medium"
>
{t('models.selectAll')}
<span className="text-muted-foreground ml-1">
({Object.keys(selectedScannedModels).length}/
{selectableModels.length})
</span>
</Label>
</div>
)}
</div>
<div
className="h-64 overflow-y-auto overscroll-contain rounded-md border"
onWheel={(e) => e.stopPropagation()}
>
<div className="p-3 space-y-2">
{filteredScannedModels.length === 0 ? (
<p className="text-sm text-muted-foreground">
{scannedModels.length === 0
? t('models.noScannedModels')
: t('models.noScannedModelsMatch')}
</p>
) : (
filteredScannedModels.map((model) => {
const isSelected = Boolean(
selectedScannedModels[model.id],
);
const selectedAbilities =
selectedScannedModels[model.id]?.abilities || [];
return (
<div
key={model.id}
className="rounded-md border p-3 space-y-2"
>
<div className="flex items-start gap-3">
<Checkbox
checked={isSelected || model.already_added}
disabled={model.already_added}
onCheckedChange={(checked) =>
toggleScannedModel(
model,
checked as boolean,
)
}
/>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium break-all">
{model.name}
</div>
<div className="text-xs text-muted-foreground">
{model.already_added
? t('models.alreadyAdded')
: model.type === 'llm'
? t('models.chat')
: model.type === 'embedding'
? t('models.embedding')
: t('models.rerank')}
</div>
</div>
</div>
{model.type === 'llm' &&
isSelected &&
!model.already_added && (
<div className="flex gap-4 pl-7">
<div className="flex items-center gap-2">
<Checkbox
id={`scan-vision-${model.id}`}
checked={selectedAbilities.includes(
'vision',
)}
onCheckedChange={(checked) =>
toggleScannedModelAbility(
model.id,
'vision',
checked as boolean,
)
}
/>
<Label
htmlFor={`scan-vision-${model.id}`}
className="text-sm"
>
<Eye className="h-3 w-3 inline mr-1" />
{t('models.visionAbility')}
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id={`scan-func-${model.id}`}
checked={selectedAbilities.includes(
'func_call',
)}
onCheckedChange={(checked) =>
toggleScannedModelAbility(
model.id,
'func_call',
checked as boolean,
)
}
/>
<Label
htmlFor={`scan-func-${model.id}`}
className="text-sm"
>
<Wrench className="h-3 w-3 inline mr-1" />
{t('models.functionCallAbility')}
</Label>
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
</>
)} )}
<ExtraArgsEditor
args={extraArgs}
onChange={setExtraArgs}
modelType={tab}
/>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
className="flex-1" className="flex-1"
size="sm" size="sm"
onClick={handleAdd} onClick={handleAddScanned}
disabled={isSubmitting || isTesting} disabled={
isSubmitting ||
scanLoading ||
Object.keys(selectedScannedModels).length === 0
}
> >
{isSubmitting ? t('common.saving') : t('common.add')} {isSubmitting
? t('common.saving')
: t('models.addSelectedModels')}
</Button> </Button>
<Button <Button
className="flex-1"
size="sm"
variant="outline" variant="outline"
onClick={handleTest} size="sm"
disabled={isSubmitting || isTesting} onClick={handleScan}
disabled={scanLoading || isSubmitting}
> >
{isTesting ? ( <RefreshCw
t('common.loading') className={`h-3.5 w-3.5 ${scanLoading ? 'animate-spin' : ''}`}
) : testResult?.success ? ( />
<>
<Check className="h-4 w-4 mr-1 text-green-500" />
{(testResult.duration / 1000).toFixed(1)}s
</>
) : (
t('common.test')
)}
</Button> </Button>
</div> </div>
</div> </TabsContent>
</TabsContent> </Tabs>
</div>
<TabsContent value="scan" className="space-y-3 mt-3">
<div className="text-xs text-muted-foreground">
{t('models.scanModelsHint')}
</div>
<div className="flex gap-2">
<Button
className="flex-1"
size="sm"
variant="outline"
onClick={handleScan}
disabled={scanLoading || isSubmitting}
>
{scanLoading ? (
<RefreshCw className="h-4 w-4 mr-1 animate-spin" />
) : (
<Search className="h-4 w-4 mr-1" />
)}
{t('models.scanModels')}
</Button>
<Button
className="flex-1"
size="sm"
onClick={handleAddScanned}
disabled={
isSubmitting ||
scanLoading ||
Object.keys(selectedScannedModels).length === 0
}
>
{isSubmitting
? t('common.saving')
: t('models.addSelectedModels')}
</Button>
</div>
<div className="space-y-2">
<Label>{t('models.scannedModels')}</Label>
<Input
placeholder={t('models.searchScannedModels')}
value={scanQuery}
onChange={(e) => setScanQuery(e.target.value)}
disabled={scannedModels.length === 0}
/>
{selectableModels.length > 0 && (
<div className="flex items-center gap-2 pt-1">
<Checkbox
id="scan-select-all"
checked={allSelected}
onCheckedChange={toggleSelectAll}
/>
<Label
htmlFor="scan-select-all"
className="text-sm font-medium"
>
{t('models.selectAll')}
<span className="text-muted-foreground ml-1">
({Object.keys(selectedScannedModels).length}/
{selectableModels.length})
</span>
</Label>
</div>
)}
</div>
<div
className="h-64 overflow-y-auto overscroll-none rounded-md border"
onWheel={(e) => e.stopPropagation()}
>
<div className="p-3 space-y-2">
{filteredScannedModels.length === 0 ? (
<p className="text-sm text-muted-foreground">
{scannedModels.length === 0
? t('models.noScannedModels')
: t('models.noScannedModelsMatch')}
</p>
) : (
filteredScannedModels.map((model) => {
const isSelected = Boolean(
selectedScannedModels[model.id],
);
const selectedAbilities =
selectedScannedModels[model.id]?.abilities || [];
return (
<div
key={model.id}
className="rounded-md border p-3 space-y-2"
>
<div className="flex items-start gap-3">
<Checkbox
checked={isSelected || model.already_added}
disabled={model.already_added}
onCheckedChange={(checked) =>
toggleScannedModel(model, checked as boolean)
}
/>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium break-all">
{model.name}
</div>
<div className="text-xs text-muted-foreground">
{model.already_added
? t('models.alreadyAdded')
: model.type === 'llm'
? t('models.chat')
: model.type === 'embedding'
? t('models.embedding')
: t('models.rerank')}
</div>
</div>
</div>
{tab === 'llm' &&
isSelected &&
!model.already_added && (
<div className="flex gap-4 pl-7">
<div className="flex items-center gap-2">
<Checkbox
id={`scan-vision-${model.id}`}
checked={selectedAbilities.includes(
'vision',
)}
onCheckedChange={(checked) =>
toggleScannedModelAbility(
model.id,
'vision',
checked as boolean,
)
}
/>
<Label
htmlFor={`scan-vision-${model.id}`}
className="text-sm"
>
<Eye className="h-3 w-3 inline mr-1" />
{t('models.visionAbility')}
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id={`scan-func-${model.id}`}
checked={selectedAbilities.includes(
'func_call',
)}
onCheckedChange={(checked) =>
toggleScannedModelAbility(
model.id,
'func_call',
checked as boolean,
)
}
/>
<Label
htmlFor={`scan-func-${model.id}`}
className="text-sm"
>
<Wrench className="h-3 w-3 inline mr-1" />
{t('models.functionCallAbility')}
</Label>
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
</TabsContent>
</Tabs>
</Tabs> </Tabs>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -6,6 +6,7 @@ import {
Trash2, Trash2,
Settings, Settings,
LogIn, LogIn,
Radar,
} from 'lucide-react'; } from 'lucide-react';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { ModelProvider } from '@/app/infra/entities/api'; import { ModelProvider } from '@/app/infra/entities/api';
@@ -60,7 +61,7 @@ interface ProviderCardProps {
abilities: string[], abilities: string[],
extraArgs: ExtraArg[], extraArgs: ExtraArg[],
) => Promise<void>; ) => Promise<void>;
onScanModels: (modelType: ModelType) => Promise<ScanModelsResult>; onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
onAddScannedModels: ( onAddScannedModels: (
modelType: ModelType, modelType: ModelType,
models: SelectedScannedModel[], models: SelectedScannedModel[],
@@ -130,6 +131,7 @@ export default function ProviderCard({
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteProviderConfirmOpen, setDeleteProviderConfirmOpen] = const [deleteProviderConfirmOpen, setDeleteProviderConfirmOpen] =
useState(false); useState(false);
const [addModelMode, setAddModelMode] = useState<'manual' | 'scan'>('manual');
const canDelete = const canDelete =
!isLangBotModels && !isLangBotModels &&
@@ -310,19 +312,75 @@ export default function ProviderCard({
<div /> <div />
)} )}
{!isLangBotModels && ( {!isLangBotModels && (
<AddModelPopover <div className="flex items-center gap-1">
isOpen={addModelPopoverOpen === provider.uuid} <AddModelPopover
onOpen={onOpenAddModel} isOpen={
onClose={onCloseAddModel} addModelPopoverOpen === provider.uuid &&
onAddModel={onAddModel} addModelMode === 'manual'
onScanModels={onScanModels} }
onAddScannedModels={onAddScannedModels} initialMode="manual"
onTestModel={onTestModel} trigger={
isSubmitting={isSubmitting} <Button
isTesting={isTesting} variant="ghost"
testResult={testResult} size="sm"
onResetTestResult={onResetTestResult} className="h-6 text-xs"
/> onClick={(e) => {
e.stopPropagation();
setAddModelMode('manual');
}}
>
<Plus className="h-3 w-3 mr-1" />
{t('models.addModel')}
</Button>
}
onOpen={() => {
setAddModelMode('manual');
onOpenAddModel();
}}
onClose={onCloseAddModel}
onAddModel={onAddModel}
onScanModels={onScanModels}
onAddScannedModels={onAddScannedModels}
onTestModel={onTestModel}
isSubmitting={isSubmitting}
isTesting={isTesting}
testResult={testResult}
onResetTestResult={onResetTestResult}
/>
<AddModelPopover
isOpen={
addModelPopoverOpen === provider.uuid &&
addModelMode === 'scan'
}
initialMode="scan"
trigger={
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
setAddModelMode('scan');
}}
>
<Radar className="h-3 w-3" />
</Button>
}
onOpen={() => {
setAddModelMode('scan');
onOpenAddModel();
}}
onClose={onCloseAddModel}
onAddModel={onAddModel}
onScanModels={onScanModels}
onAddScannedModels={onAddScannedModels}
onTestModel={onTestModel}
isSubmitting={isSubmitting}
isTesting={isTesting}
testResult={testResult}
onResetTestResult={onResetTestResult}
/>
</div>
)} )}
</div> </div>
</CardHeader> </CardHeader>

View File

@@ -90,7 +90,7 @@ export interface ProviderCardProps {
abilities: string[], abilities: string[],
extraArgs: ExtraArg[], extraArgs: ExtraArg[],
) => Promise<void>; ) => Promise<void>;
onScanModels: (modelType: ModelType) => Promise<ScanModelsResult>; onScanModels: (modelType?: ModelType) => Promise<ScanModelsResult>;
onAddScannedModels: ( onAddScannedModels: (
modelType: ModelType, modelType: ModelType,
models: SelectedScannedModel[], models: SelectedScannedModel[],