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:
parent
36824c20f8
commit
ae7b94b01e
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user