diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx index 031c7838..bb3eaa3d 100644 --- a/frontend/src/app/pages/Tools/Tools.tsx +++ b/frontend/src/app/pages/Tools/Tools.tsx @@ -21,7 +21,7 @@ import { import { Skeleton } from '@/app/components/feedback/Loading'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { Integration, INTEGRATIONS } from './integrations'; +import { INTEGRATIONS } from './integrations'; import { CATEGORY_ORDER } from './toolsHelpers'; import ToolSection from './cards/ToolSection'; import BrowserPermissionCard from './cards/BrowserPermissionCard'; @@ -30,9 +30,14 @@ import RegistryBrowserDialog from './dialogs/RegistryBrowserDialog'; import ToolsAddMenu from './dialogs/ToolsAddMenu'; import ToolDialogs from './dialogs/ToolDialogs'; import CustomToolCard from './cards/CustomToolCard'; +import PopularConnectorsRow from './cards/PopularConnectorsRow'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import SearchIcon from '@mui/icons-material/Search'; import IntegrationGalleryCard from './cards/IntegrationGalleryCard'; import { useToolsActions } from './hooks/useToolsActions'; import { useBuiltinSections } from './hooks/useBuiltinSections'; +import { useConnectorFilters } from './hooks/useConnectorFilters'; import { useCuratedRegistry } from './hooks/useCuratedRegistry'; interface ToolsProps { @@ -69,13 +74,7 @@ const Tools: React.FC = ({ onBrowseConnectors, expandToolId }) => { const [browserSectionOpen, setBrowserSectionOpen] = useState(false); const [browserCollapsed, setBrowserCollapsed] = useState>({ browser_delegation: true, browser_action: true }); // claude.ai's Connectors page tabs: All / Connected / Not connected. - const [connFilter, setConnFilter] = useState<'all' | 'connected' | 'not-connected'>('all'); - const visibleTools = useMemo(() => { - if (connFilter === 'connected') return tools.filter((t) => t.enabled !== false); - if (connFilter === 'not-connected') return tools.filter((t) => t.enabled === false); - return tools; - }, [tools, connFilter]); - const visibleGallery = useMemo(() => (connFilter === 'connected' ? [] : uninstalledIntegrations), [uninstalledIntegrations, connFilter]); + const { connFilter, setConnFilter, searchOpen, setSearchOpen, searchQ, setSearchQ, q, visibleTools, visibleGallery, popularUninstalled } = useConnectorFilters(tools, uninstalledIntegrations); useEffect(() => { dispatch(fetchTools()); @@ -119,6 +118,22 @@ const Tools: React.FC = ({ onBrowseConnectors, expandToolId }) => { ))} + + {searchOpen ? ( + setSearchQ(e.target.value)} + onBlur={() => { if (!searchQ.trim()) setSearchOpen(false); }} + sx={{ width: 240, '& .MuiOutlinedInput-root': { fontSize: '0.875rem', borderRadius: 999, '& input': { py: 0.6 } } }} + /> + ) : ( + setSearchOpen(true)} sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}> + + + )} = ({ onBrowseConnectors, expandToolId }) => { onOpenRegistry={a.openRegistryBrowser} onSnackbar={(message, severity) => a.setSnackbar({ open: true, message, severity: severity === 'error' ? 'error' : undefined })} /> + {loading ? ( @@ -135,13 +151,17 @@ const Tools: React.FC = ({ onBrowseConnectors, expandToolId }) => { ))} ) : ( + <> + {connFilter === 'all' && !q && ( + + )} Connector Type Status - {connFilter === 'all' && ( + {connFilter === 'all' && !q && ( <> {coreTools.length > 0 && ( } count={coreTools.length} open={coreSectionOpen} onToggle={() => setCoreSectionOpen((v) => !v)} grouped={groupedCore} collapsedCategories={collapsedCategories} toggleCategory={toggleCategory} expandedBuiltin={expandedBuiltin} toggleBuiltinExpand={toggleBuiltinExpand} builtinPermissions={builtinPermissions} onPermissionChange={a.handleBuiltinPermissionChange} onCategoryPermissionChange={a.handleBuiltinCategoryPermissionChange} enabled={coreSectionEnabled} onEnabledChange={(v) => a.handleSectionEnabledChange(coreTools, v)} /> @@ -212,6 +232,7 @@ const Tools: React.FC = ({ onBrowseConnectors, expandToolId }) => { )} + )} diff --git a/frontend/src/app/pages/Tools/cards/PopularConnectorsRow.tsx b/frontend/src/app/pages/Tools/cards/PopularConnectorsRow.tsx new file mode 100644 index 00000000..b486bf34 --- /dev/null +++ b/frontend/src/app/pages/Tools/cards/PopularConnectorsRow.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; +import { Integration } from '../integrations'; + +interface Props { + integrations: Integration[]; + loading: Record; + onConnect: (ig: Integration) => void; +} + +// claude.ai's POPULAR strip on the Connectors settings page: compact cards with a Connect button. +const PopularConnectorsRow: React.FC = ({ integrations, loading, onConnect }) => { + const c = useClaudeTokens(); + if (integrations.length === 0) return null; + return ( + + + Popular + + + {integrations.map((ig) => ( + + + {ig.icon} + + {ig.name} + {loading[ig.id] ? ( + + ) : ( + + )} + + ))} + + + ); +}; + +export default PopularConnectorsRow; diff --git a/frontend/src/app/pages/Tools/hooks/useConnectorFilters.ts b/frontend/src/app/pages/Tools/hooks/useConnectorFilters.ts new file mode 100644 index 00000000..b2c01be0 --- /dev/null +++ b/frontend/src/app/pages/Tools/hooks/useConnectorFilters.ts @@ -0,0 +1,34 @@ +import { useMemo, useState } from 'react'; +import { ToolDefinition } from '@/shared/state/toolsSlice'; +import { Integration } from '../integrations'; + +export type ConnFilter = 'all' | 'connected' | 'not-connected'; + +// The claude Connectors-page filter state: All/Connected/Not connected pills + the header search. +export function useConnectorFilters(tools: ToolDefinition[], uninstalledIntegrations: Integration[]) { + const [connFilter, setConnFilter] = useState('all'); + const [searchOpen, setSearchOpen] = useState(false); + const [searchQ, setSearchQ] = useState(''); + const q = searchQ.trim().toLowerCase(); + + const visibleTools = useMemo(() => { + let out = tools; + if (connFilter === 'connected') out = out.filter((t) => t.enabled !== false); + if (connFilter === 'not-connected') out = out.filter((t) => t.enabled === false); + if (q) out = out.filter((t) => (t.name || '').toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q)); + return out; + }, [tools, connFilter, q]); + + const visibleGallery = useMemo(() => { + if (connFilter === 'connected') return []; + if (!q) return uninstalledIntegrations; + return uninstalledIntegrations.filter((ig) => ig.name.toLowerCase().includes(q) || ig.description.toLowerCase().includes(q)); + }, [uninstalledIntegrations, connFilter, q]); + + const popularUninstalled = useMemo( + () => ['google-workspace', 'slack', 'notion'].map((id) => uninstalledIntegrations.find((ig) => ig.id === id)).filter((ig): ig is Integration => !!ig), + [uninstalledIntegrations], + ); + + return { connFilter, setConnFilter, searchOpen, setSearchOpen, searchQ, setSearchQ, q, visibleTools, visibleGallery, popularUninstalled }; +}