From 548916e6e176991b42677cf57c99f8f6e16ed845 Mon Sep 17 00:00:00 2001 From: one Date: Mon, 25 Aug 2025 20:35:48 +0800 Subject: [PATCH] feat(McpServersList): add a search bar (#9520) * feat(McpServersList): add a search bar * refactor: show different empty tips --- .../src/components/CollapsibleSearchBar.tsx | 27 ++++++---- .../Popups/SelectModelPopup/searchbar.tsx | 2 +- src/renderer/src/i18n/locales/en-us.json | 9 +++- src/renderer/src/i18n/locales/ja-jp.json | 9 +++- src/renderer/src/i18n/locales/ru-ru.json | 9 +++- src/renderer/src/i18n/locales/zh-cn.json | 9 +++- src/renderer/src/i18n/locales/zh-tw.json | 9 +++- src/renderer/src/i18n/translate/el-gr.json | 9 +++- src/renderer/src/i18n/translate/es-es.json | 9 +++- src/renderer/src/i18n/translate/fr-fr.json | 9 +++- src/renderer/src/i18n/translate/pt-pt.json | 9 +++- .../settings/MCPSettings/McpServersList.tsx | 53 ++++++++++++++----- .../ProviderSettings/ModelList/ModelList.tsx | 6 ++- 13 files changed, 137 insertions(+), 32 deletions(-) diff --git a/src/renderer/src/components/CollapsibleSearchBar.tsx b/src/renderer/src/components/CollapsibleSearchBar.tsx index 3ce3b436be..04b838e37a 100644 --- a/src/renderer/src/components/CollapsibleSearchBar.tsx +++ b/src/renderer/src/components/CollapsibleSearchBar.tsx @@ -1,21 +1,30 @@ +import i18n from '@renderer/i18n' import { Input, InputRef, Tooltip } from 'antd' import { Search } from 'lucide-react' import { motion } from 'motion/react' import React, { memo, useCallback, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' interface CollapsibleSearchBarProps { onSearch: (text: string) => void + placeholder?: string + tooltip?: string icon?: React.ReactNode maxWidth?: string | number + style?: React.CSSProperties } /** * A collapsible search bar for list headers * Renders as an icon initially, expands to full search input when clicked */ -const CollapsibleSearchBar: React.FC = ({ onSearch, icon, maxWidth }) => { - const { t } = useTranslation() +const CollapsibleSearchBar = ({ + onSearch, + placeholder = i18n.t('common.search'), + tooltip = i18n.t('common.search'), + icon = , + maxWidth = '100%', + style +}: CollapsibleSearchBarProps) => { const [searchVisible, setSearchVisible] = useState(false) const [searchText, setSearchText] = useState('') const inputRef = useRef(null) @@ -46,16 +55,16 @@ const CollapsibleSearchBar: React.FC = ({ onSearch, i initial="collapsed" animate={searchVisible ? 'expanded' : 'collapsed'} variants={{ - expanded: { maxWidth: maxWidth || '100%', opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } }, + expanded: { maxWidth: maxWidth, opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } }, collapsed: { maxWidth: 0, opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } } }} style={{ overflow: 'hidden', flex: 1 }}> } + suffix={icon} value={searchText} autoFocus allowClear @@ -71,7 +80,7 @@ const CollapsibleSearchBar: React.FC = ({ onSearch, i if (!searchText) setSearchVisible(false) }} onClear={handleClear} - style={{ width: '100%' }} + style={{ width: '100%', ...style }} /> = ({ onSearch, i }} style={{ cursor: 'pointer', display: 'flex' }} onClick={() => setSearchVisible(true)}> - - {icon || } + + {icon} diff --git a/src/renderer/src/components/Popups/SelectModelPopup/searchbar.tsx b/src/renderer/src/components/Popups/SelectModelPopup/searchbar.tsx index 1b4d1dbb02..ab641cf14b 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup/searchbar.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup/searchbar.tsx @@ -41,7 +41,7 @@ const SelectModelSearchBar: React.FC = ({ onSearch }) } ref={inputRef} - placeholder={t('models.search')} + placeholder={t('models.search.placeholder')} value={searchText} onChange={(e) => handleTextChange(e.target.value)} onClear={handleClear} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 428b7693e0..48bb9664a1 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "Currently, the reranker model does not support this provider ({{provider}})", "rerank_model_support_provider": "Currently, the reranker model only supports some providers ({{provider}})", "rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add.", - "search": "Search models...", + "search": { + "placeholder": "Search models...", + "tooltip": "Search models" + }, "stream_output": "Stream output", "type": { "embedding": "Embedding", @@ -2912,6 +2915,10 @@ "text": "Text", "uri": "URI" }, + "search": { + "placeholder": "Search MCP servers...", + "tooltip": "Search MCP servers" + }, "searchNpx": "Search MCP", "serverPlural": "servers", "serverSingular": "server", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5db622e895..d731da1934 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "現在、並べ替えモデルはこのプロバイダー ({{provider}}) をサポートしていません。", "rerank_model_support_provider": "現在の再順序付けモデルは、{{provider}} のみサポートしています", "rerank_model_tooltip": "設定->モデルサービスに移動し、管理ボタンをクリックして追加します。", - "search": "モデルを検索...", + "search": { + "placeholder": "モデルを検索...", + "tooltip": "モデルを検索" + }, "stream_output": "ストリーム出力", "type": { "embedding": "埋め込み", @@ -2912,6 +2915,10 @@ "text": "テキスト", "uri": "URI" }, + "search": { + "placeholder": "MCP サーバーを検索...", + "tooltip": "MCP サーバーを検索" + }, "searchNpx": "MCP を検索", "serverPlural": "サーバー", "serverSingular": "サーバー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 2a04e0ccef..21251f332d 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "В настоящее время модель переупорядочивания не поддерживает этого провайдера ({{provider}})", "rerank_model_support_provider": "Текущая модель переупорядочивания поддерживается только некоторыми поставщиками ({{provider}})", "rerank_model_tooltip": "В настройках -> Служба модели нажмите кнопку \"Управление\", чтобы добавить.", - "search": "Поиск моделей...", + "search": { + "placeholder": "Поиск моделей...", + "tooltip": "Поиск моделей" + }, "stream_output": "Потоковый вывод", "type": { "embedding": "Встраиваемые", @@ -2912,6 +2915,10 @@ "text": "Текст", "uri": "URI" }, + "search": { + "placeholder": "Найти MCP серверы...", + "tooltip": "Найти MCP серверы" + }, "searchNpx": "Найти MCP", "serverPlural": "серверы", "serverSingular": "сервер", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9db6cf963b..4307fb1208 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "目前重排序模型不支持该服务商 ({{provider}})", "rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})", "rerank_model_tooltip": "在设置 -> 模型服务中点击管理按钮添加", - "search": "搜索模型...", + "search": { + "placeholder": "搜索模型...", + "tooltip": "搜索模型" + }, "stream_output": "流式输出", "type": { "embedding": "嵌入", @@ -2912,6 +2915,10 @@ "text": "文本", "uri": "URI" }, + "search": { + "placeholder": "搜索 MCP 服务器...", + "tooltip": "搜索 MCP 服务器" + }, "searchNpx": "搜索 MCP", "serverPlural": "服务器", "serverSingular": "服务器", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 04ce4ed230..e9a41e2813 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "目前,重新排序模型不支援此提供者({{provider}})", "rerank_model_support_provider": "目前重排序模型僅支持部分服務商 ({{provider}})", "rerank_model_tooltip": "在設定 -> 模型服務中點擊管理按鈕添加", - "search": "搜尋模型...", + "search": { + "placeholder": "搜尋模型...", + "tooltip": "搜尋模型" + }, "stream_output": "串流輸出", "type": { "embedding": "嵌入", @@ -2912,6 +2915,10 @@ "text": "文字", "uri": "URI" }, + "search": { + "placeholder": "搜索 MCP 伺服器...", + "tooltip": "搜索 MCP 伺服器" + }, "searchNpx": "搜索 MCP", "serverPlural": "伺服器", "serverSingular": "伺服器", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index e095cbf0da..b0c96f2aa7 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "Ο επαναξιολογητικός μοντέλος δεν υποστηρίζει αυτόν τον πάροχο ({{provider}})", "rerank_model_support_provider": "Σημειώστε ότι το μοντέλο αναδιάταξης υποστηρίζεται από μερικούς παρόχους ({{provider}})", "rerank_model_tooltip": "Κάντε κλικ στο κουμπί Διαχείριση στο παράθυρο Ρυθμίσεις -> Υπηρεσία Μοντέλων", - "search": "Αναζήτηση μοντέλου...", + "search": { + "placeholder": "Αναζήτηση μοντέλου...", + "tooltip": "Αναζήτηση μοντέλου" + }, "stream_output": "Διαρκής Εξόδος", "type": { "embedding": "ενσωμάτωση", @@ -2912,6 +2915,10 @@ "text": "Κείμενο", "uri": "URI" }, + "search": { + "placeholder": "Αναζήτηση MCP διακομιστών...", + "tooltip": "Αναζήτηση MCP διακομιστών" + }, "searchNpx": "Αναζήτηση MCP", "serverPlural": "Διακομιστές", "serverSingular": "Διακομιστής", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 2ee02a11ec..efd6643820 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "Actualmente, el modelo de reordenamiento no admite este proveedor ({{provider}})", "rerank_model_support_provider": "Actualmente, el modelo de reordenamiento solo es compatible con algunos proveedores ({{provider}})", "rerank_model_tooltip": "Haga clic en el botón Administrar en Configuración->Servicio de modelos para agregar", - "search": "Buscar modelo...", + "search": { + "placeholder": "Buscar modelo...", + "tooltip": "Buscar modelo" + }, "stream_output": "Salida en flujo", "type": { "embedding": "Incrustación", @@ -2912,6 +2915,10 @@ "text": "Texto", "uri": "URI" }, + "search": { + "placeholder": "Buscar servidores MCP...", + "tooltip": "Buscar servidores MCP" + }, "searchNpx": "Buscar MCP", "serverPlural": "Servidores", "serverSingular": "Servidor", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 20364b838f..37008d5c4f 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "Le modèle de réordonnancement ne prend pas en charge ce fournisseur ({{provider}}) pour le moment", "rerank_model_support_provider": "Le modèle de réordonnancement ne prend actuellement en charge que certains fournisseurs ({{provider}})", "rerank_model_tooltip": "Cliquez sur le bouton Gérer dans Paramètres -> Services de modèles pour ajouter", - "search": "Rechercher un modèle...", + "search": { + "placeholder": "Rechercher un modèle...", + "tooltip": "Rechercher un modèle" + }, "stream_output": "Sortie en flux", "type": { "embedding": "Incorporation", @@ -2912,6 +2915,10 @@ "text": "Текст", "uri": "URI" }, + "search": { + "placeholder": "Rechercher des serveurs MCP...", + "tooltip": "Rechercher des serveurs MCP" + }, "searchNpx": "Поиск MCP", "serverPlural": "Serveurs", "serverSingular": "Serveur", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 5d54ecc50a..f245c4a463 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1544,7 +1544,10 @@ "rerank_model_not_support_provider": "Atualmente o modelo de reclassificação não suporta este provedor ({{provider}})", "rerank_model_support_provider": "O modelo de reclassificação atualmente suporta apenas alguns provedores ({{provider}})", "rerank_model_tooltip": "Clique no botão Gerenciar em Configurações -> Serviço de modelos para adicionar", - "search": "Procurar modelo...", + "search": { + "placeholder": "Procurar modelo...", + "tooltip": "Procurar modelo" + }, "stream_output": "Saída em fluxo", "type": { "embedding": "inserção", @@ -2912,6 +2915,10 @@ "text": "Texto", "uri": "URI" }, + "search": { + "placeholder": "Buscar servidores MCP...", + "tooltip": "Buscar servidores MCP" + }, "searchNpx": "Buscar MCP", "serverPlural": "Servidores", "serverSingular": "Servidor", diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index 57e6cc80c6..f9f98476d9 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -1,13 +1,15 @@ import { nanoid } from '@reduxjs/toolkit' -import { Sortable } from '@renderer/components/dnd' +import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' +import { Sortable, useDndReorder } from '@renderer/components/dnd' import { EditIcon, RefreshIcon } from '@renderer/components/Icons' import Scrollbar from '@renderer/components/Scrollbar' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { MCPServer } from '@renderer/types' import { formatMcpError } from '@renderer/utils/error' +import { matchKeywordsInString } from '@renderer/utils/match' import { Button, Dropdown, Empty } from 'antd' import { Plus } from 'lucide-react' -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { FC, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' import styled from 'styled-components' @@ -30,6 +32,32 @@ const McpServersList: FC = () => { const [loadingServerIds, setLoadingServerIds] = useState>(new Set()) const [serverVersions, setServerVersions] = useState>({}) + const [searchText, _setSearchText] = useState('') + + const setSearchText = useCallback((text: string) => { + startTransition(() => { + _setSearchText(text) + }) + }, []) + + const filteredMcpServers = useMemo(() => { + if (!searchText.trim()) return mcpServers + + const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) + + return mcpServers.filter((server) => { + const searchTarget = `${server.name} ${server.description} ${server.tags?.join(' ')}` + return matchKeywordsInString(keywords, searchTarget) + }) + }, [mcpServers, searchText]) + + const { onSortEnd } = useDndReorder({ + originalList: mcpServers, + filteredList: filteredMcpServers, + onUpdate: updateMcpServers, + idKey: 'id' + }) + const scrollRef = useRef(null) // 简单的滚动位置记忆 @@ -190,8 +218,14 @@ const McpServersList: FC = () => { return ( - + {t('settings.mcp.newServer')} + @@ -213,14 +247,9 @@ const McpServersList: FC = () => { { - const newList = [...mcpServers] - const [removed] = newList.splice(oldIndex, 1) - newList.splice(newIndex, 0, removed) - updateMcpServers(newList) - }} + onSortEnd={onSortEnd} layout="grid" useDragOverlay showGhost @@ -236,10 +265,10 @@ const McpServersList: FC = () => { /> )} /> - {mcpServers.length === 0 && ( + {(mcpServers.length === 0 || filteredMcpServers.length === 0) && ( )} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx index 14832e5aee..58468f09bb 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx @@ -106,7 +106,11 @@ const ModelList: React.FC = ({ providerId }) => { {modelCount} )} - +