diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.scss b/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.scss index dac151a12b..77d0c853e3 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.scss +++ b/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.scss @@ -39,4 +39,8 @@ flex: 0 0 auto; max-inline-size: 100%; } + + > [data-openbitfun-component='tooltip-trigger'] { + flex: 0 0 auto; + } } diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.tsx b/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.tsx index 4078b22423..c5b25774c5 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.tsx +++ b/src/web-ui/src/app/scenes/settings/pages/ai/MagpieSourceSection.tsx @@ -1,5 +1,5 @@ import { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react'; -import { Button, Checkbox, Input, SearchField, StatusPill } from '@openbitfun/ui'; +import { Button, Checkbox, Icon, IconButton, Input, SearchField, StatusPill, Tooltip } from '@openbitfun/ui'; import { ConfigPageRow } from '@/infrastructure/config/components/common'; import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { useSettingsDraft } from '@/infrastructure/config/settingsDraftRegistry'; @@ -16,6 +16,7 @@ interface Props { ready: boolean; onModelsChange: (models: AIModelConfig[]) => void; onManual: () => void; + onRemoveModel: (model: AIModelConfig, trigger?: HTMLElement) => void; } export function MagpieSourceSection(props: Props) { @@ -23,7 +24,7 @@ export function MagpieSourceSection(props: Props) { return ; } -function MagpieSourceEditor({ models, ready, onModelsChange, onManual }: Props) { +function MagpieSourceEditor({ models, ready, onModelsChange, onManual, onRemoveModel }: Props) { const { t, formatNumber } = useI18n('settings/models'); const [scope] = useState(getActiveSurfaceScope); const panelId = useId(); @@ -46,10 +47,12 @@ function MagpieSourceEditor({ models, ready, onModelsChange, onManual }: Props) const supported = useSyncExternalStore( listener => peerConnectionManager.subscribe(listener), supportsMagpieDiscovery, supportsMagpieDiscovery, ); - const imported = new Set((connection?.models ?? models.filter(model => + const activeConnection = connection ? connections.find(item => item.id === connection.id) : undefined; + const importedModels = activeConnection?.models ?? (connection ? [] : models.filter(model => magpieGatewayUrl(model.base_url) === magpieGatewayUrl(baseUrl) && (model.api_key?.trim() || MAGPIE_APP_KEY) === (apiKey.trim() || MAGPIE_APP_KEY) - && (!model.auth || model.auth.type === 'api_key'))).map(model => model.model_name)); + && (!model.auth || model.auth.type === 'api_key'))); + const imported = new Set(importedModels.map(model => model.model_name)); const catalogReady = result?.status === 'ready' || result?.status === 'empty'; const credentialsChanged = baseUrl !== baseline.baseUrl || apiKey !== baseline.apiKey; const dirty = selected.length > 0 || credentialsChanged; @@ -173,7 +176,27 @@ function MagpieSourceEditor({ models, ready, onModelsChange, onManual }: Props) label={model.display_name || model.id} description={model.id} onChange={event => setSelected(previous => event.target.checked ? [...previous, model.id] : previous.filter(id => id !== model.id))} /> - {imported.has(model.id) && {t('magpie.imported')}} + {imported.has(model.id) && <> + {t('magpie.imported')} + + } + onClick={event => { + event.stopPropagation(); + const config = importedModels.find(item => item.model_name === model.id); + if (config) onRemoveModel(config, event.currentTarget); + }} + /> + + } {model.id.startsWith('group/') && {t('magpie.routingGroup')}} {issue && {importIssues[issue]}} ; diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx index 52b36e4d94..2e8c7a4b4a 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx +++ b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx @@ -170,6 +170,8 @@ describe('ModelSettingsPage loading and provider actions', () => { base_url: base, api_key: key, provider: 'openai', capabilities: ['text_chat', 'function_calling'] }); expect(inference).not.toHaveBeenCalled(); expect(panel().querySelector('input[type="checkbox"]')!.disabled).toBe(true); + expect(panel().querySelector('[data-testid="settings-magpie-remove-model"]')?.dataset.modelId) + .toBe('provider/model-with-tools'); }); it.each(['empty', 'unreachable', 'access_denied', 'unrecognized', 'catalog_error'] as const)( diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx index 0fe6987b4d..ed1fccaabb 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx +++ b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx @@ -4864,7 +4864,8 @@ const ModelSettingsPage: React.FC = () => { ) : {t('acquisition.noneConfigured')}} requestEditorOpen('new-provider:magpie', () => handleSelectProvider('magpie'))} /> + onManual={() => requestEditorOpen('new-provider:magpie', () => handleSelectProvider('magpie'))} + onRemoveModel={(config, trigger) => { void requestDelete(config, trigger); }} />