From ae7b94b01ea057314f70d50b005f9f6987359c31 Mon Sep 17 00:00:00 2001 From: one Date: Sat, 22 Mar 2025 21:44:00 +0800 Subject: [PATCH] feat: add a search bar to model list (#3788) * feat: add a search bar to model list * feat: make the search bar collapsible --- .../settings/ProviderSettings/ModelList.tsx | 21 ++++++- .../ProviderSettings/ModelListSearchBar.tsx | 58 +++++++++++++++++++ .../ProviderSettings/ProviderSetting.tsx | 23 +++++--- 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 src/renderer/src/pages/settings/ProviderSettings/ModelListSearchBar.tsx diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx index b501abb8..47857582 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList.tsx @@ -20,7 +20,7 @@ import { Model, Provider } from '@renderer/types' import { maskApiKey } from '@renderer/utils/api' import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd' 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 styled from 'styled-components' @@ -38,6 +38,7 @@ const STATUS_COLORS = { interface ModelListProps { provider: Provider modelStatuses?: ModelStatus[] + searchText?: string } export interface ModelStatus { @@ -165,7 +166,7 @@ function useModelStatusRendering() { return { renderStatusIndicator, renderLatencyText } } -const ModelList: React.FC = ({ provider: _provider, modelStatuses = [] }) => { +const ModelList: React.FC = ({ provider: _provider, modelStatuses = [], searchText = '' }) => { const { t } = useTranslation() const { provider } = useProvider(_provider.id) const { updateProvider, models, removeModel } = useProvider(_provider.id) @@ -179,7 +180,21 @@ const ModelList: React.FC = ({ provider: _provider, modelStatuse const modelsWebsite = providerConfig?.websites?.models const [editingModel, setEditingModel] = useState(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]) => { acc[key] = value return acc diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelListSearchBar.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelListSearchBar.tsx new file mode 100644 index 00000000..d7b785d1 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelListSearchBar.tsx @@ -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 = ({ 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 ? ( + } + 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} + /> + ) : ( + + setSearchVisible(true)} /> + + ) +} + +export default ModelListSearchBar diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 792fc022..a2566d36 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -12,7 +12,7 @@ import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/servic import { Provider } from '@renderer/types' import { formatApiHost } from '@renderer/utils/api' 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 { isEmpty } from 'lodash' import { FC, useEffect, useState } from 'react' @@ -34,6 +34,7 @@ import GraphRAGSettings from './GraphRAGSettings' import HealthCheckPopup from './HealthCheckPopup' import LMStudioSettings from './LMStudioSettings' import ModelList, { ModelStatus } from './ModelList' +import ModelListSearchBar from './ModelListSearchBar' import OllamSettings from './OllamaSettings' import ProviderSettingsPopup from './ProviderSettingsPopup' import SelectProviderModelPopup from './SelectProviderModelPopup' @@ -49,6 +50,7 @@ const ProviderSetting: FC = ({ provider: _provider }) => { const [apiVersion, setApiVersion] = useState(provider.apiVersion) const [apiValid, setApiValid] = useState(false) const [apiChecking, setApiChecking] = useState(false) + const [searchText, setSearchText] = useState('') const { updateProvider, models } = useProvider(provider.id) const { t } = useTranslation() const { theme } = useTheme() @@ -361,22 +363,25 @@ const ProviderSetting: FC = ({ provider: _provider }) => { )} {provider.id === 'copilot' && } - - {t('common.models')} + - {!isEmpty(models) && ( + {t('common.models')} + {!isEmpty(models) && } + + {!isEmpty(models) && ( + - )} - - + /> + + )} + - + ) }