Files
zclaw_openfang/desktop/src/components/HandList.tsx
iven 0d4fa96b82
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
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

132 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;