From 6239b156cb6e4ee8da3ddccd6b341c7cc4c0dd1e Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Thu, 5 Feb 2026 23:45:46 +0800 Subject: [PATCH] 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 --- build_process/commit_9_skills_browser.md | 153 ++++++ build_process/process.md | 3 +- src/components/ui/tabs.tsx | 53 +++ src/pages/Skills/index.tsx | 576 ++++++++++++++++++----- 4 files changed, 675 insertions(+), 110 deletions(-) create mode 100644 build_process/commit_9_skills_browser.md create mode 100644 src/components/ui/tabs.tsx diff --git a/build_process/commit_9_skills_browser.md b/build_process/commit_9_skills_browser.md new file mode 100644 index 000000000..9a479a332 --- /dev/null +++ b/build_process/commit_9_skills_browser.md @@ -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) diff --git a/build_process/process.md b/build_process/process.md index 20715a3b0..c52243da4 100644 --- a/build_process/process.md +++ b/build_process/process.md @@ -14,6 +14,7 @@ * [commit_6] Auto-update functionality - electron-updater integration with UI * [commit_7] Packaging and distribution - CI/CD, multi-platform builds, icon generation * [commit_8] Chat interface - Markdown support, typing indicator, welcome screen +* [commit_9] Skills browser - Bundles, categories, detail dialog ### Plan: 1. ~~Initialize project structure~~ ✅ @@ -24,7 +25,7 @@ 6. ~~Add auto-update functionality~~ ✅ 7. ~~Packaging and distribution setup~~ ✅ 8. ~~Chat interface~~ ✅ -9. Skills browser/enable page +9. ~~Skills browser/enable page~~ ✅ 10. Cron tasks management ## Version Milestones diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 000000000..f57fffdb5 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/pages/Skills/index.tsx b/src/pages/Skills/index.tsx index 9e0cc84f0..7316499f1 100644 --- a/src/pages/Skills/index.tsx +++ b/src/pages/Skills/index.tsx @@ -2,17 +2,34 @@ * Skills Page * Browse and manage AI skills */ -import { useEffect, useState } from 'react'; -import { Search, Puzzle, RefreshCw, Lock } from 'lucide-react'; +import { useEffect, useState, useCallback } from '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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useSkillsStore } from '@/stores/skills'; +import { useGatewayStore } from '@/stores/gateway'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; 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 = { productivity: 'Productivity', @@ -26,15 +43,237 @@ const categoryLabels: Record = { custom: 'Custom', }; +const categoryIcons: Record = { + 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 ( +
+ e.stopPropagation()}> + +
+ {skill.icon || '🔧'} +
+ + {skill.name} + {skill.isCore && } + + {categoryLabels[skill.category]} +
+
+ +
+ +

{skill.description}

+ +
+ {skill.version && ( + v{skill.version} + )} + {skill.author && ( + by {skill.author} + )} + {skill.isCore && ( + + + Core Skill + + )} +
+ + {skill.dependencies && skill.dependencies.length > 0 && ( +
+

Dependencies:

+
+ {skill.dependencies.map((dep) => ( + {dep} + ))} +
+
+ )} + +
+
+ {skill.enabled ? ( + <> + + Enabled + + ) : ( + <> + + Disabled + + )} +
+
+ {skill.configurable && ( + + )} + onToggle(!skill.enabled)} + disabled={skill.isCore} + /> +
+
+
+
+
+ ); +} + +// 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 ( + + +
+
+ {bundle.icon} +
+ + {bundle.name} + {bundle.recommended && ( + + + Recommended + + )} + + + {enabledCount}/{bundleSkills.length} skills enabled + +
+
+
+
+ +

+ {bundle.description} +

+
+ {bundleSkills.slice(0, 4).map((skill) => ( + + {skill.icon} {skill.name} + + ))} + {bundleSkills.length > 4 && ( + + +{bundleSkills.length - 4} more + + )} +
+ +
+
+ ); +} + export function Skills() { const { skills, loading, error, fetchSkills, enableSkill, disableSkill } = useSkillsStore(); + const gatewayStatus = useGatewayStore((state) => state.status); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); + const [selectedSkill, setSelectedSkill] = useState(null); + const [activeTab, setActiveTab] = useState('all'); + + const isGatewayRunning = gatewayStatus.state === 'running'; // Fetch skills on mount useEffect(() => { - fetchSkills(); - }, [fetchSkills]); + if (isGatewayRunning) { + fetchSkills(); + } + }, [fetchSkills, isGatewayRunning]); // Filter skills const filteredSkills = skills.filter((skill) => { @@ -44,21 +283,49 @@ export function Skills() { return matchesSearch && matchesCategory; }); - // Get unique categories - const categories = Array.from(new Set(skills.map((s) => s.category))); + // Get unique categories with counts + const categoryStats = skills.reduce((acc, skill) => { + acc[skill.category] = (acc[skill.category] || 0) + 1; + return acc; + }, {} as Record); // Handle toggle - const handleToggle = async (skillId: string, enabled: boolean) => { + const handleToggle = useCallback(async (skillId: string, enable: boolean) => { try { - if (enabled) { - await disableSkill(skillId); - } else { + if (enable) { await enableSkill(skillId); + toast.success('Skill enabled'); + } else { + await disableSkill(skillId); + toast.success('Skill disabled'); } - } catch (error) { - // Error handled in store + } catch (err) { + 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) { return ( @@ -75,122 +342,213 @@ export function Skills() {

Skills

- Browse and manage AI skills + Browse and manage AI capabilities

- - {/* Search and Filter */} -
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
-
- - {categories.map((category) => ( - - ))} -
-
- - {/* Error Display */} - {error && ( - - - {error} + {/* Gateway Warning */} + {!isGatewayRunning && ( + + + + + Gateway is not running. Skills cannot be loaded without an active Gateway. + )} - {/* Skills Grid */} - {filteredSkills.length === 0 ? ( - - - -

No skills found

-

- {searchQuery ? 'Try a different search term' : 'No skills available'} -

-
-
- ) : ( -
- {filteredSkills.map((skill) => ( - - -
-
- {skill.icon || '🔧'} -
- - {skill.name} - {skill.isCore && ( - - )} - - - {categoryLabels[skill.category]} - -
-
- handleToggle(skill.id, skill.enabled)} - disabled={skill.isCore} - /> -
-
- -

- {skill.description} -

- {skill.version && ( - - v{skill.version} - - )} + {/* Tabs */} + + + + + All Skills + + + + Bundles + + + + + {/* Search and Filter */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+
+ + {Object.entries(categoryStats).map(([category, count]) => ( + + ))} +
+
+ + {/* Error Display */} + {error && ( + + + + {error} - ))} -
- )} + )} + + {/* Skills Grid */} + {filteredSkills.length === 0 ? ( + + + +

No skills found

+

+ {searchQuery ? 'Try a different search term' : 'No skills available'} +

+
+
+ ) : ( +
+ {filteredSkills.map((skill) => ( + setSelectedSkill(skill)} + > + +
+
+ {skill.icon || categoryIcons[skill.category]} +
+ + {skill.name} + {skill.isCore && ( + + )} + + + {categoryLabels[skill.category]} + +
+
+ { + handleToggle(skill.id, checked); + }} + disabled={skill.isCore} + onClick={(e) => e.stopPropagation()} + /> +
+
+ +

+ {skill.description} +

+
+ {skill.version && ( + + v{skill.version} + + )} + {skill.configurable && ( + + + Configurable + + )} +
+
+
+ ))} +
+ )} + + + +

+ Skill bundles are pre-configured collections of skills for common use cases. + Enable a bundle to quickly set up multiple related skills at once. +

+ +
+ {skillBundles.map((bundle) => ( + handleBundleApply(bundle)} + /> + ))} +
+
+ {/* Statistics */}
- - {skills.filter((s) => s.enabled).length} of {skills.length} skills enabled - - - {skills.filter((s) => s.isCore).length} core skills - +
+ + + {skills.filter((s) => s.enabled).length} + + {' '}of {skills.length} skills enabled + + + + {skills.filter((s) => s.isCore).length} + + {' '}core skills + +
+
+ + {/* Skill Detail Dialog */} + {selectedSkill && ( + setSelectedSkill(null)} + onToggle={(enabled) => { + handleToggle(selectedSkill.id, enabled); + setSelectedSkill({ ...selectedSkill, enabled }); + }} + /> + )} ); }