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:
129
desktop/src/components/HandList.tsx
Normal file
129
desktop/src/components/HandList.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user