feat(skills): enhance skills browser with bundles and categories

- Add skill bundles with batch enable/disable functionality
- Create SkillDetailDialog for viewing skill metadata
- Add Tabs component for All Skills / Bundles navigation
- Implement category filtering with skill counts
- Add search functionality for skills
- Show Gateway connection status awareness
- Add configuration and dependency badges
- Include recommended bundle highlighting
This commit is contained in:
Haze
2026-02-05 23:45:46 +08:00
Unverified
parent 727869f2b8
commit 6239b156cb
4 changed files with 675 additions and 110 deletions

View File

@@ -0,0 +1,153 @@
# Commit 9: Skills Browser
## Summary
Enhance the Skills page with skill bundles, category filtering, detail dialogs, and improved user experience for managing AI capabilities.
## Changes
### React Renderer
#### `src/pages/Skills/index.tsx`
Complete rewrite with enhanced features:
**New Components:**
- `SkillDetailDialog` - Modal showing skill details, dependencies, and configuration
- `BundleCard` - Skill bundle display with enable/disable actions
**Features:**
- Tabbed interface: "All Skills" and "Bundles"
- Category filtering with skill counts
- Search functionality
- Gateway connection status awareness
- Skill bundles with batch enable/disable
- Skill detail dialog with metadata
- Configuration indicator badges
- Toast notifications on skill toggle
**UI Improvements:**
- Category icons for visual distinction
- Enabled state highlighting (border and background)
- Hover states for cards
- Recommended bundle badges
- Statistics bar with counts
#### `src/components/ui/tabs.tsx` (New)
Tabs component based on shadcn/ui and Radix Tabs:
- `Tabs` - Root container
- `TabsList` - Tab navigation bar
- `TabsTrigger` - Individual tab button
- `TabsContent` - Tab panel content
### Data Structures
#### Skill Bundles
Predefined bundles:
- **Productivity Pack** - Calendar, reminders, notes, tasks, timer
- **Developer Tools** - Code assist, git ops, docs lookup
- **Information Hub** - Web search, news, weather, translate
- **Smart Home** - Lights, thermostat, security cam, routines
## Technical Details
### Component Architecture
```
Skills Page
|
+-- TabsList
| |
| +-- "All Skills" Tab
| +-- "Bundles" Tab
|
+-- TabsContent: All Skills
| |
| +-- Search/Filter Bar
| +-- Category Buttons
| +-- Skills Grid
| |
| +-- SkillCard (click -> SkillDetailDialog)
|
+-- TabsContent: Bundles
| |
| +-- BundleCard Grid
|
+-- Statistics Bar
|
+-- SkillDetailDialog (modal)
```
### Category Configuration
| Category | Icon | Label |
|----------|------|-------|
| productivity | 📋 | Productivity |
| developer | 💻 | Developer |
| smart-home | 🏠 | Smart Home |
| media | 🎬 | Media |
| communication | 💬 | Communication |
| security | 🔒 | Security |
| information | 📰 | Information |
| utility | 🔧 | Utility |
| custom | ⚡ | Custom |
### Bundle Operations
**Enable Bundle:**
```typescript
for (const skill of bundleSkills) {
if (!skill.enabled) {
await enableSkill(skill.id);
}
}
```
**Disable Bundle:**
```typescript
for (const skill of bundleSkills) {
if (!skill.isCore) {
await disableSkill(skill.id);
}
}
```
### State Management
**Local State:**
- `searchQuery` - Search input value
- `selectedCategory` - Active category filter
- `selectedSkill` - Skill for detail dialog
- `activeTab` - Current tab ("all" | "bundles")
**Store Integration:**
- `useSkillsStore` - Skills data and actions
- `useGatewayStore` - Connection status
### Filtering Logic
```typescript
const filteredSkills = skills.filter((skill) => {
const matchesSearch =
skill.name.toLowerCase().includes(searchQuery) ||
skill.description.toLowerCase().includes(searchQuery);
const matchesCategory =
selectedCategory === 'all' ||
skill.category === selectedCategory;
return matchesSearch && matchesCategory;
});
```
### UI States
**Skill Card:**
- Default: Standard border, white background
- Enabled: Primary border (50% opacity), primary background (5% opacity)
- Hover: Primary border (50% opacity)
- Core: Lock icon, switch disabled
**Bundle Card:**
- Default: Standard styling
- All Enabled: Primary border, primary background
- Recommended: Amber badge with sparkle icon
## Version
v0.1.0-alpha (incremental)

View File

@@ -14,6 +14,7 @@
* [commit_6] Auto-update functionality - electron-updater integration with UI * [commit_6] Auto-update functionality - electron-updater integration with UI
* [commit_7] Packaging and distribution - CI/CD, multi-platform builds, icon generation * [commit_7] Packaging and distribution - CI/CD, multi-platform builds, icon generation
* [commit_8] Chat interface - Markdown support, typing indicator, welcome screen * [commit_8] Chat interface - Markdown support, typing indicator, welcome screen
* [commit_9] Skills browser - Bundles, categories, detail dialog
### Plan: ### Plan:
1. ~~Initialize project structure~~ 1. ~~Initialize project structure~~
@@ -24,7 +25,7 @@
6. ~~Add auto-update functionality~~ 6. ~~Add auto-update functionality~~
7. ~~Packaging and distribution setup~~ 7. ~~Packaging and distribution setup~~
8. ~~Chat interface~~ 8. ~~Chat interface~~
9. Skills browser/enable page 9. ~~Skills browser/enable page~~
10. Cron tasks management 10. Cron tasks management
## Version Milestones ## Version Milestones

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -2,17 +2,34 @@
* Skills Page * Skills Page
* Browse and manage AI skills * Browse and manage AI skills
*/ */
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { Search, Puzzle, RefreshCw, Lock } from 'lucide-react'; import {
Search,
Puzzle,
RefreshCw,
Lock,
Package,
Info,
X,
Settings,
CheckCircle2,
XCircle,
AlertCircle,
ChevronRight,
Sparkles,
} from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useSkillsStore } from '@/stores/skills'; import { useSkillsStore } from '@/stores/skills';
import { useGatewayStore } from '@/stores/gateway';
import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { SkillCategory } from '@/types/skill'; import { toast } from 'sonner';
import type { Skill, SkillCategory, SkillBundle } from '@/types/skill';
const categoryLabels: Record<SkillCategory, string> = { const categoryLabels: Record<SkillCategory, string> = {
productivity: 'Productivity', productivity: 'Productivity',
@@ -26,15 +43,237 @@ const categoryLabels: Record<SkillCategory, string> = {
custom: 'Custom', custom: 'Custom',
}; };
const categoryIcons: Record<SkillCategory, string> = {
productivity: '📋',
developer: '💻',
'smart-home': '🏠',
media: '🎬',
communication: '💬',
security: '🔒',
information: '📰',
utility: '🔧',
custom: '⚡',
};
// Predefined skill bundles
const skillBundles: SkillBundle[] = [
{
id: 'productivity',
name: 'Productivity Pack',
nameZh: '效率工具包',
description: 'Essential tools for daily productivity including calendar, reminders, and notes',
descriptionZh: '日常效率必备工具,包含日历、提醒和笔记',
icon: '📋',
skills: ['calendar', 'reminders', 'notes', 'tasks', 'timer'],
recommended: true,
},
{
id: 'developer',
name: 'Developer Tools',
nameZh: '开发者工具',
description: 'Code assistance, git operations, and technical documentation lookup',
descriptionZh: '代码辅助、Git 操作和技术文档查询',
icon: '💻',
skills: ['code-assist', 'git-ops', 'docs-lookup', 'snippet-manager'],
recommended: true,
},
{
id: 'information',
name: 'Information Hub',
nameZh: '信息中心',
description: 'Stay informed with web search, news, weather, and knowledge base',
descriptionZh: '通过网页搜索、新闻、天气和知识库保持信息畅通',
icon: '📰',
skills: ['web-search', 'news', 'weather', 'wikipedia', 'translate'],
},
{
id: 'smart-home',
name: 'Smart Home',
nameZh: '智能家居',
description: 'Control your smart home devices and automation routines',
descriptionZh: '控制智能家居设备和自动化场景',
icon: '🏠',
skills: ['lights', 'thermostat', 'security-cam', 'routines'],
},
];
// Skill detail dialog component
interface SkillDetailDialogProps {
skill: Skill;
onClose: () => void;
onToggle: (enabled: boolean) => void;
}
function SkillDetailDialog({ skill, onClose, onToggle }: SkillDetailDialogProps) {
return (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4" onClick={onClose}>
<Card className="w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<CardHeader className="flex flex-row items-start justify-between">
<div className="flex items-center gap-4">
<span className="text-4xl">{skill.icon || '🔧'}</span>
<div>
<CardTitle className="flex items-center gap-2">
{skill.name}
{skill.isCore && <Lock className="h-4 w-4 text-muted-foreground" />}
</CardTitle>
<CardDescription>{categoryLabels[skill.category]}</CardDescription>
</div>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">{skill.description}</p>
<div className="flex flex-wrap gap-2">
{skill.version && (
<Badge variant="outline">v{skill.version}</Badge>
)}
{skill.author && (
<Badge variant="secondary">by {skill.author}</Badge>
)}
{skill.isCore && (
<Badge variant="secondary">
<Lock className="h-3 w-3 mr-1" />
Core Skill
</Badge>
)}
</div>
{skill.dependencies && skill.dependencies.length > 0 && (
<div>
<p className="text-sm font-medium mb-2">Dependencies:</p>
<div className="flex flex-wrap gap-2">
{skill.dependencies.map((dep) => (
<Badge key={dep} variant="outline">{dep}</Badge>
))}
</div>
</div>
)}
<div className="flex items-center justify-between pt-4 border-t">
<div className="flex items-center gap-2">
{skill.enabled ? (
<>
<CheckCircle2 className="h-5 w-5 text-green-500" />
<span className="text-green-600 dark:text-green-400">Enabled</span>
</>
) : (
<>
<XCircle className="h-5 w-5 text-muted-foreground" />
<span className="text-muted-foreground">Disabled</span>
</>
)}
</div>
<div className="flex items-center gap-2">
{skill.configurable && (
<Button variant="outline" size="sm">
<Settings className="h-4 w-4 mr-2" />
Configure
</Button>
)}
<Switch
checked={skill.enabled}
onCheckedChange={() => onToggle(!skill.enabled)}
disabled={skill.isCore}
/>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// Bundle card component
interface BundleCardProps {
bundle: SkillBundle;
skills: Skill[];
onApply: () => void;
}
function BundleCard({ bundle, skills, onApply }: BundleCardProps) {
const bundleSkills = skills.filter((s) => bundle.skills.includes(s.id));
const enabledCount = bundleSkills.filter((s) => s.enabled).length;
const isFullyEnabled = bundleSkills.length > 0 && enabledCount === bundleSkills.length;
return (
<Card className={cn(
'hover:border-primary/50 transition-colors cursor-pointer',
isFullyEnabled && 'border-primary/50 bg-primary/5'
)}>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<span className="text-3xl">{bundle.icon}</span>
<div>
<CardTitle className="text-base flex items-center gap-2">
{bundle.name}
{bundle.recommended && (
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
<Sparkles className="h-3 w-3 mr-1" />
Recommended
</Badge>
)}
</CardTitle>
<CardDescription className="text-xs">
{enabledCount}/{bundleSkills.length} skills enabled
</CardDescription>
</div>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground line-clamp-2">
{bundle.description}
</p>
<div className="flex flex-wrap gap-1">
{bundleSkills.slice(0, 4).map((skill) => (
<Badge
key={skill.id}
variant={skill.enabled ? 'default' : 'outline'}
className="text-xs"
>
{skill.icon} {skill.name}
</Badge>
))}
{bundleSkills.length > 4 && (
<Badge variant="outline" className="text-xs">
+{bundleSkills.length - 4} more
</Badge>
)}
</div>
<Button
variant={isFullyEnabled ? 'secondary' : 'default'}
size="sm"
className="w-full"
onClick={onApply}
>
{isFullyEnabled ? 'Disable Bundle' : 'Enable Bundle'}
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</CardContent>
</Card>
);
}
export function Skills() { export function Skills() {
const { skills, loading, error, fetchSkills, enableSkill, disableSkill } = useSkillsStore(); const { skills, loading, error, fetchSkills, enableSkill, disableSkill } = useSkillsStore();
const gatewayStatus = useGatewayStore((state) => state.status);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<SkillCategory | 'all'>('all'); const [selectedCategory, setSelectedCategory] = useState<SkillCategory | 'all'>('all');
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [activeTab, setActiveTab] = useState('all');
const isGatewayRunning = gatewayStatus.state === 'running';
// Fetch skills on mount // Fetch skills on mount
useEffect(() => { useEffect(() => {
fetchSkills(); if (isGatewayRunning) {
}, [fetchSkills]); fetchSkills();
}
}, [fetchSkills, isGatewayRunning]);
// Filter skills // Filter skills
const filteredSkills = skills.filter((skill) => { const filteredSkills = skills.filter((skill) => {
@@ -44,21 +283,49 @@ export function Skills() {
return matchesSearch && matchesCategory; return matchesSearch && matchesCategory;
}); });
// Get unique categories // Get unique categories with counts
const categories = Array.from(new Set(skills.map((s) => s.category))); const categoryStats = skills.reduce((acc, skill) => {
acc[skill.category] = (acc[skill.category] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Handle toggle // Handle toggle
const handleToggle = async (skillId: string, enabled: boolean) => { const handleToggle = useCallback(async (skillId: string, enable: boolean) => {
try { try {
if (enabled) { if (enable) {
await disableSkill(skillId);
} else {
await enableSkill(skillId); await enableSkill(skillId);
toast.success('Skill enabled');
} else {
await disableSkill(skillId);
toast.success('Skill disabled');
} }
} catch (error) { } catch (err) {
// Error handled in store toast.error(String(err));
} }
}; }, [enableSkill, disableSkill]);
// Handle bundle apply
const handleBundleApply = useCallback(async (bundle: SkillBundle) => {
const bundleSkills = skills.filter((s) => bundle.skills.includes(s.id));
const allEnabled = bundleSkills.every((s) => s.enabled);
try {
for (const skill of bundleSkills) {
if (allEnabled) {
if (!skill.isCore) {
await disableSkill(skill.id);
}
} else {
if (!skill.enabled) {
await enableSkill(skill.id);
}
}
}
toast.success(allEnabled ? 'Bundle disabled' : 'Bundle enabled');
} catch (err) {
toast.error('Failed to apply bundle');
}
}, [skills, enableSkill, disableSkill]);
if (loading) { if (loading) {
return ( return (
@@ -75,122 +342,213 @@ export function Skills() {
<div> <div>
<h1 className="text-2xl font-bold">Skills</h1> <h1 className="text-2xl font-bold">Skills</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Browse and manage AI skills Browse and manage AI capabilities
</p> </p>
</div> </div>
<Button variant="outline" onClick={fetchSkills}> <Button variant="outline" onClick={fetchSkills} disabled={!isGatewayRunning}>
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
Refresh Refresh
</Button> </Button>
</div> </div>
{/* Search and Filter */} {/* Gateway Warning */}
<div className="flex gap-4"> {!isGatewayRunning && (
<div className="relative flex-1"> <Card className="border-yellow-500 bg-yellow-50 dark:bg-yellow-900/10">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <CardContent className="py-4 flex items-center gap-3">
<Input <AlertCircle className="h-5 w-5 text-yellow-600" />
placeholder="Search skills..." <span className="text-yellow-700 dark:text-yellow-400">
value={searchQuery} Gateway is not running. Skills cannot be loaded without an active Gateway.
onChange={(e) => setSearchQuery(e.target.value)} </span>
className="pl-9"
/>
</div>
<div className="flex gap-2 flex-wrap">
<Button
variant={selectedCategory === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedCategory('all')}
>
All
</Button>
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedCategory(category)}
>
{categoryLabels[category]}
</Button>
))}
</div>
</div>
{/* Error Display */}
{error && (
<Card className="border-destructive">
<CardContent className="py-4 text-destructive">
{error}
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* Skills Grid */} {/* Tabs */}
{filteredSkills.length === 0 ? ( <Tabs value={activeTab} onValueChange={setActiveTab}>
<Card> <TabsList>
<CardContent className="flex flex-col items-center justify-center py-12"> <TabsTrigger value="all" className="gap-2">
<Puzzle className="h-12 w-12 text-muted-foreground mb-4" /> <Puzzle className="h-4 w-4" />
<h3 className="text-lg font-medium mb-2">No skills found</h3> All Skills
<p className="text-muted-foreground"> </TabsTrigger>
{searchQuery ? 'Try a different search term' : 'No skills available'} <TabsTrigger value="bundles" className="gap-2">
</p> <Package className="h-4 w-4" />
</CardContent> Bundles
</Card> </TabsTrigger>
) : ( </TabsList>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredSkills.map((skill) => ( <TabsContent value="all" className="space-y-6 mt-6">
<Card key={skill.id} className={cn(skill.enabled && 'border-primary/50')}> {/* Search and Filter */}
<CardHeader className="pb-3"> <div className="flex gap-4 flex-wrap">
<div className="flex items-start justify-between"> <div className="relative flex-1 min-w-[200px]">
<div className="flex items-center gap-3"> <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<span className="text-2xl">{skill.icon || '🔧'}</span> <Input
<div> placeholder="Search skills..."
<CardTitle className="text-base flex items-center gap-2"> value={searchQuery}
{skill.name} onChange={(e) => setSearchQuery(e.target.value)}
{skill.isCore && ( className="pl-9"
<Lock className="h-3 w-3 text-muted-foreground" /> />
)} </div>
</CardTitle> <div className="flex gap-2 flex-wrap">
<CardDescription className="text-xs"> <Button
{categoryLabels[skill.category]} variant={selectedCategory === 'all' ? 'default' : 'outline'}
</CardDescription> size="sm"
</div> onClick={() => setSelectedCategory('all')}
</div> >
<Switch All ({skills.length})
checked={skill.enabled} </Button>
onCheckedChange={() => handleToggle(skill.id, skill.enabled)} {Object.entries(categoryStats).map(([category, count]) => (
disabled={skill.isCore} <Button
/> key={category}
</div> variant={selectedCategory === category ? 'default' : 'outline'}
</CardHeader> size="sm"
<CardContent> onClick={() => setSelectedCategory(category as SkillCategory)}
<p className="text-sm text-muted-foreground line-clamp-2"> className="gap-1"
{skill.description} >
</p> <span>{categoryIcons[category as SkillCategory]}</span>
{skill.version && ( {categoryLabels[category as SkillCategory]} ({count})
<Badge variant="outline" className="mt-2 text-xs"> </Button>
v{skill.version} ))}
</Badge> </div>
)} </div>
{/* Error Display */}
{error && (
<Card className="border-destructive">
<CardContent className="py-4 text-destructive flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
{error}
</CardContent> </CardContent>
</Card> </Card>
))} )}
</div>
)} {/* Skills Grid */}
{filteredSkills.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Puzzle className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">No skills found</h3>
<p className="text-muted-foreground">
{searchQuery ? 'Try a different search term' : 'No skills available'}
</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredSkills.map((skill) => (
<Card
key={skill.id}
className={cn(
'cursor-pointer hover:border-primary/50 transition-colors',
skill.enabled && 'border-primary/50 bg-primary/5'
)}
onClick={() => setSelectedSkill(skill)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{skill.icon || categoryIcons[skill.category]}</span>
<div>
<CardTitle className="text-base flex items-center gap-2">
{skill.name}
{skill.isCore && (
<Lock className="h-3 w-3 text-muted-foreground" />
)}
</CardTitle>
<CardDescription className="text-xs">
{categoryLabels[skill.category]}
</CardDescription>
</div>
</div>
<Switch
checked={skill.enabled}
onCheckedChange={(checked) => {
handleToggle(skill.id, checked);
}}
disabled={skill.isCore}
onClick={(e) => e.stopPropagation()}
/>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{skill.description}
</p>
<div className="flex items-center gap-2 mt-2">
{skill.version && (
<Badge variant="outline" className="text-xs">
v{skill.version}
</Badge>
)}
{skill.configurable && (
<Badge variant="secondary" className="text-xs">
<Settings className="h-3 w-3 mr-1" />
Configurable
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
<TabsContent value="bundles" className="space-y-6 mt-6">
<p className="text-muted-foreground">
Skill bundles are pre-configured collections of skills for common use cases.
Enable a bundle to quickly set up multiple related skills at once.
</p>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{skillBundles.map((bundle) => (
<BundleCard
key={bundle.id}
bundle={bundle}
skills={skills}
onApply={() => handleBundleApply(bundle)}
/>
))}
</div>
</TabsContent>
</Tabs>
{/* Statistics */} {/* Statistics */}
<Card> <Card>
<CardContent className="py-4"> <CardContent className="py-4">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"> <div className="flex items-center gap-4">
{skills.filter((s) => s.enabled).length} of {skills.length} skills enabled <span className="text-muted-foreground">
</span> <span className="font-medium text-foreground">
<span className="text-muted-foreground"> {skills.filter((s) => s.enabled).length}
{skills.filter((s) => s.isCore).length} core skills </span>
</span> {' '}of {skills.length} skills enabled
</span>
<span className="text-muted-foreground">
<span className="font-medium text-foreground">
{skills.filter((s) => s.isCore).length}
</span>
{' '}core skills
</span>
</div>
<Button variant="ghost" size="sm" className="text-muted-foreground">
<Info className="h-4 w-4 mr-1" />
Learn about skills
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Skill Detail Dialog */}
{selectedSkill && (
<SkillDetailDialog
skill={selectedSkill}
onClose={() => setSelectedSkill(null)}
onToggle={(enabled) => {
handleToggle(selectedSkill.id, enabled);
setSelectedSkill({ ...selectedSkill, enabled });
}}
/>
)}
</div> </div>
); );
} }