mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 03:37:44 +02:00
[eric] tools: one claude connectors table (Connector/Type/Status), built-ins ride as rows, uninstalled get Connect
This commit is contained in:
@@ -3,13 +3,9 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
@@ -70,10 +66,8 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
const [expandedBuiltin, setExpandedBuiltin] = useState<string | null>(null);
|
||||
const [coreSectionOpen, setCoreSectionOpen] = useState(false);
|
||||
const [deferredSectionOpen, setDeferredSectionOpen] = useState(false);
|
||||
const [customSectionOpen, setCustomSectionOpen] = useState(true);
|
||||
const [browserSectionOpen, setBrowserSectionOpen] = useState(false);
|
||||
const [browserCollapsed, setBrowserCollapsed] = useState<Record<string, boolean>>({ browser_delegation: true, browser_action: true });
|
||||
const [builtinSectionOpen, setBuiltinSectionOpen] = useState(true);
|
||||
// claude.ai's Connectors page tabs: All / Connected / Not connected.
|
||||
const [connFilter, setConnFilter] = useState<'all' | 'connected' | 'not-connected'>('all');
|
||||
const visibleTools = useMemo(() => {
|
||||
@@ -105,8 +99,26 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
|
||||
return (
|
||||
<Box sx={{ px: 3, pt: 1, pb: 3, height: '100%', overflow: 'auto' }}>
|
||||
{/* The pane header already says "Tools"; a slim action row beats a second page title. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', mb: 1.5 }}>
|
||||
{/* claude.ai's Connectors page grammar: filter pills left, Add right, one table below. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
{([['all', 'All'], ['connected', 'Connected'], ['not-connected', 'Not connected']] as const).map(([value, label]) => (
|
||||
<Box
|
||||
key={value}
|
||||
role="button"
|
||||
onClick={() => setConnFilter(value)}
|
||||
sx={{
|
||||
px: 1.5, py: 0.4, borderRadius: 999, cursor: 'pointer', userSelect: 'none',
|
||||
fontSize: '0.8125rem', fontWeight: 600, lineHeight: 1.6,
|
||||
color: connFilter === value ? c.text.primary : c.text.tertiary,
|
||||
bgcolor: connFilter === value ? c.bg.secondary : 'transparent',
|
||||
'&:hover': { color: c.text.primary },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<ToolsAddMenu
|
||||
devMode={!!devMode}
|
||||
onBrowseConnectors={onBrowseConnectors}
|
||||
@@ -116,20 +128,21 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box
|
||||
onClick={() => setBuiltinSectionOpen((v) => !v)}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1, px: 0.5, cursor: 'pointer', userSelect: 'none', '&:hover .section-arrow': { color: c.text.secondary } }}
|
||||
>
|
||||
<Typography sx={{ color: c.text.muted, fontWeight: 700, fontSize: '0.6875rem', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
|
||||
Built-in
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.6875rem', fontWeight: 600 }}>{coreTools.length + deferredTools.length + browserTools.length}</Typography>
|
||||
{builtinSectionOpen ? <KeyboardArrowDownIcon className="section-arrow" sx={{ fontSize: 15, color: c.text.ghost, transition: 'color 0.15s' }} /> : <KeyboardArrowRightIcon className="section-arrow" sx={{ fontSize: 15, color: c.text.ghost, transition: 'color 0.15s' }} />}
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mt: 1 }}>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} variant="card" height={72} />
|
||||
))}
|
||||
</Box>
|
||||
<Collapse in={builtinSectionOpen} timeout={0} unmountOnExit>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pl: 1 }}>
|
||||
|
||||
) : (
|
||||
<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' && (
|
||||
<>
|
||||
{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)} />
|
||||
)}
|
||||
@@ -154,52 +167,8 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
onPermissionChange={a.handleBuiltinPermissionChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<AgentWorkflowsSection />
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box onClick={() => setCustomSectionOpen((v) => !v)} sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 1, px: 0.5, cursor: 'pointer', userSelect: 'none', '&:hover .section-arrow': { color: c.text.secondary } }}>
|
||||
<Typography sx={{ color: c.text.muted, fontWeight: 700, fontSize: '0.6875rem', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
|
||||
Connections
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.6875rem', fontWeight: 600 }}>{tools.length + uninstalledIntegrations.length}</Typography>
|
||||
{customSectionOpen ? <KeyboardArrowDownIcon className="section-arrow" sx={{ fontSize: 15, color: c.text.ghost, transition: 'color 0.15s' }} /> : <KeyboardArrowRightIcon className="section-arrow" sx={{ fontSize: 15, color: c.text.ghost, transition: 'color 0.15s' }} />}
|
||||
<Box sx={{ display: 'flex', gap: 0.5, ml: 1 }} onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
{([['all', 'All'], ['connected', 'Connected'], ['not-connected', 'Not connected']] as const).map(([value, label]) => (
|
||||
<Box
|
||||
key={value}
|
||||
role="button"
|
||||
onClick={() => setConnFilter(value)}
|
||||
sx={{
|
||||
px: 1.25, py: 0.3, borderRadius: 999, cursor: 'pointer', userSelect: 'none',
|
||||
fontSize: '0.75rem', fontWeight: 600, lineHeight: 1.6,
|
||||
color: connFilter === value ? c.text.primary : c.text.tertiary,
|
||||
bgcolor: connFilter === value ? c.bg.secondary : 'transparent',
|
||||
'&:hover': { color: c.text.primary },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Collapse in={customSectionOpen} timeout={0} unmountOnExit>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pl: 1, mt: 1 }}>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} variant="card" height={72} />
|
||||
))}
|
||||
</Box>
|
||||
) : (tools.length === 0 && uninstalledIntegrations.length === 0) ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', py: 6, color: c.text.ghost, gap: 1.5 }}>
|
||||
<BuildIcon sx={{ fontSize: 40, opacity: 0.3 }} />
|
||||
<Typography sx={{ fontSize: '0.875rem' }}>No custom tools defined yet. Create one to get started.</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', ml: 1, border: `1px solid ${c.border.subtle}`, borderRadius: '12px', overflow: 'hidden', bgcolor: c.bg.surface }}>
|
||||
</>
|
||||
)}
|
||||
{visibleTools.map((tool) => (
|
||||
<CustomToolCard
|
||||
key={tool.id}
|
||||
@@ -237,9 +206,16 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
|
||||
onToggle={a.handleIntegrationToggle}
|
||||
/>
|
||||
))}
|
||||
{connFilter !== 'all' && visibleTools.length === 0 && visibleGallery.length === 0 && (
|
||||
<Box sx={{ px: 2, py: 4, textAlign: 'center' }}>
|
||||
<Typography sx={{ fontSize: '0.875rem', color: c.text.ghost }}>Nothing {connFilter === 'connected' ? 'connected' : 'disconnected'} yet.</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Collapse>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<AgentWorkflowsSection />
|
||||
</Box>
|
||||
|
||||
<ToolDialogs
|
||||
|
||||
@@ -48,42 +48,43 @@ const BrowserPermissionCard: React.FC<BrowserPermissionCardProps> = ({
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<Card sx={{ bgcolor: c.bg.surface, border: `1px solid ${browserSectionOpen && browserSectionEnabled ? c.accent.primary : c.border.subtle}`, borderRadius: 2, boxShadow: c.shadow.sm, '&:hover': { borderColor: c.accent.primary, boxShadow: '0 0 0 1px rgba(174,86,48,0.12)' }, transition: 'border-color 0.2s, box-shadow 0.2s' }}>
|
||||
<CardContent sx={{ py: 1.5, px: 2, '&:last-child': { pb: 1.5 } }}>
|
||||
<Card sx={{ bgcolor: browserSectionOpen && browserSectionEnabled ? c.bg.elevated : 'transparent', border: 'none', borderRadius: 0, boxShadow: 'none', borderBottom: `1px solid ${c.border.subtle}`, '&:last-of-type': { borderBottom: 'none' }, '&:hover': { bgcolor: c.bg.elevated }, transition: 'background-color 0.12s' }}>
|
||||
<CardContent sx={{ py: 1.4, px: 2, '&:last-child': { pb: 1.4 } }}>
|
||||
<Box
|
||||
onClick={() => browserSectionEnabled && onToggleOpen()}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 2, cursor: browserSectionEnabled ? 'pointer' : 'default' }}
|
||||
sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', alignItems: 'center', gap: 2, cursor: browserSectionEnabled ? 'pointer' : 'default' }}
|
||||
>
|
||||
<Box sx={{
|
||||
width: 36, height: 36, borderRadius: 2, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
bgcolor: c.bg.secondary, color: c.text.tertiary, flexShrink: 0,
|
||||
opacity: browserSectionEnabled ? 1 : 0.4, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
<PublicIcon sx={{ fontSize: 18 }} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0, opacity: browserSectionEnabled ? 1 : 0.4, transition: 'opacity 0.2s' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem' }}>Browser</Typography>
|
||||
<Chip label={`${browserTools.length} tools`} size="small" sx={{ bgcolor: c.bg.secondary, color: c.text.muted, fontSize: '0.6875rem', height: 20, '& .MuiChip-label': { px: 0.6 } }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, minWidth: 0 }}>
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, color: c.text.tertiary, flexShrink: 0,
|
||||
opacity: browserSectionEnabled ? 1 : 0.55, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
<PublicIcon sx={{ fontSize: 18 }} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ color: browserSectionEnabled ? c.text.primary : c.text.tertiary, fontWeight: 600, fontSize: '0.9375rem', mb: 0.25 }}>Browser</Typography>
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>Browser automation delegation and individual browser tools</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>{browserTools.length} tools</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>Browser automation delegation and individual browser tools</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={browserSectionEnabled}
|
||||
onChange={(_, checked) => handleSectionEnabledChange(browserTools, checked)}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{browserSectionEnabled && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, flexShrink: 0 }}>
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.8125rem' }}>Built-in</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5 }}>
|
||||
<Box onClick={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={browserSectionEnabled}
|
||||
onChange={(_, checked) => handleSectionEnabledChange(browserTools, checked)}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{browserSectionEnabled && (
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.ghost, transition: 'transform 0.2s', transform: browserSectionOpen ? 'rotate(180deg)' : 'rotate(0deg)' }} />
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<Collapse in={browserSectionOpen && browserSectionEnabled} timeout={0} unmountOnExit>
|
||||
|
||||
@@ -104,92 +104,99 @@ const CustomToolCard: React.FC<CustomToolCardProps> = ({
|
||||
>
|
||||
<CardContent sx={{ py: 1.4, px: 2, '&:last-child': { pb: 1.4 } }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 2, cursor: isDisabled ? 'default' : 'pointer' }}
|
||||
sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', alignItems: 'center', gap: 2, cursor: isDisabled ? 'default' : 'pointer' }}
|
||||
data-onboarding={isYoutube ? 'actions-youtube-chevron' : isReddit ? 'actions-reddit-chevron' : undefined}
|
||||
onClick={() => !isDisabled && onToggleExpand(tool.id, isExpanded)}
|
||||
>
|
||||
{ig && (
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, fontSize: '1.125rem', fontWeight: 700, color: ig.color, flexShrink: 0,
|
||||
opacity: isDisabled ? 0.55 : 1, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
{ig.icon}
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
|
||||
<Typography sx={{ color: isDisabled ? c.text.tertiary : c.text.primary, fontWeight: 600, fontSize: '0.9375rem' }}>{tool.name}</Typography>
|
||||
{tool.command && <Chip icon={<TerminalIcon sx={{ fontSize: 12 }} />} label={`/${tool.command}`} size="small" sx={{ bgcolor: 'rgba(174,86,48,0.12)', color: c.accent.hover, fontSize: '0.6875rem', height: 20 }} />}
|
||||
{tool.auth_status === 'connected' && (
|
||||
<Typography sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.4, color: c.status.success, fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 13 }} /> Connected{!ig && tool.connected_account_email ? ` · ${tool.connected_account_email}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
{tool.auth_status === 'configured' && !ig?.credentialFields && (
|
||||
<Typography sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.4, color: c.status.warning, fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
<SettingsIcon sx={{ fontSize: 13 }} /> Configured
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{tool.description && (
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>{tool.description}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, minWidth: 0 }}>
|
||||
{ig && (
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, fontSize: '1.125rem', fontWeight: 700, color: ig.color, flexShrink: 0,
|
||||
opacity: isDisabled ? 0.55 : 1, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
{ig.icon}
|
||||
</Box>
|
||||
)}
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>
|
||||
{[isMcp ? (isStdio ? 'MCP · stdio' : 'MCP') : null, totalToolCount > 0 ? `${totalToolCount} tools` : null].filter(Boolean).join(' · ')}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
|
||||
<Typography sx={{ color: isDisabled ? c.text.tertiary : c.text.primary, fontWeight: 600, fontSize: '0.9375rem' }}>{tool.name}</Typography>
|
||||
{tool.command && <Chip icon={<TerminalIcon sx={{ fontSize: 12 }} />} label={`/${tool.command}`} size="small" sx={{ bgcolor: 'rgba(174,86,48,0.12)', color: c.accent.hover, fontSize: '0.6875rem', height: 20 }} />}
|
||||
</Box>
|
||||
{tool.description && (
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>{tool.description}</Typography>
|
||||
)}
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>
|
||||
{totalToolCount > 0 ? `${totalToolCount} tools` : ''}
|
||||
{ig && (
|
||||
<Box component="a" href={ig.website} target="_blank" rel="noreferrer" onClick={(e: React.MouseEvent) => e.stopPropagation()} sx={{ color: c.text.ghost, ml: totalToolCount > 0 ? 1 : 0, textDecoration: 'none', '&:hover': { color: c.text.secondary, textDecoration: 'underline' } }}>
|
||||
docs<OpenInNewIcon sx={{ fontSize: 10, ml: 0.25, verticalAlign: 'middle' }} />
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.8125rem' }}>{isMcp ? (isStdio ? 'MCP · stdio' : 'MCP') : 'Custom'}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 0.25 }}>
|
||||
{tool.auth_status === 'connected' && (
|
||||
<Typography sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.4, color: c.status.success, fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 13 }} /> Connected
|
||||
</Typography>
|
||||
)}
|
||||
{tool.auth_status === 'configured' && !ig?.credentialFields && (
|
||||
<Typography sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.4, color: c.status.warning, fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
<SettingsIcon sx={{ fontSize: 13 }} /> Configured
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
<CustomToolConnect
|
||||
tool={tool}
|
||||
ig={ig}
|
||||
isDisabled={isDisabled}
|
||||
onOAuthConnect={handleOAuthConnect}
|
||||
onDeviceCodeConnect={handleDeviceCodeConnect}
|
||||
onOpenCredentialsDialog={openCredentialsDialog}
|
||||
onM365Disconnect={handleM365Disconnect}
|
||||
onDisconnectIntegration={handleDisconnectIntegration}
|
||||
/>
|
||||
{ig && (
|
||||
<Box component="a" href={ig.website} target="_blank" rel="noreferrer" onClick={(e: React.MouseEvent) => e.stopPropagation()} sx={{ color: c.text.ghost, ml: 1, textDecoration: 'none', '&:hover': { color: c.text.secondary, textDecoration: 'underline' } }}>
|
||||
docs<OpenInNewIcon sx={{ fontSize: 10, ml: 0.25, verticalAlign: 'middle' }} />
|
||||
<Box
|
||||
data-onboarding={
|
||||
isYoutube
|
||||
? 'actions-youtube-toggle'
|
||||
: isReddit
|
||||
? 'actions-reddit-toggle'
|
||||
: undefined
|
||||
}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{!!integrationLoading[ig.id] && <CircularProgress size={16} sx={{ color: ig.color }} />}
|
||||
<Switch
|
||||
size="small"
|
||||
checked={tool.enabled !== false}
|
||||
onChange={() => handleIntegrationToggle(ig)}
|
||||
disabled={!!integrationLoading[ig.id]}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: ig.color },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: ig.color },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomToolConnect
|
||||
tool={tool}
|
||||
ig={ig}
|
||||
isDisabled={isDisabled}
|
||||
onOAuthConnect={handleOAuthConnect}
|
||||
onDeviceCodeConnect={handleDeviceCodeConnect}
|
||||
onOpenCredentialsDialog={openCredentialsDialog}
|
||||
onM365Disconnect={handleM365Disconnect}
|
||||
onDisconnectIntegration={handleDisconnectIntegration}
|
||||
/>
|
||||
{ig && (
|
||||
<Box
|
||||
data-onboarding={
|
||||
isYoutube
|
||||
? 'actions-youtube-toggle'
|
||||
: isReddit
|
||||
? 'actions-reddit-toggle'
|
||||
: undefined
|
||||
}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{!!integrationLoading[ig.id] && <CircularProgress size={16} sx={{ color: ig.color }} />}
|
||||
<Switch
|
||||
size="small"
|
||||
checked={tool.enabled !== false}
|
||||
onChange={() => handleIntegrationToggle(ig)}
|
||||
disabled={!!integrationLoading[ig.id]}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: ig.color },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: ig.color },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{!isDisabled && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, flexShrink: 0 }}>
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.ghost, transition: 'transform 0.2s', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)' }} />
|
||||
{!ig && (
|
||||
{!isDisabled && (
|
||||
<>
|
||||
<Tooltip title="Edit" placement="left"><IconButton size="small" onClick={(e) => { e.stopPropagation(); openEdit(tool); }} sx={{ color: c.text.ghost, '&:hover': { color: c.accent.primary } }}><EditIcon sx={{ fontSize: 16 }} /></IconButton></Tooltip>
|
||||
<Tooltip title="Delete" placement="left"><IconButton size="small" onClick={(e) => { e.stopPropagation(); handleDelete(tool.id); }} sx={{ color: c.text.ghost, '&:hover': { color: c.status.error } }}><DeleteIcon sx={{ fontSize: 16 }} /></IconButton></Tooltip>
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.ghost, transition: 'transform 0.2s', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)' }} />
|
||||
{!ig && (
|
||||
<>
|
||||
<Tooltip title="Edit" placement="left"><IconButton size="small" onClick={(e) => { e.stopPropagation(); openEdit(tool); }} sx={{ color: c.text.ghost, '&:hover': { color: c.accent.primary } }}><EditIcon sx={{ fontSize: 16 }} /></IconButton></Tooltip>
|
||||
<Tooltip title="Delete" placement="left"><IconButton size="small" onClick={(e) => { e.stopPropagation(); handleDelete(tool.id); }} sx={{ color: c.text.ghost, '&:hover': { color: c.status.error } }}><DeleteIcon sx={{ fontSize: 16 }} /></IconButton></Tooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Typography from '@mui/material/Typography';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Button from '@mui/material/Button';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { Integration } from '../integrations';
|
||||
@@ -23,23 +23,25 @@ const IntegrationGalleryCard: React.FC<IntegrationGalleryCardProps> = ({ integra
|
||||
sx={{ bgcolor: 'transparent', border: 'none', borderRadius: 0, boxShadow: 'none', borderBottom: `1px solid ${c.border.subtle}`, '&:last-of-type': { borderBottom: 'none' }, '&:hover': { bgcolor: c.bg.elevated }, transition: 'background-color 0.12s' }}
|
||||
>
|
||||
<CardContent sx={{ py: 1.4, px: 2, '&:last-child': { pb: 1.4 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, fontSize: '1.125rem', fontWeight: 700, color: c.text.ghost,
|
||||
}}>
|
||||
{ig.icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.9375rem', mb: 0.25 }}>{ig.name}</Typography>
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>{ig.description}</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>
|
||||
Not installed
|
||||
<Box component="a" href={ig.website} target="_blank" rel="noreferrer" sx={{ color: c.text.ghost, ml: 1, textDecoration: 'none', '&:hover': { color: c.text.secondary, textDecoration: 'underline' } }}>
|
||||
docs<OpenInNewIcon sx={{ fontSize: 10, ml: 0.25, verticalAlign: 'middle' }} />
|
||||
</Box>
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, minWidth: 0 }}>
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, fontSize: '1.125rem', fontWeight: 700, color: c.text.ghost,
|
||||
}}>
|
||||
{ig.icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.9375rem', mb: 0.25 }}>{ig.name}</Typography>
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>{ig.description}</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>
|
||||
<Box component="a" href={ig.website} target="_blank" rel="noreferrer" sx={{ color: c.text.ghost, textDecoration: 'none', '&:hover': { color: c.text.secondary, textDecoration: 'underline' } }}>
|
||||
docs<OpenInNewIcon sx={{ fontSize: 10, ml: 0.25, verticalAlign: 'middle' }} />
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.8125rem' }}>MCP</Typography>
|
||||
<Box
|
||||
data-onboarding={
|
||||
ig.id === 'youtube'
|
||||
@@ -48,19 +50,22 @@ const IntegrationGalleryCard: React.FC<IntegrationGalleryCardProps> = ({ integra
|
||||
? 'actions-reddit-toggle'
|
||||
: undefined
|
||||
}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}
|
||||
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.75 }}
|
||||
>
|
||||
{isLoading && <CircularProgress size={16} sx={{ color: ig.color }} />}
|
||||
<Switch
|
||||
<Button
|
||||
size="small"
|
||||
checked={false}
|
||||
onChange={() => handleIntegrationToggle(ig)}
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
onClick={() => handleIntegrationToggle(ig)}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: ig.color },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: ig.color },
|
||||
textTransform: 'none', fontSize: '0.8125rem', fontWeight: 600, px: 1.75, py: 0.4,
|
||||
color: c.text.primary, borderColor: c.border.medium, borderRadius: 999,
|
||||
'&:hover': { borderColor: c.border.strong, bgcolor: c.bg.elevated },
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
@@ -94,45 +94,43 @@ const ToolSection: React.FC<ToolSectionProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{ bgcolor: c.bg.surface, border: `1px solid ${open && enabled ? c.accent.primary : c.border.subtle}`, borderRadius: 2, boxShadow: c.shadow.sm, '&:hover': { borderColor: c.accent.primary, boxShadow: '0 0 0 1px rgba(174,86,48,0.12)' }, transition: 'border-color 0.2s, box-shadow 0.2s' }}>
|
||||
<CardContent sx={{ py: 1.1, px: 1.75, '&:last-child': { pb: 1.1 } }}>
|
||||
<Card sx={{ bgcolor: open && enabled ? c.bg.elevated : 'transparent', border: 'none', borderRadius: 0, boxShadow: 'none', borderBottom: `1px solid ${c.border.subtle}`, '&:last-of-type': { borderBottom: 'none' }, '&:hover': { bgcolor: c.bg.elevated }, transition: 'background-color 0.12s' }}>
|
||||
<CardContent sx={{ py: 1.4, px: 2, '&:last-child': { pb: 1.4 } }}>
|
||||
<Box
|
||||
onClick={() => enabled && onToggle()}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 2, cursor: enabled ? 'pointer' : 'default' }}
|
||||
sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', alignItems: 'center', gap: 2, cursor: enabled ? 'pointer' : 'default' }}
|
||||
>
|
||||
<Box sx={{
|
||||
width: 30, height: 30, borderRadius: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
bgcolor: c.bg.secondary, color: c.text.tertiary, flexShrink: 0,
|
||||
opacity: enabled ? 1 : 0.4, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0, opacity: enabled ? 1 : 0.4, transition: 'opacity 0.2s' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '0.9375rem' }}>{label}</Typography>
|
||||
<Chip label={`${count} tools`} size="small" sx={{ bgcolor: c.bg.secondary, color: c.text.muted, fontSize: '0.6875rem', height: 20, '& .MuiChip-label': { px: 0.6 } }} />
|
||||
{deferred && (
|
||||
<Chip label="on-demand" size="small" sx={{ bgcolor: c.status.warningBg, color: c.status.warning, fontSize: '0.625rem', height: 18, '& .MuiChip-label': { px: 0.6 } }} />
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, minWidth: 0 }}>
|
||||
<Box sx={{
|
||||
width: 34, height: 34, borderRadius: '9px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface, color: c.text.tertiary, flexShrink: 0,
|
||||
opacity: enabled ? 1 : 0.55, transition: 'opacity 0.2s',
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ color: enabled ? c.text.primary : c.text.tertiary, fontWeight: 600, fontSize: '0.9375rem', mb: 0.25 }}>{label}</Typography>
|
||||
<Typography noWrap sx={{ color: c.text.muted, fontSize: '0.8125rem' }}>{sectionDescription}</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.75rem', mt: 0.25 }}>{count} tools{deferred ? ' · on-demand' : ''}</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.8125rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{sectionDescription}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabled}
|
||||
onChange={(_, checked) => onEnabledChange(checked)}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{enabled && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25, flexShrink: 0 }}>
|
||||
<Typography sx={{ color: c.text.secondary, fontSize: '0.8125rem' }}>Built-in</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5 }}>
|
||||
<Box onClick={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabled}
|
||||
onChange={(_, checked) => onEnabledChange(checked)}
|
||||
sx={{
|
||||
'& .MuiSwitch-switchBase.Mui-checked': { color: c.accent.primary },
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: c.accent.primary },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{enabled && (
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.ghost, transition: 'transform 0.2s', transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }} />
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<Collapse in={open && enabled} timeout={0} unmountOnExit>
|
||||
|
||||
Reference in New Issue
Block a user