Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /** * HandList - 左侧导航的 Hands 列表 * * 显示所有可用的 Hands(自主能力包), * 允许用户选择一个 Hand 来查看其任务和结果。 */ import { useEffect } from 'react'; import { useHandStore, type Hand } from '../store/handStore'; import { Zap, Loader2, RefreshCw, CheckCircle, XCircle, AlertTriangle } from 'lucide-react'; interface HandListProps { selectedHandId?: string; onSelectHand?: (handId: string) => void; } // 状态图标 function HandStatusIcon({ status }: { status: Hand['status'] }) { switch (status) { case 'running': return <Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />; case 'needs_approval': return <AlertTriangle className="w-3.5 h-3.5 text-yellow-500" />; case 'error': return <XCircle className="w-3.5 h-3.5 text-red-500" />; case 'setup_needed': case 'unavailable': return <AlertTriangle className="w-3.5 h-3.5 text-orange-500" />; default: return <CheckCircle className="w-3.5 h-3.5 text-green-500" />; } } // 状态标签 const STATUS_LABELS: Record<Hand['status'], string> = { idle: '就绪', running: '运行中', needs_approval: '待审批', error: '错误', unavailable: '不可用', setup_needed: '需配置', }; export function HandList({ selectedHandId, onSelectHand }: HandListProps) { const hands = useHandStore((s) => s.hands); const loadHands = useHandStore((s) => s.loadHands); const isLoading = useHandStore((s) => s.isLoading); useEffect(() => { loadHands(); }, [loadHands]); if (isLoading && hands.length === 0) { return ( <div className="p-4 text-center"> <Loader2 className="w-5 h-5 animate-spin mx-auto text-gray-400 mb-2" /> <p className="text-xs text-gray-400">加载中...</p> </div> ); } if (hands.length === 0) { return ( <div className="p-4 text-center"> <Zap className="w-8 h-8 mx-auto text-gray-300 mb-2" /> <p className="text-xs text-gray-400 mb-1">暂无可用 Hands</p> <p className="text-xs text-gray-300">连接 OpenFang 后显示</p> </div> ); } return ( <div className="flex flex-col h-full"> {/* 头部 */} <div className="p-3 border-b border-gray-200 flex items-center justify-between"> <div> <h3 className="text-xs font-semibold text-gray-700">自主能力包</h3> <p className="text-xs text-gray-400">{hands.length} 个可用</p> </div> <button onClick={() => loadHands()} disabled={isLoading} className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors disabled:opacity-50" title="刷新" > <RefreshCw className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`} /> </button> </div> {/* Hands 列表 */} <div className="flex-1 overflow-y-auto"> {hands.map((hand) => ( <button key={hand.id} onClick={() => onSelectHand?.(hand.id)} className={`w-full text-left p-3 border-b border-gray-100 hover:bg-gray-100 transition-colors ${ selectedHandId === hand.id ? 'bg-blue-50 border-l-2 border-l-blue-500' : '' }`} > <div className="flex items-start gap-2"> <span className="text-lg flex-shrink-0">{hand.icon || '🤖'}</span> <div className="flex-1 min-w-0"> <div className="flex items-center gap-1.5"> <span className="font-medium text-gray-800 text-sm truncate"> {hand.name} </span> <HandStatusIcon status={hand.status} /> </div> <p className="text-xs text-gray-400 truncate mt-0.5"> {hand.description} </p> <div className="flex items-center gap-2 mt-1"> <span className="text-xs text-gray-400"> {STATUS_LABELS[hand.status]} </span> {hand.toolCount !== undefined && ( <span className="text-xs text-gray-300"> {hand.toolCount} 工具 </span> )} </div> </div> </div> </button> ))} </div> </div> ); } export default HandList; |