fix(web): filter model selector by model name (#35624)

This commit is contained in:
Wu Tianwei 2026-04-28 14:55:10 +08:00 committed by fatelei
parent bfee04be8c
commit 217ab4d2c3
No known key found for this signature in database
GPG Key ID: 2F91DA05646F4EED
2 changed files with 179 additions and 10 deletions

View File

@ -55,7 +55,14 @@ vi.mock('../../hooks', async () => {
})
vi.mock('../popup-item', () => ({
default: ({ model }: { model: Model }) => <div>{model.provider}</div>,
default: ({ model }: { model: Model }) => (
<div>
<span>{model.provider}</span>
{model.models.map(modelItem => (
<span key={modelItem.model}>{modelItem.model}</span>
))}
</div>
),
}))
vi.mock('@/context/provider-context', () => ({
@ -207,6 +214,156 @@ describe('Popup', () => {
expect((input as HTMLInputElement).value).toBe('')
})
it('should show matching models when searching by model name', () => {
renderPopup(
<Popup
modelList={[
makeModel({
models: [makeModelItem({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' } })],
}),
makeModel({
provider: 'anthropic',
label: { en_US: 'Anthropic', zh_Hans: 'Anthropic' },
models: [makeModelItem({ model: 'claude-3', label: { en_US: 'Claude 3', zh_Hans: 'Claude 3' } })],
}),
]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'claude' } },
)
expect(screen.queryByText('openai')).not.toBeInTheDocument()
expect(screen.getByText('anthropic')).toBeInTheDocument()
expect(screen.getByText('claude-3')).toBeInTheDocument()
expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
expect(screen.queryByText('No model found for \u201Cclaude\u201D')).not.toBeInTheDocument()
})
it('should show empty search placeholder when no provider or model name matches', () => {
renderPopup(
<Popup
modelList={[
makeModel({
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [
makeModelItem({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' } }),
],
}),
]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'mistral' } },
)
expect(screen.getByText('No model found for \u201Cmistral\u201D'))!.toBeInTheDocument()
expect(screen.queryByText('openai')).not.toBeInTheDocument()
expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
})
it('should show all models of a provider when searching by provider label', () => {
renderPopup(
<Popup
modelList={[
makeModel({
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [
makeModelItem({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' } }),
makeModelItem({ model: 'gpt-4o', label: { en_US: 'GPT-4o', zh_Hans: 'GPT-4o' } }),
],
}),
makeModel({
provider: 'anthropic',
label: { en_US: 'Anthropic', zh_Hans: 'Anthropic' },
models: [
makeModelItem({ model: 'claude-3', label: { en_US: 'Claude 3', zh_Hans: 'Claude 3' } }),
],
}),
]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'openai' } },
)
expect(screen.getByText('openai'))!.toBeInTheDocument()
expect(screen.getByText('gpt-4'))!.toBeInTheDocument()
expect(screen.getByText('gpt-4o'))!.toBeInTheDocument()
expect(screen.queryByText('anthropic')).not.toBeInTheDocument()
expect(screen.queryByText('claude-3')).not.toBeInTheDocument()
})
it('should match by model provider key when model label does not contain the search text', () => {
renderPopup(
<Popup
modelList={[
makeModel({
provider: 'azure_openai',
label: { en_US: 'Azure', zh_Hans: 'Azure' },
models: [
makeModelItem({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' } }),
],
}),
]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'openai' } },
)
expect(screen.getByText('azure_openai'))!.toBeInTheDocument()
expect(screen.getByText('gpt-4'))!.toBeInTheDocument()
})
it('should still apply scope features when matching by provider label', () => {
mockSupportFunctionCall.mockReturnValue(false)
renderPopup(
<Popup
modelList={[
makeModel({
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [
makeModelItem({ model: 'gpt-4', features: [ModelFeatureEnum.vision] }),
makeModelItem({ model: 'gpt-4-tool', features: [ModelFeatureEnum.toolCall] }),
],
}),
]}
onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.toolCall]}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'openai' } },
)
expect(screen.getByText('No model found for \u201Copenai\u201D'))!.toBeInTheDocument()
expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
expect(screen.queryByText('gpt-4-tool')).not.toBeInTheDocument()
})
it('should not show compatible-only helper text when no scope features are applied', () => {
renderPopup(
<Popup

View File

@ -143,18 +143,26 @@ const Popup: FC<PopupProps> = ({
}, [aiCreditVisibleProviders, installedProviderMap, modelList])
const filteredModelList = useMemo(() => {
const normalizedSearch = searchText.toLowerCase()
const matchesLabel = (label: Record<string, string>) => {
if (label[language] !== undefined)
return label[language].toLowerCase().includes(normalizedSearch)
return Object.values(label).some(value =>
value.toLowerCase().includes(normalizedSearch),
)
}
const filtered = installedModelList.map((model) => {
const matchesProviderSearch = !searchText
|| model.provider.toLowerCase().includes(searchText.toLowerCase())
|| Object.values(model.label).some(label => label.toLowerCase().includes(searchText.toLowerCase()))
const providerMatched = !!searchText && (
matchesLabel(model.label)
|| model.provider.toLowerCase().includes(normalizedSearch)
)
const filteredModels = model.models
.filter((modelItem) => {
if (modelItem.label[language] !== undefined)
return modelItem.label[language].toLowerCase().includes(searchText.toLowerCase())
return Object.values(modelItem.label).some(label =>
label.toLowerCase().includes(searchText.toLowerCase()),
)
if (!searchText || providerMatched)
return true
return matchesLabel(modelItem.label)
})
.filter((modelItem) => {
if (scopeFeatures.length === 0)
@ -165,8 +173,12 @@ const Popup: FC<PopupProps> = ({
return modelItem.features?.includes(feature) ?? false
})
})
if (!matchesProviderSearch || (filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider)))
if (
(searchText && filteredModels.length === 0)
|| (!searchText && filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider))
) {
return null
}
return { ...model, models: filteredModels }
}).filter((model): model is Model => model !== null)