Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
/**
|
||
* 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">连接 ZCLAW 后显示</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;
|