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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user