refactor(skills): add skill-adapter and refactor SkillMarket
- Add skill-adapter.ts to bridge configStore and UI skill formats - Refactor SkillMarket to use new skill-adapter instead of skill-discovery - Add health check state to connectionStore - Update multiple components with improved typing - Clean up test artifacts and add new test results - Update README and add skill-market-mvp plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,33 +11,30 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Search,
|
||||
Package,
|
||||
Check,
|
||||
Plus,
|
||||
Minus,
|
||||
Sparkles,
|
||||
Tag,
|
||||
Layers,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { useConfigStore, type SkillInfo } from '../store/configStore';
|
||||
import {
|
||||
SkillDiscoveryEngine,
|
||||
type SkillInfo,
|
||||
type SkillSuggestion,
|
||||
} from '../lib/skill-discovery';
|
||||
adaptSkillsCatalog,
|
||||
type SkillDisplay,
|
||||
} from '../lib/skill-adapter';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface SkillMarketProps {
|
||||
className?: string;
|
||||
onSkillInstall?: (skill: SkillInfo) => void;
|
||||
onSkillUninstall?: (skill: SkillInfo) => void;
|
||||
onSkillInstall?: (skill: SkillDisplay) => void;
|
||||
onSkillUninstall?: (skill: SkillDisplay) => void;
|
||||
}
|
||||
|
||||
type CategoryFilter = 'all' | 'development' | 'security' | 'analytics' | 'content' | 'ops' | 'management' | 'testing' | 'business' | 'marketing';
|
||||
@@ -80,7 +77,7 @@ function SkillCard({
|
||||
onInstall,
|
||||
onUninstall,
|
||||
}: {
|
||||
skill: SkillInfo;
|
||||
skill: SkillDisplay;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
onInstall: () => void;
|
||||
@@ -240,35 +237,6 @@ function SkillCard({
|
||||
);
|
||||
}
|
||||
|
||||
function SuggestionCard({ suggestion }: { suggestion: SkillSuggestion }) {
|
||||
const confidencePercent = Math.round(suggestion.confidence * 100);
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{suggestion.skill.name}
|
||||
</span>
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 ml-auto">
|
||||
{confidencePercent}% 匹配
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 mb-2">{suggestion.reason}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{suggestion.matchedPatterns.map((pattern) => (
|
||||
<span
|
||||
key={pattern}
|
||||
className="px-1.5 py-0.5 text-xs bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded"
|
||||
>
|
||||
{pattern}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// === Main Component ===
|
||||
|
||||
export function SkillMarket({
|
||||
@@ -276,19 +244,23 @@ export function SkillMarket({
|
||||
onSkillInstall,
|
||||
onSkillUninstall,
|
||||
}: SkillMarketProps) {
|
||||
const [engine] = useState(() => new SkillDiscoveryEngine());
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
// Use configStore instead of SkillDiscoveryEngine
|
||||
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
|
||||
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
|
||||
const updateSkill = useConfigStore((s) => s.updateSkill);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<CategoryFilter>('all');
|
||||
const [expandedSkillId, setExpandedSkillId] = useState<string | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<SkillSuggestion[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// Load skills
|
||||
// Adapt skills to display format
|
||||
const skills = useMemo(() => adaptSkillsCatalog(skillsCatalog), [skillsCatalog]);
|
||||
|
||||
// Load skills on mount
|
||||
useEffect(() => {
|
||||
const allSkills = engine.getAllSkills();
|
||||
setSkills(allSkills);
|
||||
}, [engine]);
|
||||
loadSkillsCatalog();
|
||||
}, [loadSkillsCatalog]);
|
||||
|
||||
// Filter skills
|
||||
const filteredSkills = useMemo(() => {
|
||||
@@ -301,13 +273,17 @@ export function SkillMarket({
|
||||
|
||||
// Search filter
|
||||
if (searchQuery.trim()) {
|
||||
const searchResult = engine.searchSkills(searchQuery);
|
||||
const matchingIds = new Set(searchResult.results.map((s) => s.id));
|
||||
result = result.filter((s) => matchingIds.has(s.id));
|
||||
const queryLower = searchQuery.toLowerCase();
|
||||
result = result.filter((s) =>
|
||||
s.name.toLowerCase().includes(queryLower) ||
|
||||
s.description.toLowerCase().includes(queryLower) ||
|
||||
s.triggers.some((t) => t.toLowerCase().includes(queryLower)) ||
|
||||
s.capabilities.some((c) => c.toLowerCase().includes(queryLower))
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [skills, categoryFilter, searchQuery, engine]);
|
||||
}, [skills, categoryFilter, searchQuery]);
|
||||
|
||||
// Get categories from skills
|
||||
const categories = useMemo(() => {
|
||||
@@ -323,44 +299,31 @@ export function SkillMarket({
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// engine.refreshIndex doesn't exist - skip
|
||||
setSkills(engine.getAllSkills());
|
||||
await loadSkillsCatalog();
|
||||
setIsRefreshing(false);
|
||||
}, [engine]);
|
||||
}, [loadSkillsCatalog]);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
(skill: SkillInfo) => {
|
||||
// Install skill - update local state
|
||||
setSkills((prev) => prev.map(s => ({ ...s, installed: true })));
|
||||
onSkillInstall?.(skill);
|
||||
},
|
||||
[onSkillInstall]
|
||||
async (skill: SkillDisplay) => {
|
||||
// Update skill via configStore (persists to backend)
|
||||
await updateSkill(skill.id, { enabled: true });
|
||||
onSkillInstall?.(skill);
|
||||
},
|
||||
[updateSkill, onSkillInstall]
|
||||
);
|
||||
|
||||
const handleUninstall = useCallback(
|
||||
(skill: SkillInfo) => {
|
||||
// Uninstall skill - update local state
|
||||
setSkills((prev) => prev.map(s => ({ ...s, installed: false })));
|
||||
onSkillUninstall?.(skill);
|
||||
async (skill: SkillDisplay) => {
|
||||
// Update skill via configStore (persists to backend)
|
||||
await updateSkill(skill.id, { enabled: false });
|
||||
onSkillUninstall?.(skill);
|
||||
},
|
||||
[onSkillUninstall]
|
||||
[updateSkill, onSkillUninstall]
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
// Get suggestions based on search
|
||||
const mockConversation = [{ role: 'user' as const, content: query }];
|
||||
const newSuggestions = await engine.suggestSkills(mockConversation, 'default', 3);
|
||||
setSuggestions(newSuggestions.slice(0, 3));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
},
|
||||
[engine]
|
||||
);
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full ${className}`}>
|
||||
@@ -405,25 +368,8 @@ export function SkillMarket({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
<AnimatePresence>
|
||||
{suggestions.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="mt-3 space-y-2"
|
||||
>
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<Info className="w-3 h-3" />
|
||||
推荐技能
|
||||
</h4>
|
||||
{suggestions.map((suggestion) => (
|
||||
<SuggestionCard key={suggestion.skill.id} suggestion={suggestion} />
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Suggestions - placeholder for future AI-powered recommendations */}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
|
||||
Reference in New Issue
Block a user