From 85e39ecafd7f026f2586c0d28cb534f1c40eb313 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 16 Mar 2026 10:24:00 +0800 Subject: [PATCH] feat(l4): add Phase 1 UI components for self-evolution capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwarmDashboard (多 Agent 协作面板): - Task list with real-time status updates - Subtask visualization with results - Communication style indicators (Sequential/Parallel/Debate) - Task creation form with manual triggers SkillMarket (技能市场): - Browse 12 built-in skills by category - Keyword/capability search - Skill details with triggers and capabilities - Install/uninstall with L4 autonomy hooks HeartbeatConfig (心跳配置): - Enable/disable periodic proactive checks - Interval slider (5-120 minutes) - Proactivity level selector (Silent/Light/Standard/Autonomous) - Quiet hours configuration - Built-in check item toggles ReflectionLog (反思日志): - Reflection history with pattern analysis - Improvement suggestions by priority - Identity change proposal approval workflow - Manual reflection trigger - Config panel for trigger settings Part of ZCLAW L4 Self-Evolution capability. Co-Authored-By: Claude Opus 4.6 --- desktop/src/components/HeartbeatConfig.tsx | 527 ++++++++++++++++++ desktop/src/components/ReflectionLog.tsx | 608 +++++++++++++++++++++ desktop/src/components/SkillMarket.tsx | 473 ++++++++++++++++ desktop/src/components/SwarmDashboard.tsx | 591 ++++++++++++++++++++ 4 files changed, 2199 insertions(+) create mode 100644 desktop/src/components/HeartbeatConfig.tsx create mode 100644 desktop/src/components/ReflectionLog.tsx create mode 100644 desktop/src/components/SkillMarket.tsx create mode 100644 desktop/src/components/SwarmDashboard.tsx diff --git a/desktop/src/components/HeartbeatConfig.tsx b/desktop/src/components/HeartbeatConfig.tsx new file mode 100644 index 0000000..f16ec49 --- /dev/null +++ b/desktop/src/components/HeartbeatConfig.tsx @@ -0,0 +1,527 @@ +/** + * HeartbeatConfig - Configuration UI for periodic proactive checks + * + * Allows users to configure: + * - Heartbeat interval (default 30 minutes) + * - Enable/disable built-in check items + * - Quiet hours (no notifications during sleep time) + * - Proactivity level (silent/light/standard/autonomous) + * + * Part of ZCLAW L4 Self-Evolution capability. + */ + +import { useState, useCallback, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Heart, + Settings, + Clock, + Moon, + Sun, + Volume2, + VolumeX, + AlertTriangle, + CheckCircle, + Info, + RefreshCw, +} from 'lucide-react'; +import { + HeartbeatEngine, + DEFAULT_HEARTBEAT_CONFIG, + type HeartbeatConfig as HeartbeatConfigType, + type HeartbeatResult, +} from '../lib/heartbeat-engine'; + +// === Types === + +interface HeartbeatConfigProps { + className?: string; + onConfigChange?: (config: HeartbeatConfigType) => void; +} + +type ProactivityLevel = 'silent' | 'light' | 'standard' | 'autonomous'; + +// === Proactivity Level Config === + +const PROACTIVITY_CONFIG: Record = { + silent: { + label: '静默', + description: '从不主动推送,仅被动响应', + icon: VolumeX, + }, + light: { + label: '轻度', + description: '仅紧急事项推送(如定时任务完成)', + icon: Volume2, + }, + standard: { + label: '标准', + description: '定期巡检 + 任务通知 + 建议推送', + icon: AlertTriangle, + }, + autonomous: { + label: '自主', + description: 'Agent 自行判断何时推送', + icon: Heart, + }, +}; + +// === Check Item Config === + +interface CheckItemConfig { + id: string; + name: string; + description: string; + enabled: boolean; +} + +const BUILT_IN_CHECKS: CheckItemConfig[] = [ + { + id: 'pending-tasks', + name: '待办任务检查', + description: '检查是否有未完成的任务需要跟进', + enabled: true, + }, + { + id: 'memory-health', + name: '记忆健康检查', + description: '检查记忆存储是否过大需要清理', + enabled: true, + }, + { + id: 'idle-greeting', + name: '空闲问候', + description: '长时间未使用时发送简短问候', + enabled: false, + }, +]; + +// === Components === + +function ProactivityLevelSelector({ + value, + onChange, +}: { + value: ProactivityLevel; + onChange: (level: ProactivityLevel) => void; +}) { + return ( +
+ {(Object.keys(PROACTIVITY_CONFIG) as ProactivityLevel[]).map((level) => { + const config = PROACTIVITY_CONFIG[level]; + const Icon = config.icon; + const isSelected = value === level; + + return ( + + ); + })} +
+ ); +} + +function QuietHoursConfig({ + start, + end, + onStartChange, + onEndChange, + enabled, + onToggle, +}: { + start?: string; + end?: string; + onStartChange: (time: string) => void; + onEndChange: (time: string) => void; + enabled: boolean; + onToggle: (enabled: boolean) => void; +}) { + return ( +
+
+
+ + 免打扰时段 +
+ +
+ + + {enabled && ( + +
+ + onEndChange(e.target.value)} + className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" + /> +
+ +
+ + onStartChange(e.target.value)} + className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" + /> +
+
+ )} +
+
+ ); +} + +function CheckItemToggle({ + item, + onToggle, +}: { + item: CheckItemConfig; + onToggle: (enabled: boolean) => void; +}) { + return ( +
+
+
+ {item.name} +
+
+ {item.description} +
+
+ +
+ ); +} + +// === Main Component === + +export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatConfigProps) { + const [config, setConfig] = useState(DEFAULT_HEARTBEAT_CONFIG); + const [checkItems, setCheckItems] = useState(BUILT_IN_CHECKS); + const [lastResult, setLastResult] = useState(null); + const [isTesting, setIsTesting] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + // Load saved config + useEffect(() => { + const saved = localStorage.getItem('zclaw-heartbeat-config'); + if (saved) { + try { + const parsed = JSON.parse(saved); + setConfig({ ...DEFAULT_HEARTBEAT_CONFIG, ...parsed }); + } catch { + // Use defaults + } + } + + const savedChecks = localStorage.getItem('zclaw-heartbeat-checks'); + if (savedChecks) { + try { + setCheckItems(JSON.parse(savedChecks)); + } catch { + // Use defaults + } + } + }, []); + + const updateConfig = useCallback( + (updates: Partial) => { + setConfig((prev) => { + const next = { ...prev, ...updates }; + setHasChanges(true); + onConfigChange?.(next); + return next; + }); + }, + [onConfigChange] + ); + + const toggleCheckItem = useCallback((id: string, enabled: boolean) => { + setCheckItems((prev) => { + const next = prev.map((item) => + item.id === id ? { ...item, enabled } : item + ); + setHasChanges(true); + return next; + }); + }, []); + + const handleSave = useCallback(() => { + localStorage.setItem('zclaw-heartbeat-config', JSON.stringify(config)); + localStorage.setItem('zclaw-heartbeat-checks', JSON.stringify(checkItems)); + setHasChanges(false); + }, [config, checkItems]); + + const handleTestHeartbeat = useCallback(async () => { + setIsTesting(true); + try { + const engine = new HeartbeatEngine('zclaw-main', config); + const result = await engine.tick(); + setLastResult(result); + } catch (error) { + console.error('[HeartbeatConfig] Test failed:', error); + } finally { + setIsTesting(false); + } + }, [config]); + + return ( +
+ {/* Header */} +
+
+ +

心跳配置

+
+
+ + +
+
+ + {/* Content */} +
+ {/* Enable Toggle */} +
+
+
+ +
+
+
+ 启用主动巡检 +
+
+ Agent 将定期检查并主动推送通知 +
+
+
+ +
+ + + {config.enabled && ( + + {/* Interval */} +
+
+ + + 巡检间隔 + +
+
+ updateConfig({ intervalMinutes: parseInt(e.target.value) })} + className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-pink-500" + /> + + {config.intervalMinutes} 分钟 + +
+
+ + {/* Proactivity Level */} +
+
+ + + 主动性级别 + +
+
+ updateConfig({ proactivityLevel: level })} + /> +
+
+ + {/* Quiet Hours */} +
+ updateConfig({ quietHoursStart: time })} + onEndChange={(time) => updateConfig({ quietHoursEnd: time })} + onToggle={(enabled) => + updateConfig({ + quietHoursStart: enabled ? '22:00' : undefined, + quietHoursEnd: enabled ? '08:00' : undefined, + }) + } + /> +
+ + {/* Check Items */} +
+
+ + + 检查项目 + +
+
+ {checkItems.map((item) => ( + toggleCheckItem(item.id, enabled)} + /> + ))} +
+
+ + {/* Last Result */} + {lastResult && ( +
+
+ {lastResult.status === 'ok' ? ( + + ) : ( + + )} + + 上次测试结果 + +
+
+ 检查了 {lastResult.checkedItems} 项 + {lastResult.alerts.length > 0 && ` · ${lastResult.alerts.length} 个提醒`} +
+ {lastResult.alerts.length > 0 && ( +
+ {lastResult.alerts.map((alert, i) => ( +
+ {alert.title}: {alert.content} +
+ ))} +
+ )} +
+ )} +
+ )} +
+ + {/* Info */} +
+ +

+ 心跳机制让 Agent 具备主动意识,能够定期检查任务状态、记忆健康度等,并根据主动性级别推送通知。 + 在"自主"模式下,Agent 将自行判断是否需要通知你。 +

+
+
+
+ ); +} + +export default HeartbeatConfig; diff --git a/desktop/src/components/ReflectionLog.tsx b/desktop/src/components/ReflectionLog.tsx new file mode 100644 index 0000000..d50517a --- /dev/null +++ b/desktop/src/components/ReflectionLog.tsx @@ -0,0 +1,608 @@ +/** + * ReflectionLog - Self-reflection history and identity change approval UI + * + * Displays: + * - Reflection history (patterns, improvements) + * - Pending identity change proposals + * - Approve/reject identity modifications + * - Manual reflection trigger + * + * Part of ZCLAW L4 Self-Evolution capability. + */ + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Brain, + Sparkles, + Check, + X, + Clock, + ChevronDown, + ChevronRight, + RefreshCw, + AlertTriangle, + TrendingUp, + TrendingDown, + Minus, + FileText, + History, + Play, + Settings, +} from 'lucide-react'; +import { + ReflectionEngine, + type ReflectionResult, + type PatternObservation, + type ImprovementSuggestion, + type ReflectionConfig, + DEFAULT_REFLECTION_CONFIG, +} from '../lib/reflection-engine'; +import { getAgentIdentityManager, type IdentityChangeProposal } from '../lib/agent-identity'; + +// === Types === + +interface ReflectionLogProps { + className?: string; + agentId?: string; + onProposalApprove?: (proposal: IdentityChangeProposal) => void; + onProposalReject?: (proposal: IdentityChangeProposal) => void; +} + +// === Sentiment Config === + +const SENTIMENT_CONFIG: Record = { + positive: { + icon: TrendingUp, + color: 'text-green-600 dark:text-green-400', + bgColor: 'bg-green-100 dark:bg-green-900/30', + }, + negative: { + icon: TrendingDown, + color: 'text-red-600 dark:text-red-400', + bgColor: 'bg-red-100 dark:bg-red-900/30', + }, + neutral: { + icon: Minus, + color: 'text-gray-600 dark:text-gray-400', + bgColor: 'bg-gray-100 dark:bg-gray-800', + }, +}; + +// === Priority Config === + +const PRIORITY_CONFIG: Record = { + high: { + color: 'text-red-600 dark:text-red-400', + bgColor: 'bg-red-100 dark:bg-red-900/30', + }, + medium: { + color: 'text-yellow-600 dark:text-yellow-400', + bgColor: 'bg-yellow-100 dark:bg-yellow-900/30', + }, + low: { + color: 'text-blue-600 dark:text-blue-400', + bgColor: 'bg-blue-100 dark:bg-blue-900/30', + }, +}; + +// === Components === + +function SentimentBadge({ sentiment }: { sentiment: string }) { + const config = SENTIMENT_CONFIG[sentiment] || SENTIMENT_CONFIG.neutral; + const Icon = config.icon; + return ( + + + {sentiment === 'positive' ? '积极' : sentiment === 'negative' ? '消极' : '中性'} + + ); +} + +function PriorityBadge({ priority }: { priority: string }) { + const config = PRIORITY_CONFIG[priority] || PRIORITY_CONFIG.medium; + return ( + + {priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'} + + ); +} + +function PatternCard({ pattern }: { pattern: PatternObservation }) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + + + {expanded && pattern.evidence.length > 0 && ( + +
证据
+
    + {pattern.evidence.map((ev, i) => ( +
  • + {ev} +
  • + ))} +
+
+ )} +
+
+ ); +} + +function ImprovementCard({ improvement }: { improvement: ImprovementSuggestion }) { + return ( +
+
+
+ + {improvement.area} + + +
+

{improvement.suggestion}

+
+
+ ); +} + +function ProposalCard({ + proposal, + onApprove, + onReject, +}: { + proposal: IdentityChangeProposal; + onApprove: () => void; + onReject: () => void; +}) { + const [expanded, setExpanded] = useState(false); + const identityManager = getAgentIdentityManager(); + + const fileName = proposal.filePath.split('/').pop() || proposal.filePath; + const fileType = fileName.toLowerCase().replace('.md', '').toUpperCase(); + + return ( +
+
+
+ +
+
+
+ + {fileType} 变更提议 + + + 待审批 + +
+

+ {proposal.reason} +

+
+ +
+ + + {expanded && ( + +
+
+
+ 当前内容 +
+
+                  {proposal.currentContent.slice(0, 500)}
+                  {proposal.currentContent.length > 500 && '...'}
+                
+
+
+
+ 建议内容 +
+
+                  {proposal.proposedContent.slice(0, 500)}
+                  {proposal.proposedContent.length > 500 && '...'}
+                
+
+
+
+ )} +
+ +
+ + +
+
+ ); +} + +function ReflectionEntry({ + result, + isExpanded, + onToggle, +}: { + result: ReflectionResult; + isExpanded: boolean; + onToggle: () => void; +}) { + const positivePatterns = result.patterns.filter((p) => p.sentiment === 'positive').length; + const negativePatterns = result.patterns.filter((p) => p.sentiment === 'negative').length; + const highPriorityImprovements = result.improvements.filter((i) => i.priority === 'high').length; + + return ( +
+ + + + {isExpanded && ( + +
+ {/* Patterns */} + {result.patterns.length > 0 && ( +
+

+ 行为模式 +

+
+ {result.patterns.map((pattern, i) => ( + + ))} +
+
+ )} + + {/* Improvements */} + {result.improvements.length > 0 && ( +
+

+ 改进建议 +

+
+ {result.improvements.map((improvement, i) => ( + + ))} +
+
+ )} + + {/* Meta */} +
+ 新增记忆: {result.newMemories} + 身份变更提议: {result.identityProposals.length} +
+
+
+ )} +
+
+ ); +} + +// === Main Component === + +export function ReflectionLog({ + className = '', + agentId = 'zclaw-main', + onProposalApprove, + onProposalReject, +}: ReflectionLogProps) { + const [engine] = useState(() => new ReflectionEngine()); + const [history, setHistory] = useState([]); + const [pendingProposals, setPendingProposals] = useState([]); + const [expandedId, setExpandedId] = useState(null); + const [isReflecting, setIsReflecting] = useState(false); + const [config, setConfig] = useState(DEFAULT_REFLECTION_CONFIG); + const [showConfig, setShowConfig] = useState(false); + + // Load history and pending proposals + useEffect(() => { + const loadedHistory = engine.getHistory(); + setHistory([...loadedHistory].reverse()); // Most recent first + + const identityManager = getAgentIdentityManager(); + const proposals = identityManager.getPendingProposals(agentId); + setPendingProposals(proposals); + }, [engine, agentId]); + + const handleReflect = useCallback(async () => { + setIsReflecting(true); + try { + const result = await engine.reflect(agentId); + setHistory((prev) => [result, ...prev]); + + // Update pending proposals + if (result.identityProposals.length > 0) { + setPendingProposals((prev) => [...prev, ...result.identityProposals]); + } + } catch (error) { + console.error('[ReflectionLog] Reflection failed:', error); + } finally { + setIsReflecting(false); + } + }, [engine, agentId]); + + const handleApproveProposal = useCallback( + (proposal: IdentityChangeProposal) => { + const identityManager = getAgentIdentityManager(); + identityManager.approveChange(proposal.id); + setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id)); + onProposalApprove?.(proposal); + }, + [onProposalApprove] + ); + + const handleRejectProposal = useCallback( + (proposal: IdentityChangeProposal) => { + const identityManager = getAgentIdentityManager(); + identityManager.rejectChange(proposal.id); + setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id)); + onProposalReject?.(proposal); + }, + [onProposalReject] + ); + + const stats = useMemo(() => { + const totalReflections = history.length; + const totalPatterns = history.reduce((sum, r) => sum + r.patterns.length, 0); + const totalImprovements = history.reduce((sum, r) => sum + r.improvements.length, 0); + const totalIdentityChanges = history.reduce((sum, r) => sum + r.identityProposals.length, 0); + return { totalReflections, totalPatterns, totalImprovements, totalIdentityChanges }; + }, [history]); + + return ( +
+ {/* Header */} +
+
+ +

反思日志

+
+
+ + +
+
+ + {/* Stats Bar */} +
+ + 反思: {stats.totalReflections} + + + 模式: {stats.totalPatterns} + + + 建议: {stats.totalImprovements} + + + 变更: {stats.totalIdentityChanges} + +
+ + {/* Config Panel */} + + {showConfig && ( + +
+
+ 对话后触发反思 + + setConfig((prev) => ({ ...prev, triggerAfterConversations: parseInt(e.target.value) || 5 })) + } + className="w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" + /> +
+
+ 允许修改 SOUL.md + +
+
+ 变更需审批 + +
+
+
+ )} +
+ + {/* Content */} +
+ {/* Pending Proposals */} + {pendingProposals.length > 0 && ( +
+

+ + 待审批变更 ({pendingProposals.length}) +

+ {pendingProposals.map((proposal) => ( + handleApproveProposal(proposal)} + onReject={() => handleRejectProposal(proposal)} + /> + ))} +
+ )} + + {/* History */} +
+

+ + 反思历史 +

+ + {history.length === 0 ? ( +
+ +

暂无反思记录

+ +
+ ) : ( + history.map((result, i) => ( + setExpandedId((prev) => (prev === result.timestamp ? null : result.timestamp))} + /> + )) + )} +
+
+
+ ); +} + +export default ReflectionLog; diff --git a/desktop/src/components/SkillMarket.tsx b/desktop/src/components/SkillMarket.tsx new file mode 100644 index 0000000..f0127f1 --- /dev/null +++ b/desktop/src/components/SkillMarket.tsx @@ -0,0 +1,473 @@ +/** + * SkillMarket - Skill browsing, search, and management UI + * + * Displays available skills (12 built-in + custom), allows users to: + * - Browse skills by category + * - Search skills by keyword/capability + * - View skill details and capabilities + * - Install/uninstall skills (with L4 autonomy integration) + * + * Part of ZCLAW L4 Self-Evolution capability. + */ + +import { useState, useEffect, useMemo, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Search, + Package, + Check, + X, + Plus, + Minus, + Sparkles, + Tag, + Layers, + ChevronDown, + ChevronRight, + RefreshCw, + Info, +} from 'lucide-react'; +import { + SkillDiscoveryEngine, + type SkillInfo, + type SkillSuggestion, +} from '../lib/skill-discovery'; + +// === Types === + +interface SkillMarketProps { + className?: string; + onSkillInstall?: (skill: SkillInfo) => void; + onSkillUninstall?: (skill: SkillInfo) => void; +} + +type CategoryFilter = 'all' | 'development' | 'security' | 'analytics' | 'content' | 'ops' | 'management' | 'testing' | 'business' | 'marketing'; + +// === Category Config === + +const CATEGORY_CONFIG: Record = { + development: { label: '开发', color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/30' }, + security: { label: '安全', color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-900/30' }, + analytics: { label: '分析', color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30' }, + content: { label: '内容', color: 'text-pink-600 dark:text-pink-400', bgColor: 'bg-pink-100 dark:bg-pink-900/30' }, + ops: { label: '运维', color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-100 dark:bg-orange-900/30' }, + management: { label: '管理', color: 'text-cyan-600 dark:text-cyan-400', bgColor: 'bg-cyan-100 dark:bg-cyan-900/30' }, + testing: { label: '测试', color: 'text-green-600 dark:text-green-400', bgColor: 'bg-green-100 dark:bg-green-900/30' }, + business: { label: '商业', color: 'text-yellow-600 dark:text-yellow-400', bgColor: 'bg-yellow-100 dark:bg-yellow-900/30' }, + marketing: { label: '营销', color: 'text-indigo-600 dark:text-indigo-400', bgColor: 'bg-indigo-100 dark:bg-indigo-900/30' }, +}; + +// === Components === + +function CategoryBadge({ category }: { category?: string }) { + if (!category) return null; + const config = CATEGORY_CONFIG[category] || { + label: category, + color: 'text-gray-600 dark:text-gray-400', + bgColor: 'bg-gray-100 dark:bg-gray-800', + }; + return ( + + + {config.label} + + ); +} + +function SkillCard({ + skill, + isExpanded, + onToggle, + onInstall, + onUninstall, +}: { + skill: SkillInfo; + isExpanded: boolean; + onToggle: () => void; + onInstall: () => void; + onUninstall: () => void; +}) { + const config = CATEGORY_CONFIG[skill.category || ''] || CATEGORY_CONFIG.development; + + return ( +
+ + + + {isExpanded && ( + +
+ {/* Triggers */} +
+

+ 触发词 +

+
+ {skill.triggers.map((trigger) => ( + + {trigger} + + ))} +
+
+ + {/* Capabilities */} +
+

+ 能力 +

+
+ {skill.capabilities.map((cap) => ( + + {cap} + + ))} +
+
+ + {/* Tool Dependencies */} + {skill.toolDeps.length > 0 && ( +
+

+ 工具依赖 +

+
+ {skill.toolDeps.map((dep) => ( + + {dep} + + ))} +
+
+ )} + + {/* Actions */} +
+ {skill.installed ? ( + + ) : ( + + )} +
+
+
+ )} +
+
+ ); +} + +function SuggestionCard({ suggestion }: { suggestion: SkillSuggestion }) { + const confidencePercent = Math.round(suggestion.confidence * 100); + + return ( +
+
+ + + {suggestion.skill.name} + + + {confidencePercent}% 匹配 + +
+

{suggestion.reason}

+
+ {suggestion.matchedPatterns.map((pattern) => ( + + {pattern} + + ))} +
+
+ ); +} + +// === Main Component === + +export function SkillMarket({ + className = '', + onSkillInstall, + onSkillUninstall, +}: SkillMarketProps) { + const [engine] = useState(() => new SkillDiscoveryEngine()); + const [skills, setSkills] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('all'); + const [expandedSkillId, setExpandedSkillId] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); + + // Load skills + useEffect(() => { + const allSkills = engine.getAllSkills(); + setSkills(allSkills); + }, [engine]); + + // Filter skills + const filteredSkills = useMemo(() => { + let result = skills; + + // Category filter + if (categoryFilter !== 'all') { + result = result.filter((s) => s.category === categoryFilter); + } + + // 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)); + } + + return result; + }, [skills, categoryFilter, searchQuery, engine]); + + // Get categories from skills + const categories = useMemo(() => { + const cats = new Set(skills.map((s) => s.category).filter(Boolean)); + return ['all', ...Array.from(cats)] as CategoryFilter[]; + }, [skills]); + + // Stats + const stats = useMemo(() => { + const installed = skills.filter((s) => s.installed).length; + return { total: skills.length, installed }; + }, [skills]); + + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + await new Promise((resolve) => setTimeout(resolve, 500)); + engine.refreshIndex(); + setSkills(engine.getAllSkills()); + setIsRefreshing(false); + }, [engine]); + + const handleInstall = useCallback( + (skill: SkillInfo) => { + engine.installSkill(skill.id); + setSkills(engine.getAllSkills()); + onSkillInstall?.(skill); + }, + [engine, onSkillInstall] + ); + + const handleUninstall = useCallback( + (skill: SkillInfo) => { + engine.uninstallSkill(skill.id); + setSkills(engine.getAllSkills()); + onSkillUninstall?.(skill); + }, + [engine, onSkillUninstall] + ); + + const handleSearch = useCallback( + (query: string) => { + setSearchQuery(query); + if (query.trim()) { + // Get suggestions based on search + const mockConversation = [{ role: 'user' as const, content: query }]; + const newSuggestions = engine.suggestSkills(mockConversation); + setSuggestions(newSuggestions.slice(0, 3)); + } else { + setSuggestions([]); + } + }, + [engine] + ); + + return ( +
+ {/* Header */} +
+
+ +

技能市场

+
+
+ +
+
+ + {/* Stats Bar */} +
+ + 总计: {stats.total} + + + 已安装: {stats.installed} + +
+ + {/* Search */} +
+
+ + handleSearch(e.target.value)} + placeholder="搜索技能、能力、触发词..." + className="w-full pl-9 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm" + /> +
+ + {/* Suggestions */} + + {suggestions.length > 0 && ( + +

+ + 推荐技能 +

+ {suggestions.map((suggestion) => ( + + ))} +
+ )} +
+
+ + {/* Category Filter */} +
+ {categories.map((cat) => ( + + ))} +
+ + {/* Skill List */} +
+ {filteredSkills.length === 0 ? ( +
+ +

+ {searchQuery ? '未找到匹配的技能' : '暂无技能'} +

+
+ ) : ( + filteredSkills.map((skill) => ( + setExpandedSkillId((prev) => (prev === skill.id ? null : skill.id))} + onInstall={() => handleInstall(skill)} + onUninstall={() => handleUninstall(skill)} + /> + )) + )} +
+
+ ); +} + +export default SkillMarket; diff --git a/desktop/src/components/SwarmDashboard.tsx b/desktop/src/components/SwarmDashboard.tsx new file mode 100644 index 0000000..3cdf186 --- /dev/null +++ b/desktop/src/components/SwarmDashboard.tsx @@ -0,0 +1,591 @@ +/** + * SwarmDashboard - Multi-Agent Collaboration Task Dashboard + * + * Visualizes swarm tasks, multi-agent collaboration) with real-time + * status updates, task history, and manual trigger functionality. + * + * Part of ZCLAW L4 Self-Evolution capability. + */ + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Users, + Play, + Pause, + CheckCircle, + XCircle, + Clock, + ChevronDown, + ChevronRight, + Layers, + GitBranch, + MessageSquare, + RefreshCw, + Plus, + History, + Sparkles, +} from 'lucide-react'; +import { + AgentSwarm, + type SwarmTask, + type Subtask, + type SwarmTaskStatus, + type CommunicationStyle, +} from '../lib/agent-swarm'; +import { useAgentStore } from '../store/agentStore'; + +// === Types === + +interface SwarmDashboardProps { + className?: string; + onTaskSelect?: (task: SwarmTask) => void; +} + +type FilterType = 'all' | 'active' | 'completed' | 'failed'; + +// === Status Config === + +const TASK_STATUS_CONFIG: Record = { + planning: { + label: '规划中', + className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + dotClass: 'bg-purple-500', + icon: Layers, + }, + executing: { + label: '执行中', + className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + dotClass: 'bg-blue-500 animate-pulse', + icon: Play, + }, + aggregating: { + label: '汇总中', + className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400', + dotClass: 'bg-cyan-500 animate-pulse', + icon: RefreshCw, + }, + done: { + label: '已完成', + className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', + dotClass: 'bg-green-500', + icon: CheckCircle, + }, + failed: { + label: '失败', + className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', + dotClass: 'bg-red-500', + icon: XCircle, + }, +}; + +const SUBTASK_STATUS_CONFIG: Record = { + pending: { label: '待执行', dotClass: 'bg-gray-400' }, + running: { label: '执行中', dotClass: 'bg-blue-500 animate-pulse' }, + done: { label: '完成', dotClass: 'bg-green-500' }, + failed: { label: '失败', dotClass: 'bg-red-500' }, +}; + +const COMMUNICATION_STYLE_CONFIG: Record = { + sequential: { + label: '顺序执行', + icon: GitBranch, + description: '每个 Agent 依次处理,输出传递给下一个', + }, + parallel: { + label: '并行执行', + icon: Layers, + description: '多个 Agent 同时处理不同子任务', + }, + debate: { + label: '辩论模式', + icon: MessageSquare, + description: '多个 Agent 提供观点,协调者综合', + }, +}; + +// === Components === + +function TaskStatusBadge({ status }: { status: SwarmTaskStatus }) { + const config = TASK_STATUS_CONFIG[status]; + const Icon = config.icon; + return ( + + + {config.label} + + ); +} + +function SubtaskStatusDot({ status }: { status: string }) { + const config = SUBTASK_STATUS_CONFIG[status] || SUBTASK_STATUS_CONFIG.pending; + return ; +} + +function CommunicationStyleBadge({ style }: { style: CommunicationStyle }) { + const config = COMMUNICATION_STYLE_CONFIG[style]; + const Icon = config.icon; + return ( + + + {config.label} + + ); +} + +function SubtaskItem({ + subtask, + agentName, + isExpanded, + onToggle, +}: { + subtask: Subtask; + agentName: string; + isExpanded: boolean; + onToggle: () => void; +}) { + const duration = useMemo(() => { + if (!subtask.startedAt) return null; + const start = new Date(subtask.startedAt).getTime(); + const end = subtask.completedAt ? new Date(subtask.completedAt).getTime() : Date.now(); + return Math.round((end - start) / 1000); + }, [subtask.startedAt, subtask.completedAt]); + + return ( +
+ + + + {isExpanded && subtask.result && ( + +
+ {subtask.result} +
+
+ )} +
+ + {subtask.error && ( +
+

{subtask.error}

+
+ )} +
+ ); +} + +function TaskCard({ + task, + isSelected, + onSelect, +}: { + task: SwarmTask; + isSelected: boolean; + onSelect: () => void; +}) { + const [expandedSubtasks, setExpandedSubtasks] = useState>(new Set()); + const { agents } = useAgentStore(); + + const toggleSubtask = useCallback((subtaskId: string) => { + setExpandedSubtasks((prev) => { + const next = new Set(prev); + if (next.has(subtaskId)) { + next.delete(subtaskId); + } else { + next.add(subtaskId); + } + return next; + }); + }, []); + + const completedCount = task.subtasks.filter((s) => s.status === 'done').length; + const totalDuration = useMemo(() => { + if (!task.completedAt) return null; + const start = new Date(task.createdAt).getTime(); + const end = new Date(task.completedAt).getTime(); + return Math.round((end - start) / 1000); + }, [task.createdAt, task.completedAt]); + + const getAgentName = (agentId: string) => { + const agent = agents.find((a) => a.id === agentId); + return agent?.name || agentId; + }; + + return ( +
+ + + + {isSelected && ( + +
+

+ 子任务 +

+
+ {task.subtasks.map((subtask) => ( + toggleSubtask(subtask.id)} + /> + ))} +
+ + {task.finalResult && ( +
+

+ 最终结果 +

+

+ {task.finalResult} +

+
+ )} +
+
+ )} +
+
+ ); +} + +function CreateTaskForm({ + onSubmit, + onCancel, +}: { + onSubmit: (description: string, style: CommunicationStyle) => void; + onCancel: () => void; +}) { + const [description, setDescription] = useState(''); + const [style, setStyle] = useState('sequential'); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (description.trim()) { + onSubmit(description.trim(), style); + } + }; + + return ( +
+
+ +