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>
This commit is contained in:
iven
2026-03-14 23:16:32 +08:00
parent 67e1da635d
commit 07079293f4
126 changed files with 36229 additions and 1035 deletions

View File

@@ -0,0 +1,129 @@
/**
* 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;