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

@@ -1,19 +1,51 @@
import { useState, useEffect } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import { useChatStore } from '../store/chatStore';
import { Plus, Trash2, Bot, X } from 'lucide-react';
import { toChatAgent, useChatStore } from '../store/chatStore';
import { Bot, Plus, X, Globe, Cat, Search, BarChart2 } from 'lucide-react';
interface CloneFormData {
name: string;
role: string;
nickname: string;
scenarios: string;
workspaceDir: string;
userName: string;
userRole: string;
restrictFiles: boolean;
privacyOptIn: boolean;
}
const DEFAULT_WORKSPACE = '~/.openclaw/zclaw-workspace';
function createFormFromDraft(quickConfig: {
agentName?: string;
agentRole?: string;
agentNickname?: string;
scenarios?: string[];
workspaceDir?: string;
userName?: string;
userRole?: string;
restrictFiles?: boolean;
privacyOptIn?: boolean;
}): CloneFormData {
return {
name: quickConfig.agentName || '',
role: quickConfig.agentRole || '',
nickname: quickConfig.agentNickname || '',
scenarios: quickConfig.scenarios?.join(', ') || '',
workspaceDir: quickConfig.workspaceDir || DEFAULT_WORKSPACE,
userName: quickConfig.userName || '',
userRole: quickConfig.userRole || '',
restrictFiles: quickConfig.restrictFiles ?? true,
privacyOptIn: quickConfig.privacyOptIn ?? false,
};
}
export function CloneManager() {
const { clones, loadClones, createClone, deleteClone, connectionState } = useGatewayStore();
const { agents } = useChatStore();
const { clones, loadClones, createClone, deleteClone, connectionState, quickConfig, saveQuickConfig } = useGatewayStore();
const { agents, currentAgent, setCurrentAgent } = useChatStore();
const [showForm, setShowForm] = useState(false);
const [form, setForm] = useState<CloneFormData>({ name: '', role: '', scenarios: '' });
const [form, setForm] = useState<CloneFormData>(createFormFromDraft({}));
const connected = connectionState === 'connected';
@@ -23,14 +55,54 @@ export function CloneManager() {
}
}, [connected]);
useEffect(() => {
if (showForm) {
setForm(createFormFromDraft(quickConfig));
}
}, [showForm, quickConfig]);
const handleCreate = async () => {
if (!form.name.trim()) return;
await createClone({
const scenarios = form.scenarios
? form.scenarios.split(',').map((s) => s.trim()).filter(Boolean)
: undefined;
await saveQuickConfig({
agentName: form.name,
agentRole: form.role || undefined,
agentNickname: form.nickname || undefined,
scenarios,
workspaceDir: form.workspaceDir || undefined,
userName: form.userName || undefined,
userRole: form.userRole || undefined,
restrictFiles: form.restrictFiles,
privacyOptIn: form.privacyOptIn,
});
const clone = await createClone({
name: form.name,
role: form.role || undefined,
scenarios: form.scenarios ? form.scenarios.split(',').map(s => s.trim()) : undefined,
nickname: form.nickname || undefined,
scenarios,
workspaceDir: form.workspaceDir || undefined,
userName: form.userName || undefined,
userRole: form.userRole || undefined,
restrictFiles: form.restrictFiles,
privacyOptIn: form.privacyOptIn,
});
setForm({ name: '', role: '', scenarios: '' });
if (clone) {
setCurrentAgent(toChatAgent(clone));
}
setForm(createFormFromDraft({
...quickConfig,
agentName: form.name,
agentRole: form.role,
agentNickname: form.nickname,
scenarios,
workspaceDir: form.workspaceDir,
userName: form.userName,
userRole: form.userRole,
restrictFiles: form.restrictFiles,
privacyOptIn: form.privacyOptIn,
}));
setShowForm(false);
};
@@ -45,28 +117,40 @@ export function CloneManager() {
id: a.id,
name: a.name,
role: '默认助手',
nickname: a.name,
scenarios: [],
workspaceDir: '~/.openclaw/zclaw-workspace',
userName: quickConfig.userName || '未设置',
userRole: '',
restrictFiles: true,
privacyOptIn: false,
createdAt: '',
}));
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
<span className="text-xs font-medium text-gray-500"></span>
<button
onClick={() => setShowForm(true)}
className="p-1 text-gray-400 hover:text-orange-500 rounded"
title="创建分身"
>
<Plus className="w-4 h-4" />
</button>
</div>
// Function to assign pseudo icons/colors based on names for UI matching
const getIconAndColor = (name: string) => {
if (name.includes('Browser') || name.includes('浏览器')) {
return { icon: <Globe className="w-5 h-5" />, bg: 'bg-blue-500 text-white' };
}
if (name.includes('AutoClaw') || name.includes('ZCLAW')) {
return { icon: <Cat className="w-6 h-6" />, bg: 'bg-gradient-to-br from-orange-400 to-red-500 text-white' };
}
if (name.includes('沉思')) {
return { icon: <Search className="w-5 h-5" />, bg: 'bg-blue-100 text-blue-600' };
}
if (name.includes('监控')) {
return { icon: <BarChart2 className="w-5 h-5" />, bg: 'bg-orange-100 text-orange-600' };
}
return { icon: <Bot className="w-5 h-5" />, bg: 'bg-gray-200 text-gray-600' };
};
return (
<div className="h-full flex flex-col py-2">
{/* Create form */}
{showForm && (
<div className="p-3 border-b border-gray-200 bg-orange-50 space-y-2">
<div className="mx-2 mb-2 p-3 border border-gray-200 rounded-lg bg-white space-y-2 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-orange-700"></span>
<span className="text-xs font-medium text-gray-900"> Agent</span>
<button onClick={() => setShowForm(false)} className="text-gray-400 hover:text-gray-600">
<X className="w-3.5 h-3.5" />
</button>
@@ -76,61 +160,134 @@ export function CloneManager() {
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder="名称 (必填)"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-orange-500"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<input
type="text"
value={form.role}
onChange={e => setForm({ ...form, role: e.target.value })}
placeholder="角色 (如: 代码助手)"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-orange-500"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<input
type="text"
value={form.nickname}
onChange={e => setForm({ ...form, nickname: e.target.value })}
placeholder="昵称 / 对你的称呼"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<input
type="text"
value={form.scenarios}
onChange={e => setForm({ ...form, scenarios: e.target.value })}
placeholder="场景标签 (逗号分隔)"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-orange-500"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<input
type="text"
value={form.workspaceDir}
onChange={e => setForm({ ...form, workspaceDir: e.target.value })}
placeholder="工作目录"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<div className="grid grid-cols-2 gap-2">
<input
type="text"
value={form.userName}
onChange={e => setForm({ ...form, userName: e.target.value })}
placeholder="你的名字"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
<input
type="text"
value={form.userRole}
onChange={e => setForm({ ...form, userRole: e.target.value })}
placeholder="你的角色"
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
/>
</div>
<label className="flex items-center justify-between text-xs text-gray-600 px-1">
<span>访</span>
<input
type="checkbox"
checked={form.restrictFiles}
onChange={e => setForm({ ...form, restrictFiles: e.target.checked })}
/>
</label>
<label className="flex items-center justify-between text-xs text-gray-600 px-1">
<span></span>
<input
type="checkbox"
checked={form.privacyOptIn}
onChange={e => setForm({ ...form, privacyOptIn: e.target.checked })}
/>
</label>
<button
onClick={handleCreate}
disabled={!form.name.trim()}
className="w-full text-xs bg-orange-500 text-white rounded py-1.5 hover:bg-orange-600 disabled:opacity-50"
className="w-full text-xs bg-gray-900 text-white rounded py-1.5 hover:bg-gray-800 disabled:opacity-50"
>
</button>
</div>
)}
{/* Clone list */}
<div className="flex-1 overflow-y-auto custom-scrollbar">
{displayClones.map((clone) => (
<div
key={clone.id}
className="group flex items-center gap-3 px-3 py-3 hover:bg-gray-100 cursor-pointer border-b border-gray-50"
>
<div className="w-9 h-9 bg-gradient-to-br from-orange-400 to-red-500 rounded-xl flex items-center justify-center text-white flex-shrink-0">
<Bot className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{clone.name}</div>
<div className="text-xs text-gray-400 truncate">{clone.role || '默认助手'}</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(clone.id); }}
className="opacity-0 group-hover:opacity-100 p-1 text-gray-300 hover:text-red-500 transition-opacity"
title="删除"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
{displayClones.map((clone, idx) => {
const { icon, bg } = getIconAndColor(clone.name);
const isActive = currentAgent ? currentAgent.id === clone.id : idx === 0;
const canDelete = clones.length > 0;
{displayClones.length === 0 && (
<div className="text-center py-8 text-xs text-gray-400">
<Bot className="w-8 h-8 mx-auto mb-2 opacity-30" />
<p></p>
<p className="mt-1"> + </p>
return (
<div
key={clone.id}
onClick={() => setCurrentAgent(toChatAgent(clone))}
className={`group sidebar-item mx-2 px-3 py-3 rounded-lg cursor-pointer mb-1 flex items-start gap-3 transition-colors ${
isActive ? 'bg-white shadow-sm border border-gray-100' : 'hover:bg-black/5'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${bg}`}>
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-0.5">
<span className={`truncate ${isActive ? 'font-semibold text-gray-900' : 'font-medium text-gray-900'}`}>{clone.name}</span>
{isActive ? <span className="text-xs text-orange-500"></span> : null}
</div>
<p className="text-xs text-gray-500 truncate">{clone.role || '新分身'}</p>
</div>
{canDelete && (
<button
onClick={(e) => { e.stopPropagation(); handleDelete(clone.id); }}
className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 focus:pointer-events-auto focus:opacity-100 p-1 mt-1 text-gray-300 hover:text-red-500 transition-opacity"
title="删除"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
);
})}
{/* Add new clone button as an item if we want, or keep the traditional way */}
{!showForm && (
<div
onClick={() => {
if (connected) {
setShowForm(true);
}
}}
className={`sidebar-item mx-2 px-3 py-3 rounded-lg mb-1 flex items-center gap-3 transition-colors border border-dashed border-gray-300 ${
connected
? 'cursor-pointer text-gray-500 hover:text-gray-900 hover:bg-black/5'
: 'cursor-not-allowed text-gray-400 bg-gray-50'
}`}
>
<div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 bg-gray-50">
<Plus className="w-5 h-5" />
</div>
<span className="text-sm font-medium">{connected ? '快速配置新 Agent' : '连接 Gateway 后创建'}</span>
</div>
)}
</div>