/** * HandsPanel - ZCLAW Hands Management UI * * Displays available ZCLAW Hands (autonomous capability packages) * with detailed status, requirements, and activation controls. * * Design based on ZCLAW Dashboard v0.4.0 */ import { useState, useEffect, useCallback } from 'react'; import { useHandStore, type Hand, type HandRequirement } from '../store/handStore'; import { Zap, RefreshCw, ChevronRight, CheckCircle, XCircle, Loader2, AlertTriangle, Settings, Play, Clock } from 'lucide-react'; import { BrowserHandCard } from './BrowserHand'; import type { HandParameter } from '../types/hands'; import { HAND_DEFINITIONS } from '../types/hands'; import { HandParamsForm } from './HandParamsForm'; import { ApprovalsPanel } from './ApprovalsPanel'; import { useToast } from './ui/Toast'; // === Tab Type === type TabType = 'hands' | 'approvals'; // === Status Badge Component === type HandStatus = 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed'; // === Parameter Validation Helper === function validateAllParameters( parameters: HandParameter[], values: Record ): Record { const errors: Record = {}; parameters.forEach(param => { if (param.required) { const value = values[param.name]; if (value === undefined || value === null || value === '') { errors[param.name] = `${param.label} is required`; } } }); return errors; } interface StatusConfig { label: string; className: string; dotClass: string; } const STATUS_CONFIG: Record = { idle: { label: '就绪', className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', dotClass: 'bg-green-500', }, running: { label: '运行中', className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', dotClass: 'bg-blue-500 animate-pulse', }, needs_approval: { label: '待审批', className: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', dotClass: 'bg-yellow-500', }, error: { label: '错误', className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', dotClass: 'bg-red-500', }, unavailable: { label: '不可用', className: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400', dotClass: 'bg-gray-400', }, setup_needed: { label: '需配置', className: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', dotClass: 'bg-orange-500', }, }; function HandStatusBadge({ status }: { status: string }) { const config = STATUS_CONFIG[status as HandStatus] || STATUS_CONFIG.unavailable; return ( {config.label} ); } // === Category Badge Component === const CATEGORY_CONFIG: Record = { productivity: { label: '生产力', className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' }, data: { label: '数据', className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400' }, content: { label: '内容', className: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' }, communication: { label: '通信', className: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' }, }; function CategoryBadge({ category }: { category?: string }) { if (!category) return null; const config = CATEGORY_CONFIG[category] || { label: category, className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400' }; return ( {config.label} ); } // === Requirement Item Component === function RequirementItem({ requirement }: { requirement: HandRequirement }) { return (
{requirement.met ? ( ) : ( )}
{requirement.description} {requirement.details && ( ({requirement.details}) )}
); } // === Hand Details Modal Component === interface HandDetailsModalProps { hand: Hand; isOpen: boolean; onClose: () => void; onActivate: (params?: Record) => void; isActivating: boolean; } function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: HandDetailsModalProps) { // Get Hand parameters from definitions const handDefinition = HAND_DEFINITIONS.find(h => h.id === hand.id); const parameters: HandParameter[] = handDefinition?.parameters || []; // Form state for parameters const [paramValues, setParamValues] = useState>({}); const [paramErrors, setParamErrors] = useState>({}); const [showParamsForm, setShowParamsForm] = useState(false); // Initialize default values useEffect(() => { if (parameters.length > 0) { const defaults: Record = {}; parameters.forEach(p => { if (p.defaultValue !== undefined) { defaults[p.name] = p.defaultValue; } }); setParamValues(defaults); } }, [parameters]); // Reset form when modal opens/closes useEffect(() => { if (isOpen) { setShowParamsForm(false); setParamErrors({}); } }, [isOpen]); const handleActivateClick = useCallback(() => { if (parameters.length > 0 && !showParamsForm) { // Show params form first setShowParamsForm(true); return; } // Validate parameters if showing form if (showParamsForm) { const errors = validateAllParameters(parameters, paramValues); setParamErrors(errors); if (Object.keys(errors).length > 0) { return; } // Pass parameters to onActivate onActivate(paramValues); } else { onActivate(); } }, [parameters, showParamsForm, paramValues, onActivate]); if (!isOpen) return null; const canActivate = hand.status === 'idle' || hand.status === 'setup_needed'; const hasUnmetRequirements = hand.requirements?.some(r => !r.met); return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}
{hand.icon || '🤖'}

{hand.name}

{/* Body */}
{/* Description */}

{hand.description}

{/* Agent Config */} {(hand.provider || hand.model) && (

代理配置

{hand.provider && (
提供商

{hand.provider}

)} {hand.model && (
模型

{hand.model}

)}
)} {/* Requirements */} {hand.requirements && hand.requirements.length > 0 && (

环境要求

{hand.requirements.map((req, idx) => ( ))}
)} {/* Tools */} {hand.tools && hand.tools.length > 0 && (

工具 ({hand.tools.length})

{hand.tools.map((tool, idx) => ( {tool} ))}
)} {/* Parameters Form (shown when activating) */} {showParamsForm && parameters.length > 0 && (

执行参数

)} {/* Dashboard Metrics */} {hand.metrics && hand.metrics.length > 0 && (

仪表盘指标 ({hand.metrics.length})

{hand.metrics.map((metric, idx) => (
{metric}
-
))}
)}
{/* Footer */}
); } // === Hand Card Component === interface HandCardProps { hand: Hand; onDetails: (hand: Hand) => void; onActivate: (hand: Hand, params?: Record) => void; isActivating: boolean; } function HandCard({ hand, onDetails, onActivate, isActivating }: HandCardProps) { const canActivate = hand.status === 'idle'; const hasUnmetRequirements = hand.requirements_met === false; return (
{/* Header */}
{hand.icon || '🤖'}

{hand.name}

{/* Description */}

{hand.description}

{/* Requirements Summary (if any unmet) */} {hasUnmetRequirements && (
部分环境要求未满足
)} {/* Meta Info */}
{hand.toolCount !== undefined && ( {hand.toolCount} 个工具 )} {hand.metricCount !== undefined && ( {hand.metricCount} 个指标 )} {hand.category && ( )}
{/* Actions */}
); } // === Main HandsPanel Component === export function HandsPanel() { const { hands, loadHands, triggerHand, isLoading, error: storeError, getHandDetails } = useHandStore(); const [selectedHand, setSelectedHand] = useState(null); const [activatingHandId, setActivatingHandId] = useState(null); const [showModal, setShowModal] = useState(false); const [activeTab, setActiveTab] = useState('hands'); const { toast } = useToast(); useEffect(() => { loadHands(); }, [loadHands]); const handleDetails = useCallback(async (hand: Hand) => { // Load full details before showing modal const details = await getHandDetails(hand.id); setSelectedHand(details || hand); setShowModal(true); }, [getHandDetails]); const handleActivate = useCallback(async (hand: Hand, params?: Record) => { setActivatingHandId(hand.id); try { const result = await triggerHand(hand.id, params); if (result) { toast(`Hand "${hand.name}" 已成功激活`, 'success'); // Refresh hands after activation await loadHands(); } else { // Check if there's an error in the store const errorMsg = storeError || '激活失败,请检查后端连接'; console.error(`[HandsPanel] Hand activation failed:`, errorMsg); toast(`Hand "${hand.name}" 激活失败: ${errorMsg}`, 'error'); } } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); console.error(`[HandsPanel] Hand activation error:`, errorMsg); toast(`Hand "${hand.name}" 激活异常: ${errorMsg}`, 'error'); } finally { setActivatingHandId(null); } }, [triggerHand, loadHands, toast, storeError]); const handleCloseModal = useCallback(() => { setShowModal(false); setSelectedHand(null); }, []); const handleModalActivate = useCallback(async (params?: Record) => { if (!selectedHand) return; setShowModal(false); await handleActivate(selectedHand, params); }, [selectedHand, handleActivate]); if (isLoading && hands.length === 0) { return (

加载 Hands 中...

); } if (hands.length === 0) { return (

暂无可用的 Hands

请连接到 ZCLAW 以查看可用的自主能力包。

); } return (
{/* Header */}

Hands

自主能力包

{/* Tabs */}
{/* Tab Content */} {activeTab === 'approvals' ? ( ) : ( <> {/* Stats */}
可用 {hands.length} 就绪 {hands.filter(h => h.status === 'idle').length}
{/* Hand Cards Grid */}
{hands.map((hand) => { // Check if this is a Browser Hand const isBrowserHand = hand.id === 'browser' || hand.name === 'Browser' || hand.name?.toLowerCase().includes('browser'); return isBrowserHand ? ( ) : ( ); })}
{/* Details Modal */} {selectedHand && ( )} )}
); } export default HandsPanel;