mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 19:57:44 +02:00
[eric] marketplace: pixel pass against live claude.ai, sectioned Filter by checks, exact sort labels, Add menus, connection tabs
This commit is contained in:
@@ -28,8 +28,9 @@ const DirectoryConnectorsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tools = useAppSelector((s) => s.tools.items);
|
||||
const [query, setQuery] = useState('');
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [filterSelected, setFilterSelected] = useState<string[]>(['installed', 'not-installed']);
|
||||
const [sort, setSort] = useState('popular');
|
||||
const toggleFilter = (value: string) => setFilterSelected((p) => (p.includes(value) ? p.filter((v) => v !== value) : [...p, value]));
|
||||
const [installingId, setInstallingId] = useState<string | null>(null);
|
||||
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ open: false, message: '', severity: 'success' });
|
||||
|
||||
@@ -45,11 +46,10 @@ const DirectoryConnectorsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
const q = query.trim().toLowerCase();
|
||||
let out = INTEGRATIONS.filter((ig) =>
|
||||
!q || ig.name.toLowerCase().includes(q) || ig.description.toLowerCase().includes(q));
|
||||
if (filter === 'installed') out = out.filter((ig) => !!installedToolByName[ig.name]);
|
||||
else if (filter === 'not-installed') out = out.filter((ig) => !installedToolByName[ig.name]);
|
||||
out = out.filter((ig) => (installedToolByName[ig.name] ? filterSelected.includes('installed') : filterSelected.includes('not-installed')));
|
||||
if (sort === 'name') out = [...out].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return out;
|
||||
}, [query, filter, sort, installedToolByName]);
|
||||
}, [query, filterSelected, sort, installedToolByName]);
|
||||
|
||||
const popular = useMemo(
|
||||
() => POPULAR_IDS.map((id) => INTEGRATIONS.find((ig) => ig.id === id)).filter((ig): ig is Integration => !!ig),
|
||||
@@ -100,16 +100,14 @@ const DirectoryConnectorsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
chipLabel="Anthropic & Partners"
|
||||
query={query}
|
||||
onQuery={setQuery}
|
||||
filterOptions={[
|
||||
{ value: 'all', label: 'All connectors' },
|
||||
{ value: 'installed', label: 'Installed' },
|
||||
{ value: 'not-installed', label: 'Not installed' },
|
||||
filterSections={[
|
||||
{ label: 'Status', options: [{ value: 'installed', label: 'Installed' }, { value: 'not-installed', label: 'Not installed' }] },
|
||||
]}
|
||||
filterValue={filter}
|
||||
onFilter={setFilter}
|
||||
filterSelected={filterSelected}
|
||||
onToggleFilter={toggleFilter}
|
||||
sortOptions={[
|
||||
{ value: 'popular', label: 'Popular' },
|
||||
{ value: 'name', label: 'Name' },
|
||||
{ value: 'popular', label: 'Default' },
|
||||
{ value: 'name', label: 'Alphabetical' },
|
||||
]}
|
||||
sortValue={sort}
|
||||
onSort={setSort}
|
||||
@@ -120,7 +118,7 @@ const DirectoryConnectorsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
'&::-webkit-scrollbar': { width: 6 },
|
||||
'&::-webkit-scrollbar-thumb': { background: c.border.medium, borderRadius: 3 },
|
||||
}}>
|
||||
{!query.trim() && filter === 'all' && (
|
||||
{!query.trim() && filterSelected.length === 2 && (
|
||||
<>
|
||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 600, letterSpacing: '0.06em', color: c.text.tertiary, textTransform: 'uppercase', mb: 1.25 }}>
|
||||
Popular
|
||||
|
||||
@@ -15,48 +15,77 @@ export interface PickerOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PickerProps {
|
||||
export interface FilterSection {
|
||||
label: string;
|
||||
options: PickerOption[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const DropdownPill: React.FC<PickerProps> = ({ label, options, value, onChange }) => {
|
||||
const pillSx = (c: ReturnType<typeof useClaudeTokens>) => ({
|
||||
display: 'flex', alignItems: 'center', gap: 0.75, px: 1.75, py: 0.9,
|
||||
borderRadius: `${c.radius.md}px`, border: `1px solid ${c.border.medium}`,
|
||||
cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap',
|
||||
'&:hover': { borderColor: c.border.strong, bgcolor: c.bg.elevated },
|
||||
});
|
||||
|
||||
// claude.ai's Filter pill: always reads "Filter by"; state lives in the menu as checkable rows under section headers.
|
||||
const FilterPill: React.FC<{ sections: FilterSection[]; selected: string[]; onToggle: (value: string) => void }> = ({ sections, selected, onToggle }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
const active = options.find((o) => o.value === value);
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
role="button"
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => setAnchor(e.currentTarget)}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 0.75, px: 1.75, py: 0.9,
|
||||
borderRadius: `${c.radius.md}px`, border: `1px solid ${c.border.medium}`,
|
||||
cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap',
|
||||
'&:hover': { borderColor: c.border.strong, bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '0.9375rem', color: c.text.primary }}>
|
||||
{active && active.value !== options[0].value ? active.label : label}
|
||||
</Typography>
|
||||
<Box role="button" onClick={(e: React.MouseEvent<HTMLElement>) => setAnchor(e.currentTarget)} sx={pillSx(c)}>
|
||||
<Typography sx={{ fontSize: '0.9375rem', color: c.text.primary }}>Filter by</Typography>
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.tertiary }} />
|
||||
</Box>
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={!!anchor}
|
||||
onClose={() => setAnchor(null)}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 170 } }}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 190 } }}
|
||||
>
|
||||
{sections.flatMap((section, i) => [
|
||||
<Typography key={`h-${section.label}`} sx={{ px: 2, pt: i === 0 ? 0.75 : 1.25, pb: 0.25, fontSize: '0.75rem', color: c.text.tertiary }}>
|
||||
{section.label}
|
||||
</Typography>,
|
||||
...section.options.map((o) => (
|
||||
<MenuItem
|
||||
key={o.value}
|
||||
onClick={() => onToggle(o.value)}
|
||||
sx={{ fontSize: '0.875rem', color: c.text.primary, display: 'flex', justifyContent: 'space-between', gap: 2, '&:hover': { bgcolor: c.bg.secondary } }}
|
||||
>
|
||||
{o.label}
|
||||
<CheckIcon sx={{ fontSize: 16, color: '#3b82f6', visibility: selected.includes(o.value) ? 'visible' : 'hidden' }} />
|
||||
</MenuItem>
|
||||
)),
|
||||
])}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SortPill: React.FC<{ options: PickerOption[]; value: string; onChange: (value: string) => void }> = ({ options, value, onChange }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Box role="button" onClick={(e: React.MouseEvent<HTMLElement>) => setAnchor(e.currentTarget)} sx={pillSx(c)}>
|
||||
<Typography sx={{ fontSize: '0.9375rem', color: c.text.primary }}>Sort by</Typography>
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: c.text.tertiary }} />
|
||||
</Box>
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={!!anchor}
|
||||
onClose={() => setAnchor(null)}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 190 } }}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<MenuItem
|
||||
key={o.value}
|
||||
onClick={() => { onChange(o.value); setAnchor(null); }}
|
||||
sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1, '&:hover': { bgcolor: c.bg.secondary } }}
|
||||
sx={{ fontSize: '0.875rem', color: c.text.primary, display: 'flex', justifyContent: 'space-between', gap: 2, '&:hover': { bgcolor: c.bg.secondary } }}
|
||||
>
|
||||
<Box sx={{ width: 18, display: 'flex' }}>{o.value === value && <CheckIcon sx={{ fontSize: 16, color: c.text.secondary }} />}</Box>
|
||||
{o.label}
|
||||
<CheckIcon sx={{ fontSize: 16, color: '#3b82f6', visibility: o.value === value ? 'visible' : 'hidden' }} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
@@ -69,9 +98,9 @@ interface Props {
|
||||
chipLabel: string;
|
||||
query: string;
|
||||
onQuery: (q: string) => void;
|
||||
filterOptions: PickerOption[];
|
||||
filterValue: string;
|
||||
onFilter: (v: string) => void;
|
||||
filterSections: FilterSection[];
|
||||
filterSelected: string[];
|
||||
onToggleFilter: (value: string) => void;
|
||||
sortOptions: PickerOption[];
|
||||
sortValue: string;
|
||||
onSort: (v: string) => void;
|
||||
@@ -80,7 +109,7 @@ interface Props {
|
||||
// The Directory's search row + chip/filter row, shared by both tabs (same chrome on claude.ai).
|
||||
const DirectoryFilterBar: React.FC<Props> = ({
|
||||
searchPlaceholder, chipLabel, query, onQuery,
|
||||
filterOptions, filterValue, onFilter, sortOptions, sortValue, onSort,
|
||||
filterSections, filterSelected, onToggleFilter, sortOptions, sortValue, onSort,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
@@ -112,8 +141,8 @@ const DirectoryFilterBar: React.FC<Props> = ({
|
||||
<Typography sx={{ fontSize: '0.9375rem', fontWeight: 500, color: c.text.primary, whiteSpace: 'nowrap' }}>{chipLabel}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<DropdownPill label="Filter by" options={filterOptions} value={filterValue} onChange={onFilter} />
|
||||
<DropdownPill label="Sort by" options={sortOptions} value={sortValue} onChange={onSort} />
|
||||
<FilterPill sections={filterSections} selected={filterSelected} onToggle={onToggleFilter} />
|
||||
<SortPill options={sortOptions} value={sortValue} onChange={onSort} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -49,9 +49,10 @@ const DirectorySkillsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
const localSkills = useAppSelector((s) => s.skills.items);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
// Defaults to the Anthropic set, mirroring claude.ai's Directory landing view.
|
||||
const [filter, setFilter] = useState('anthropic');
|
||||
// claude.ai grammar: Filter by is checkable sections. Community starts unchecked so the landing view is the Anthropic set, same as claude.ai's.
|
||||
const [filterSelected, setFilterSelected] = useState<string[]>(['installed', 'not-installed', 'anthropic']);
|
||||
const [sort, setSort] = useState('popular');
|
||||
const toggleFilter = (value: string) => setFilterSelected((p) => (p.includes(value) ? p.filter((v) => v !== value) : [...p, value]));
|
||||
const [community, setCommunity] = useState<CommunitySkill[]>([]);
|
||||
const [communityLoading, setCommunityLoading] = useState(false);
|
||||
const [installingKey, setInstallingKey] = useState<string | null>(null);
|
||||
@@ -98,7 +99,7 @@ const DirectorySkillsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
// Join curated skills to their skills.sh twin (source anthropics/skills) so Anthropic cards get real install counts.
|
||||
const communityByName = new Map(community.map((cs) => [cs.name.trim().toLowerCase(), cs]));
|
||||
const out: SkillCardModel[] = [];
|
||||
if (filter !== 'community') {
|
||||
if (filterSelected.includes('anthropic')) {
|
||||
for (const sk of curated) {
|
||||
if (q && !sk.name.toLowerCase().includes(q) && !sk.description.toLowerCase().includes(q)) continue;
|
||||
const twin = communityByName.get(sk.name.trim().toLowerCase());
|
||||
@@ -113,7 +114,7 @@ const DirectorySkillsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (filter !== 'anthropic') {
|
||||
if (filterSelected.includes('community')) {
|
||||
const curatedNames = new Set(curated.map((sk) => sk.name.trim().toLowerCase()));
|
||||
for (const cs of community) {
|
||||
// Anthropic-sourced twins already render as curated cards; listing them twice reads as duplicates.
|
||||
@@ -131,10 +132,16 @@ const DirectorySkillsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (sort === 'name') out.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
else out.sort((a, b) => (b.installs ?? -1) - (a.installs ?? -1));
|
||||
return out;
|
||||
}, [curated, community, query, filter, sort]);
|
||||
const statusOk = (installed: boolean): boolean => (installed ? filterSelected.includes('installed') : filterSelected.includes('not-installed'));
|
||||
const withStatus = out.filter((card) => statusOk(installedNames.has((card.curated?.name ?? card.community?.name ?? '').trim().toLowerCase())));
|
||||
if (sort === 'name') withStatus.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
else {
|
||||
// claude.ai's landing order: installed first, then popularity descending.
|
||||
const rank = (card: SkillCardModel): number => (installedNames.has((card.curated?.name ?? card.community?.name ?? '').trim().toLowerCase()) ? 1 : 0);
|
||||
withStatus.sort((a, b) => rank(b) - rank(a) || (b.installs ?? -1) - (a.installs ?? -1));
|
||||
}
|
||||
return withStatus;
|
||||
}, [curated, community, query, filterSelected, sort, installedNames]);
|
||||
|
||||
const handleInstallCurated = async (card: SkillCardModel) => {
|
||||
if (!card.curated) return;
|
||||
@@ -160,19 +167,18 @@ const DirectorySkillsTab: React.FC<Props> = ({ onOpenInstalled }) => {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, height: '100%', minHeight: 0 }}>
|
||||
<DirectoryFilterBar
|
||||
searchPlaceholder="Search skills..."
|
||||
chipLabel={filter === 'community' ? 'Community' : filter === 'all' ? 'All skills' : 'Anthropic'}
|
||||
chipLabel={filterSelected.includes('community') ? (filterSelected.includes('anthropic') ? 'Anthropic & Community' : 'Community') : 'Anthropic'}
|
||||
query={query}
|
||||
onQuery={setQuery}
|
||||
filterOptions={[
|
||||
{ value: 'all', label: 'All skills' },
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'community', label: 'Community' },
|
||||
filterSections={[
|
||||
{ label: 'Status', options: [{ value: 'installed', label: 'Installed' }, { value: 'not-installed', label: 'Not installed' }] },
|
||||
{ label: 'Source', options: [{ value: 'anthropic', label: 'Anthropic' }, { value: 'community', label: 'Community' }] },
|
||||
]}
|
||||
filterValue={filter}
|
||||
onFilter={setFilter}
|
||||
filterSelected={filterSelected}
|
||||
onToggleFilter={toggleFilter}
|
||||
sortOptions={[
|
||||
{ value: 'popular', label: 'Popular' },
|
||||
{ value: 'name', label: 'Name' },
|
||||
{ value: 'popular', label: 'Most popular' },
|
||||
{ value: 'name', label: 'Name A-Z' },
|
||||
]}
|
||||
sortValue={sort}
|
||||
onSort={setSort}
|
||||
|
||||
@@ -17,7 +17,8 @@ import Alert from '@mui/material/Alert';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
@@ -60,7 +61,6 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import SkillBuilderChat, { SkillPreviewData } from './SkillBuilderChat';
|
||||
import DirectoryDialog from '../Directory/DirectoryDialog';
|
||||
import UploadSkillDialog from '../Directory/UploadSkillDialog';
|
||||
import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined';
|
||||
import DriveFolderUploadOutlinedIcon from '@mui/icons-material/DriveFolderUploadOutlined';
|
||||
|
||||
interface SkillForm {
|
||||
@@ -107,6 +107,7 @@ const Skills: React.FC = () => {
|
||||
const [builderOpen, setBuilderOpen] = useState(false);
|
||||
const [directoryOpen, setDirectoryOpen] = useState(false);
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [addMenuAnchor, setAddMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleBuilderPreview = useCallback((data: SkillPreviewData | null) => {
|
||||
setBuilderPreview(data);
|
||||
@@ -350,88 +351,63 @@ const Skills: React.FC = () => {
|
||||
bgcolor: c.bg.secondary,
|
||||
}}
|
||||
>
|
||||
{/* The Settings pane header already says Skills; this row is just the action strip. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', px: 2, pt: 1.5, pb: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 0.25 }}>
|
||||
<Tooltip title="Import .swarm">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent(IMPORT_OPEN_EVENT))}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}
|
||||
>
|
||||
<UploadFileIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Search">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSearchFilter((p) => (p === '' ? ' ' : ''))}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}
|
||||
>
|
||||
<SearchIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Upload skill">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setUploadOpen(true)}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}
|
||||
>
|
||||
<DriveFolderUploadOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Create skill">
|
||||
<IconButton size="small" onClick={openCreate} sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
|
||||
<AddIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ px: 1.5, pb: 0.5 }}>
|
||||
{/* claude.ai's Skills header grammar: search icon, Browse, Add menu (Create with Claude / Write skill instructions / Upload a skill), plus our Import .swarm row. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.75, px: 1.5, pt: 1.5, pb: 1 }}>
|
||||
<Tooltip title="Search">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSearchFilter((p) => (p === '' ? ' ' : ''))}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}
|
||||
>
|
||||
<SearchIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AutoFixHighIcon sx={{ fontSize: 14 }} />}
|
||||
onClick={() => setBuilderOpen(true)}
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
color: c.accent.primary,
|
||||
justifyContent: 'center',
|
||||
gap: 0.5,
|
||||
py: 0.8,
|
||||
px: 1.5,
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${c.accent.primary}40`,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}10`, borderColor: c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Build with AI
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ px: 1.5, pb: 0.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<StorefrontOutlinedIcon sx={{ fontSize: 14 }} />}
|
||||
onClick={() => setDirectoryOpen(true)}
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
color: c.bg.surface,
|
||||
bgcolor: c.text.primary,
|
||||
justifyContent: 'center',
|
||||
gap: 0.5,
|
||||
py: 0.8,
|
||||
px: 1.5,
|
||||
borderRadius: 999,
|
||||
'&:hover': { bgcolor: c.text.secondary },
|
||||
textTransform: 'none', fontSize: '0.8125rem', fontWeight: 600, px: 1.5, py: 0.4,
|
||||
color: c.text.primary, bgcolor: c.bg.secondary, borderRadius: `${c.radius.md}px`,
|
||||
'&:hover': { bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
Browse directory
|
||||
Browse
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<KeyboardArrowDownIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => setAddMenuAnchor(e.currentTarget)}
|
||||
sx={{
|
||||
textTransform: 'none', fontSize: '0.8125rem', fontWeight: 600, px: 1.5, py: 0.4,
|
||||
color: c.text.primary, bgcolor: c.bg.secondary, borderRadius: `${c.radius.md}px`,
|
||||
'&:hover': { bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={addMenuAnchor}
|
||||
open={!!addMenuAnchor}
|
||||
onClose={() => setAddMenuAnchor(null)}
|
||||
PaperProps={{ sx: { bgcolor: c.bg.surface, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, mt: 0.5, minWidth: 220 } }}
|
||||
>
|
||||
<MenuItem onClick={() => { setAddMenuAnchor(null); setBuilderOpen(true); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<AutoFixHighIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Create with Claude
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { setAddMenuAnchor(null); openCreate(); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<DescriptionIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Write skill instructions
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { setAddMenuAnchor(null); setUploadOpen(true); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<DriveFolderUploadOutlinedIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Upload a skill
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { setAddMenuAnchor(null); window.dispatchEvent(new CustomEvent(IMPORT_OPEN_EVENT)); }} sx={{ fontSize: '0.875rem', color: c.text.primary, gap: 1.5, '&:hover': { bgcolor: c.bg.secondary } }}>
|
||||
<UploadFileIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
Import .swarm
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<Collapse in={searchFilter !== ''} timeout={0} unmountOnExit>
|
||||
@@ -776,15 +752,16 @@ const Skills: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600, fontFamily: c.font.sans }}>
|
||||
{editingId ? 'Edit Skill' : 'New Skill'}
|
||||
{editingId ? 'Edit skill' : 'Write skill instructions'}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '8px !important' }}>
|
||||
<TextField
|
||||
label="Name"
|
||||
label="Skill name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="weekly-status-report"
|
||||
sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.secondary } }}
|
||||
/>
|
||||
<TextField
|
||||
@@ -793,6 +770,7 @@ const Skills: React.FC = () => {
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Generate weekly status reports from recent work. Use when asked for updates or progress summaries."
|
||||
sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.secondary } }}
|
||||
/>
|
||||
<TextField
|
||||
@@ -805,13 +783,14 @@ const Skills: React.FC = () => {
|
||||
sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.secondary } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Content (Markdown)"
|
||||
label="Instructions"
|
||||
value={form.content}
|
||||
onChange={(e) => setForm({ ...form, content: e.target.value })}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={12}
|
||||
maxRows={24}
|
||||
placeholder="Summarize my recent work in three sections: wins, blockers, and next steps. Keep the tone professional but not stiff..."
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: c.bg.secondary, fontFamily: c.font.mono, fontSize: '0.875rem',
|
||||
@@ -832,7 +811,7 @@ const Skills: React.FC = () => {
|
||||
textTransform: 'none', borderRadius: `${c.radius.md}px`,
|
||||
}}
|
||||
>
|
||||
{editingId ? 'Save Changes' : 'Create Skill'}
|
||||
{editingId ? 'Save Changes' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -75,6 +75,17 @@ const Tools: React.FC = () => {
|
||||
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [directoryOpen, setDirectoryOpen] = useState(false);
|
||||
const [customConnectorOpen, setCustomConnectorOpen] = useState(false);
|
||||
// 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],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchTools());
|
||||
@@ -121,7 +132,7 @@ const Tools: React.FC = () => {
|
||||
onClick={handleMenuOpen}
|
||||
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.pressed }, textTransform: 'none', borderRadius: 2, fontSize: '0.8125rem' }}
|
||||
>
|
||||
New Tool
|
||||
Add
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={menuAnchor}
|
||||
@@ -202,6 +213,24 @@ const Tools: React.FC = () => {
|
||||
</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 ? (
|
||||
@@ -217,7 +246,7 @@ const Tools: React.FC = () => {
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pl: 1 }}>
|
||||
{uninstalledIntegrations.map((ig) => (
|
||||
{visibleGallery.map((ig) => (
|
||||
<IntegrationGalleryCard
|
||||
key={ig.id}
|
||||
integration={ig}
|
||||
@@ -225,7 +254,7 @@ const Tools: React.FC = () => {
|
||||
onToggle={a.handleIntegrationToggle}
|
||||
/>
|
||||
))}
|
||||
{tools.map((tool) => (
|
||||
{visibleTools.map((tool) => (
|
||||
<CustomToolCard
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
|
||||
Reference in New Issue
Block a user