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