mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 05:51:26 +08:00
feat(McpServersList): add a search bar (#9520)
* feat(McpServersList): add a search bar * refactor: show different empty tips
This commit is contained in:
parent
ffa2eb57b1
commit
548916e6e1
@ -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<CollapsibleSearchBarProps> = ({ onSearch, icon, maxWidth }) => {
|
||||
const { t } = useTranslation()
|
||||
const CollapsibleSearchBar = ({
|
||||
onSearch,
|
||||
placeholder = i18n.t('common.search'),
|
||||
tooltip = i18n.t('common.search'),
|
||||
icon = <Search size={14} color="var(--color-icon)" />,
|
||||
maxWidth = '100%',
|
||||
style
|
||||
}: CollapsibleSearchBarProps) => {
|
||||
const [searchVisible, setSearchVisible] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
@ -46,16 +55,16 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ 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 }}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={t('models.search')}
|
||||
placeholder={placeholder}
|
||||
size="small"
|
||||
suffix={icon || <Search size={14} color="var(--color-icon)" />}
|
||||
suffix={icon}
|
||||
value={searchText}
|
||||
autoFocus
|
||||
allowClear
|
||||
@ -71,7 +80,7 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, i
|
||||
if (!searchText) setSearchVisible(false)
|
||||
}}
|
||||
onClear={handleClear}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', ...style }}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
@ -83,8 +92,8 @@ const CollapsibleSearchBar: React.FC<CollapsibleSearchBarProps> = ({ onSearch, i
|
||||
}}
|
||||
style={{ cursor: 'pointer', display: 'flex' }}
|
||||
onClick={() => setSearchVisible(true)}>
|
||||
<Tooltip title={t('models.search')} mouseLeaveDelay={0}>
|
||||
{icon || <Search size={14} color="var(--color-icon)" />}
|
||||
<Tooltip title={tooltip} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
|
||||
{icon}
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@ -41,7 +41,7 @@ const SelectModelSearchBar: React.FC<SelectModelSearchBarProps> = ({ onSearch })
|
||||
</SearchIcon>
|
||||
}
|
||||
ref={inputRef}
|
||||
placeholder={t('models.search')}
|
||||
placeholder={t('models.search.placeholder')}
|
||||
value={searchText}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
onClear={handleClear}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "サーバー",
|
||||
|
||||
@ -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": "сервер",
|
||||
|
||||
@ -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": "服务器",
|
||||
|
||||
@ -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": "伺服器",
|
||||
|
||||
@ -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": "Διακομιστής",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<Set<string>>(new Set())
|
||||
const [serverVersions, setServerVersions] = useState<Record<string, string | null>>({})
|
||||
|
||||
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<HTMLDivElement>(null)
|
||||
|
||||
// 简单的滚动位置记忆
|
||||
@ -190,8 +218,14 @@ const McpServersList: FC = () => {
|
||||
return (
|
||||
<Container ref={scrollRef}>
|
||||
<ListHeader>
|
||||
<SettingTitle style={{ gap: 3 }}>
|
||||
<SettingTitle style={{ gap: 6 }}>
|
||||
<span>{t('settings.mcp.newServer')}</span>
|
||||
<CollapsibleSearchBar
|
||||
onSearch={setSearchText}
|
||||
placeholder={t('settings.mcp.search.placeholder')}
|
||||
tooltip={t('settings.mcp.search.tooltip')}
|
||||
style={{ borderRadius: 20 }}
|
||||
/>
|
||||
</SettingTitle>
|
||||
<ButtonGroup>
|
||||
<InstallNpxUv mini />
|
||||
@ -213,14 +247,9 @@ const McpServersList: FC = () => {
|
||||
</ButtonGroup>
|
||||
</ListHeader>
|
||||
<Sortable
|
||||
items={mcpServers}
|
||||
items={filteredMcpServers}
|
||||
itemKey="id"
|
||||
onSortEnd={({ oldIndex, newIndex }) => {
|
||||
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) && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={t('settings.mcp.noServers')}
|
||||
description={mcpServers.length === 0 ? t('settings.mcp.noServers') : t('common.no_results')}
|
||||
style={{ marginTop: 20 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -106,7 +106,11 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
{modelCount}
|
||||
</CustomTag>
|
||||
)}
|
||||
<CollapsibleSearchBar onSearch={setSearchText} />
|
||||
<CollapsibleSearchBar
|
||||
onSearch={setSearchText}
|
||||
placeholder={t('models.search.placeholder')}
|
||||
tooltip={t('models.search.tooltip')}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Tooltip title={t('settings.models.check.button_caption')} mouseLeaveDelay={0}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user