Files
zclaw_openfang/desktop/src/components/HandList.tsx
iven 07079293f4 feat(hands): restructure Hands UI with Chinese localization
Major changes:
- Add HandList.tsx component for left sidebar
- Add HandTaskPanel.tsx for middle content area
- Restructure Sidebar tabs: 分身/HANDS/Workflow
- Remove Hands tab from RightPanel
- Localize all UI text to Chinese
- Archive legacy OpenClaw documentation
- Add Hands integration lessons document
- Update feature checklist with new components

UI improvements:
- Left sidebar now shows Hands list with status icons
- Middle area shows selected Hand's tasks and results
- Consistent styling with Tailwind CSS
- Chinese status labels and buttons

Documentation:
- Create docs/archive/openclaw-legacy/ for old docs
- Add docs/knowledge-base/hands-integration-lessons.md
- Update docs/knowledge-base/feature-checklist.md
- Update docs/knowledge-base/README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 23:16:32 +08:00

130 lines
4.2 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 { useGatewayStore, type Hand } from '../store/gatewayStore';
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, loadHands, isLoading } = useGatewayStore();
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;