feat: add a search bar to model list (#3788)

* feat: add a search bar to model list

* feat: make the search bar collapsible
This commit is contained in:
one 2025-03-22 21:44:00 +08:00 committed by GitHub
parent 36824c20f8
commit ae7b94b01e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 12 deletions

View File

@ -20,7 +20,7 @@ import { Model, Provider } from '@renderer/types'
import { maskApiKey } from '@renderer/utils/api' import { maskApiKey } from '@renderer/utils/api'
import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd' import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd'
import { groupBy, sortBy, toPairs } from 'lodash' import { groupBy, sortBy, toPairs } from 'lodash'
import React, { useCallback, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -38,6 +38,7 @@ const STATUS_COLORS = {
interface ModelListProps { interface ModelListProps {
provider: Provider provider: Provider
modelStatuses?: ModelStatus[] modelStatuses?: ModelStatus[]
searchText?: string
} }
export interface ModelStatus { export interface ModelStatus {
@ -165,7 +166,7 @@ function useModelStatusRendering() {
return { renderStatusIndicator, renderLatencyText } return { renderStatusIndicator, renderLatencyText }
} }
const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuses = [] }) => { const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuses = [], searchText = '' }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { provider } = useProvider(_provider.id) const { provider } = useProvider(_provider.id)
const { updateProvider, models, removeModel } = useProvider(_provider.id) const { updateProvider, models, removeModel } = useProvider(_provider.id)
@ -179,7 +180,21 @@ const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuse
const modelsWebsite = providerConfig?.websites?.models const modelsWebsite = providerConfig?.websites?.models
const [editingModel, setEditingModel] = useState<Model | null>(null) const [editingModel, setEditingModel] = useState<Model | null>(null)
const modelGroups = groupBy(models, 'group') const [debouncedSearchText, setDebouncedSearchText] = useState(searchText)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchText(searchText)
}, 50)
return () => clearTimeout(timer)
}, [searchText])
const filteredModels = debouncedSearchText
? models.filter((model) => model.name.toLowerCase().includes(debouncedSearchText.toLowerCase()))
: models
const modelGroups = groupBy(filteredModels, 'group')
const sortedModelGroups = sortBy(toPairs(modelGroups), [0]).reduce((acc, [key, value]) => { const sortedModelGroups = sortBy(toPairs(modelGroups), [0]).reduce((acc, [key, value]) => {
acc[key] = value acc[key] = value
return acc return acc

View File

@ -0,0 +1,58 @@
import { SearchOutlined } from '@ant-design/icons'
import { Input, Tooltip } from 'antd'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ModelListSearchBarProps {
onSearch: (text: string) => void
}
/**
* A collapsible search bar for the model list
* Renders as an icon initially, expands to full search input when clicked
*/
const ModelListSearchBar: React.FC<ModelListSearchBarProps> = ({ onSearch }) => {
const { t } = useTranslation()
const [searchVisible, setSearchVisible] = useState(false)
const [searchText, setSearchText] = useState('')
const handleTextChange = (text: string) => {
setSearchText(text)
onSearch(text)
}
const handleClear = () => {
setSearchText('')
setSearchVisible(false)
onSearch('')
}
return searchVisible ? (
<Input
type="text"
placeholder={t('models.search')}
size="small"
style={{ width: '160px' }}
suffix={<SearchOutlined style={{ color: 'var(--color-text-3)' }} />}
onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
handleTextChange('')
if (!searchText) setSearchVisible(false)
}
}}
onBlur={() => {
if (!searchText) setSearchVisible(false)
}}
autoFocus
allowClear
onClear={handleClear}
/>
) : (
<Tooltip title={t('models.search')} mouseEnterDelay={0.5}>
<SearchOutlined onClick={() => setSearchVisible(true)} />
</Tooltip>
)
}
export default ModelListSearchBar

View File

@ -12,7 +12,7 @@ import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/servic
import { Provider } from '@renderer/types' import { Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api' import { formatApiHost } from '@renderer/utils/api'
import { providerCharge } from '@renderer/utils/oauth' import { providerCharge } from '@renderer/utils/oauth'
import { Button, Divider, Flex, Input, Space, Switch } from 'antd' import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
@ -34,6 +34,7 @@ import GraphRAGSettings from './GraphRAGSettings'
import HealthCheckPopup from './HealthCheckPopup' import HealthCheckPopup from './HealthCheckPopup'
import LMStudioSettings from './LMStudioSettings' import LMStudioSettings from './LMStudioSettings'
import ModelList, { ModelStatus } from './ModelList' import ModelList, { ModelStatus } from './ModelList'
import ModelListSearchBar from './ModelListSearchBar'
import OllamSettings from './OllamaSettings' import OllamSettings from './OllamaSettings'
import ProviderSettingsPopup from './ProviderSettingsPopup' import ProviderSettingsPopup from './ProviderSettingsPopup'
import SelectProviderModelPopup from './SelectProviderModelPopup' import SelectProviderModelPopup from './SelectProviderModelPopup'
@ -49,6 +50,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const [apiVersion, setApiVersion] = useState(provider.apiVersion) const [apiVersion, setApiVersion] = useState(provider.apiVersion)
const [apiValid, setApiValid] = useState(false) const [apiValid, setApiValid] = useState(false)
const [apiChecking, setApiChecking] = useState(false) const [apiChecking, setApiChecking] = useState(false)
const [searchText, setSearchText] = useState('')
const { updateProvider, models } = useProvider(provider.id) const { updateProvider, models } = useProvider(provider.id)
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
@ -361,22 +363,25 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
)} )}
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />} {provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
<SettingSubtitle style={{ marginBottom: 5 }}> <SettingSubtitle style={{ marginBottom: 5 }}>
<Flex align="center" justify="space-between" style={{ width: '100%' }}> <Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<span>{t('common.models')}</span>
<Space> <Space>
{!isEmpty(models) && ( <span>{t('common.models')}</span>
{!isEmpty(models) && <ModelListSearchBar onSearch={setSearchText} />}
</Space>
{!isEmpty(models) && (
<Tooltip title={t('settings.models.check.button_caption')} mouseEnterDelay={0.5}>
<Button <Button
type="text" type="text"
size="small" size="small"
icon={<HeartOutlined />} icon={<HeartOutlined />}
onClick={onHealthCheck} onClick={onHealthCheck}
loading={isHealthChecking} loading={isHealthChecking}
title={t('settings.models.check.button_caption')}></Button> />
)} </Tooltip>
</Space> )}
</Flex> </Space>
</SettingSubtitle> </SettingSubtitle>
<ModelList provider={provider} modelStatuses={modelStatuses} /> <ModelList provider={provider} modelStatuses={modelStatuses} searchText={searchText} />
</SettingContainer> </SettingContainer>
) )
} }