mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 02:07:45 +02:00
[eric] tools: header search + POPULAR connect strip close the claude connectors parity
This commit is contained in:
@@ -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<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
const [browserSectionOpen, setBrowserSectionOpen] = useState(false);
|
||||
const [browserCollapsed, setBrowserCollapsed] = useState<Record<string, boolean>>({ 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<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{searchOpen ? (
|
||||
<TextField
|
||||
autoFocus
|
||||
size="small"
|
||||
placeholder="Search connectors..."
|
||||
value={searchQ}
|
||||
onChange={(e) => setSearchQ(e.target.value)}
|
||||
onBlur={() => { if (!searchQ.trim()) setSearchOpen(false); }}
|
||||
sx={{ width: 240, '& .MuiOutlinedInput-root': { fontSize: '0.875rem', borderRadius: 999, '& input': { py: 0.6 } } }}
|
||||
/>
|
||||
) : (
|
||||
<IconButton size="small" onClick={() => setSearchOpen(true)} sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
|
||||
<SearchIcon sx={{ fontSize: 19 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
<ToolsAddMenu
|
||||
devMode={!!devMode}
|
||||
onBrowseConnectors={onBrowseConnectors}
|
||||
@@ -126,6 +141,7 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
onOpenRegistry={a.openRegistryBrowser}
|
||||
onSnackbar={(message, severity) => a.setSnackbar({ open: true, message, severity: severity === 'error' ? 'error' : undefined })}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading ? (
|
||||
@@ -135,13 +151,17 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{connFilter === 'all' && !q && (
|
||||
<PopularConnectorsRow integrations={popularUninstalled} loading={a.integrationLoading} onConnect={a.handleIntegrationToggle} />
|
||||
)}
|
||||
<Box sx={{ border: `1px solid ${c.border.subtle}`, borderRadius: '12px', overflow: 'hidden', bgcolor: c.bg.surface }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', gap: 2, px: 2, py: 1.1, borderBottom: `1px solid ${c.border.subtle}` }}>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem' }}>Connector</Typography>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem' }}>Type</Typography>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem', textAlign: 'right' }}>Status</Typography>
|
||||
</Box>
|
||||
{connFilter === 'all' && (
|
||||
{connFilter === 'all' && !q && (
|
||||
<>
|
||||
{coreTools.length > 0 && (
|
||||
<ToolSection label="Core Tools" icon={<LockIcon sx={{ fontSize: 14, color: c.text.tertiary }} />} 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<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
|
||||
@@ -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<string, boolean>;
|
||||
onConnect: (ig: Integration) => void;
|
||||
}
|
||||
|
||||
// claude.ai's POPULAR strip on the Connectors settings page: compact cards with a Connect button.
|
||||
const PopularConnectorsRow: React.FC<Props> = ({ integrations, loading, onConnect }) => {
|
||||
const c = useClaudeTokens();
|
||||
if (integrations.length === 0) return null;
|
||||
return (
|
||||
<Box sx={{ mb: 2.5 }}>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 600, letterSpacing: '0.06em', color: c.text.tertiary, textTransform: 'uppercase', mb: 1.25 }}>
|
||||
Popular
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 1.5 }}>
|
||||
{integrations.map((ig) => (
|
||||
<Box key={ig.id} sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.25, px: 1.75, py: 1.25,
|
||||
border: `1px solid ${c.border.subtle}`, borderRadius: '12px', bgcolor: c.bg.surface,
|
||||
transition: 'border-color 0.12s, box-shadow 0.12s',
|
||||
'&:hover': { borderColor: c.border.medium, boxShadow: c.shadow.sm },
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: 30, height: 30, borderRadius: '8px', flexShrink: 0,
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', color: ig.color,
|
||||
}}>
|
||||
{ig.icon}
|
||||
</Box>
|
||||
<Typography noWrap sx={{ fontSize: '0.9375rem', fontWeight: 600, color: c.text.primary, flex: 1 }}>{ig.name}</Typography>
|
||||
{loading[ig.id] ? (
|
||||
<CircularProgress size={16} sx={{ color: ig.color }} />
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => onConnect(ig)}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.8125rem', fontWeight: 600, px: 1.5, py: 0.3,
|
||||
color: c.text.primary, borderColor: c.border.medium, borderRadius: 999, minWidth: 0,
|
||||
'&:hover': { borderColor: c.border.strong, bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopularConnectorsRow;
|
||||
@@ -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<ConnFilter>('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 };
|
||||
}
|
||||
Reference in New Issue
Block a user