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,445 @@
/**
* WorkflowList - OpenFang Workflow Management UI
*
* Displays available OpenFang Workflows and allows executing them.
*
* Design based on OpenFang Dashboard v0.4.0
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import type { Workflow } from '../store/gatewayStore';
import {
Play,
Edit,
Trash2,
History,
Plus,
List,
GitBranch,
RefreshCw,
Loader2,
X,
} from 'lucide-react';
// === View Toggle Types ===
type ViewMode = 'list' | 'visual';
// === Workflow Execute Modal ===
interface ExecuteModalProps {
workflow: Workflow;
isOpen: boolean;
onClose: () => void;
onExecute: (id: string, input?: Record<string, unknown>) => Promise<void>;
isExecuting: boolean;
}
function ExecuteModal({ workflow, isOpen, onClose, onExecute, isExecuting }: ExecuteModalProps) {
const [input, setInput] = useState('');
const handleExecute = async () => {
let parsedInput: Record<string, unknown> | undefined;
if (input.trim()) {
try {
parsedInput = JSON.parse(input);
} catch {
alert('输入格式错误,请使用有效的 JSON 格式。');
return;
}
}
await onExecute(workflow.id, parsedInput);
setInput('');
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center">
<Play className="w-4 h-4 text-green-600 dark:text-green-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">{workflow.name}</p>
</div>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="p-4">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
(JSON ):
</div>
<textarea
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder='{"key": "value"}'
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
</button>
<button
onClick={handleExecute}
disabled={isExecuting}
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isExecuting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Play className="w-4 h-4" />
</>
)}
</button>
</div>
</div>
</div>
);
}
// === Workflow Table Row ===
interface WorkflowRowProps {
workflow: Workflow;
onExecute: (workflow: Workflow) => void;
onEdit: (workflow: Workflow) => void;
onDelete: (workflow: Workflow) => void;
onHistory: (workflow: Workflow) => void;
isExecuting: boolean;
}
function WorkflowRow({ workflow, onExecute, onEdit, onDelete, onHistory, isExecuting }: WorkflowRowProps) {
// Format created date if available
const createdDate = workflow.createdAt
? new Date(workflow.createdAt).toLocaleDateString('zh-CN')
: new Date().toLocaleDateString('zh-CN');
return (
<tr className="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
{/* Name */}
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center flex-shrink-0">
<GitBranch className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</div>
<div className="min-w-0">
<div className="font-medium text-gray-900 dark:text-white truncate">
{workflow.name}
</div>
{workflow.description && (
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
{workflow.description}
</div>
)}
</div>
</div>
</td>
{/* Steps */}
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-8 h-8 bg-gray-100 dark:bg-gray-700 rounded-full text-sm font-medium text-gray-700 dark:text-gray-300">
{workflow.steps}
</span>
</td>
{/* Created */}
<td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
{createdDate}
</td>
{/* Actions */}
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => onExecute(workflow)}
disabled={isExecuting}
className="p-1.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
title="Run"
>
{isExecuting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Play className="w-4 h-4" />
)}
</button>
<button
onClick={() => onEdit(workflow)}
className="p-1.5 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-md"
title="Edit"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => onHistory(workflow)}
className="p-1.5 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
title="History"
>
<History className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(workflow)}
className="p-1.5 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
);
}
// === Main WorkflowList Component ===
export function WorkflowList() {
const { workflows, loadWorkflows, executeWorkflow, isLoading } = useGatewayStore();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [executingWorkflowId, setExecutingWorkflowId] = useState<string | null>(null);
const [selectedWorkflow, setSelectedWorkflow] = useState<Workflow | null>(null);
const [showExecuteModal, setShowExecuteModal] = useState(false);
useEffect(() => {
loadWorkflows();
}, [loadWorkflows]);
const handleExecute = useCallback(async (id: string, input?: Record<string, unknown>) => {
setExecutingWorkflowId(id);
try {
await executeWorkflow(id, input);
} finally {
setExecutingWorkflowId(null);
}
}, [executeWorkflow]);
const handleExecuteClick = useCallback((workflow: Workflow) => {
setSelectedWorkflow(workflow);
setShowExecuteModal(true);
}, []);
const handleEdit = useCallback((workflow: Workflow) => {
// TODO: Implement workflow editor
console.log('Edit workflow:', workflow.id);
alert('工作流编辑器即将推出!');
}, []);
const handleDelete = useCallback((workflow: Workflow) => {
// TODO: Implement workflow deletion
console.log('Delete workflow:', workflow.id);
if (confirm(`确定要删除 "${workflow.name}" 吗?`)) {
alert('工作流删除功能即将推出!');
}
}, []);
const handleHistory = useCallback((workflow: Workflow) => {
// TODO: Implement workflow history view
console.log('View history:', workflow.id);
alert('工作流历史功能即将推出!');
}, []);
const handleNewWorkflow = useCallback(() => {
// TODO: Implement new workflow creation
alert('工作流构建器即将推出!');
}, []);
const handleCloseModal = useCallback(() => {
setShowExecuteModal(false);
setSelectedWorkflow(null);
}, []);
// Loading state
if (isLoading && workflows.length === 0) {
return (
<div className="p-4 text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-gray-400 mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">...</p>
</div>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
</p>
</div>
<button
onClick={() => loadWorkflows()}
disabled={isLoading}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 disabled:opacity-50"
>
{isLoading ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<RefreshCw className="w-3.5 h-3.5" />
)}
</button>
</div>
{/* Toolbar */}
<div className="flex items-center justify-between">
{/* View Toggle */}
<div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
<button
onClick={() => setViewMode('list')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
viewMode === 'list'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<List className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setViewMode('visual')}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
viewMode === 'visual'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<GitBranch className="w-3.5 h-3.5" />
</button>
</div>
{/* New Workflow Button */}
<button
onClick={handleNewWorkflow}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Content */}
{viewMode === 'list' ? (
workflows.length === 0 ? (
// Empty State
<div className="p-8 text-center">
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-3">
<GitBranch className="w-6 h-6 text-gray-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mb-4">
</p>
<button
onClick={handleNewWorkflow}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
) : (
// Table View
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50">
<th className="px-4 py-2.5 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
</th>
<th className="px-4 py-2.5 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
</th>
<th className="px-4 py-2.5 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
</th>
<th className="px-4 py-2.5 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody>
{workflows.map((workflow) => (
<WorkflowRow
key={workflow.id}
workflow={workflow}
onExecute={handleExecuteClick}
onEdit={handleEdit}
onDelete={handleDelete}
onHistory={handleHistory}
isExecuting={executingWorkflowId === workflow.id}
/>
))}
</tbody>
</table>
</div>
)
) : (
// Visual Builder View (placeholder)
<div className="p-8 text-center bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-3">
<GitBranch className="w-6 h-6 text-gray-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
</p>
<p className="text-xs text-gray-400 dark:text-gray-500">
</p>
</div>
)}
{/* Execute Modal */}
{selectedWorkflow && (
<ExecuteModal
workflow={selectedWorkflow}
isOpen={showExecuteModal}
onClose={handleCloseModal}
onExecute={handleExecute}
isExecuting={executingWorkflowId === selectedWorkflow.id}
/>
)}
</div>
);
}
export default WorkflowList;