feat(l4): add Phase 1 UI components for self-evolution capability
SwarmDashboard (多 Agent 协作面板): - Task list with real-time status updates - Subtask visualization with results - Communication style indicators (Sequential/Parallel/Debate) - Task creation form with manual triggers SkillMarket (技能市场): - Browse 12 built-in skills by category - Keyword/capability search - Skill details with triggers and capabilities - Install/uninstall with L4 autonomy hooks HeartbeatConfig (心跳配置): - Enable/disable periodic proactive checks - Interval slider (5-120 minutes) - Proactivity level selector (Silent/Light/Standard/Autonomous) - Quiet hours configuration - Built-in check item toggles ReflectionLog (反思日志): - Reflection history with pattern analysis - Improvement suggestions by priority - Identity change proposal approval workflow - Manual reflection trigger - Config panel for trigger settings Part of ZCLAW L4 Self-Evolution capability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
591
desktop/src/components/SwarmDashboard.tsx
Normal file
591
desktop/src/components/SwarmDashboard.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
/**
|
||||
* SwarmDashboard - Multi-Agent Collaboration Task Dashboard
|
||||
*
|
||||
* Visualizes swarm tasks, multi-agent collaboration) with real-time
|
||||
* status updates, task history, and manual trigger functionality.
|
||||
*
|
||||
* Part of ZCLAW L4 Self-Evolution capability.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Users,
|
||||
Play,
|
||||
Pause,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Layers,
|
||||
GitBranch,
|
||||
MessageSquare,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
History,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AgentSwarm,
|
||||
type SwarmTask,
|
||||
type Subtask,
|
||||
type SwarmTaskStatus,
|
||||
type CommunicationStyle,
|
||||
} from '../lib/agent-swarm';
|
||||
import { useAgentStore } from '../store/agentStore';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface SwarmDashboardProps {
|
||||
className?: string;
|
||||
onTaskSelect?: (task: SwarmTask) => void;
|
||||
}
|
||||
|
||||
type FilterType = 'all' | 'active' | 'completed' | 'failed';
|
||||
|
||||
// === Status Config ===
|
||||
|
||||
const TASK_STATUS_CONFIG: Record<SwarmTaskStatus, { label: string; className: string; dotClass: string; icon: typeof CheckCircle }> = {
|
||||
planning: {
|
||||
label: '规划中',
|
||||
className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
dotClass: 'bg-purple-500',
|
||||
icon: Layers,
|
||||
},
|
||||
executing: {
|
||||
label: '执行中',
|
||||
className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
dotClass: 'bg-blue-500 animate-pulse',
|
||||
icon: Play,
|
||||
},
|
||||
aggregating: {
|
||||
label: '汇总中',
|
||||
className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400',
|
||||
dotClass: 'bg-cyan-500 animate-pulse',
|
||||
icon: RefreshCw,
|
||||
},
|
||||
done: {
|
||||
label: '已完成',
|
||||
className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
dotClass: 'bg-green-500',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
failed: {
|
||||
label: '失败',
|
||||
className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
||||
dotClass: 'bg-red-500',
|
||||
icon: XCircle,
|
||||
},
|
||||
};
|
||||
|
||||
const SUBTASK_STATUS_CONFIG: Record<string, { label: string; dotClass: string }> = {
|
||||
pending: { label: '待执行', dotClass: 'bg-gray-400' },
|
||||
running: { label: '执行中', dotClass: 'bg-blue-500 animate-pulse' },
|
||||
done: { label: '完成', dotClass: 'bg-green-500' },
|
||||
failed: { label: '失败', dotClass: 'bg-red-500' },
|
||||
};
|
||||
|
||||
const COMMUNICATION_STYLE_CONFIG: Record<CommunicationStyle, { label: string; icon: typeof Users; description: string }> = {
|
||||
sequential: {
|
||||
label: '顺序执行',
|
||||
icon: GitBranch,
|
||||
description: '每个 Agent 依次处理,输出传递给下一个',
|
||||
},
|
||||
parallel: {
|
||||
label: '并行执行',
|
||||
icon: Layers,
|
||||
description: '多个 Agent 同时处理不同子任务',
|
||||
},
|
||||
debate: {
|
||||
label: '辩论模式',
|
||||
icon: MessageSquare,
|
||||
description: '多个 Agent 提供观点,协调者综合',
|
||||
},
|
||||
};
|
||||
|
||||
// === Components ===
|
||||
|
||||
function TaskStatusBadge({ status }: { status: SwarmTaskStatus }) {
|
||||
const config = TASK_STATUS_CONFIG[status];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${config.className}`}>
|
||||
<Icon className="w-3 h-3" />
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtaskStatusDot({ status }: { status: string }) {
|
||||
const config = SUBTASK_STATUS_CONFIG[status] || SUBTASK_STATUS_CONFIG.pending;
|
||||
return <span className={`w-2 h-2 rounded-full ${config.dotClass}`} title={config.label} />;
|
||||
}
|
||||
|
||||
function CommunicationStyleBadge({ style }: { style: CommunicationStyle }) {
|
||||
const config = COMMUNICATION_STYLE_CONFIG[style];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||
title={config.description}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtaskItem({
|
||||
subtask,
|
||||
agentName,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
subtask: Subtask;
|
||||
agentName: string;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const duration = useMemo(() => {
|
||||
if (!subtask.startedAt) return null;
|
||||
const start = new Date(subtask.startedAt).getTime();
|
||||
const end = subtask.completedAt ? new Date(subtask.completedAt).getTime() : Date.now();
|
||||
return Math.round((end - start) / 1000);
|
||||
}, [subtask.startedAt, subtask.completedAt]);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left"
|
||||
>
|
||||
<SubtaskStatusDot status={subtask.status} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{subtask.description}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
分配给: {agentName}
|
||||
{duration !== null && <span className="ml-2">· {duration}s</span>}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isExpanded && subtask.result && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="p-3 text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap max-h-40 overflow-y-auto">
|
||||
{subtask.result}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{subtask.error && (
|
||||
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs text-red-600 dark:text-red-400">{subtask.error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskCard({
|
||||
task,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: {
|
||||
task: SwarmTask;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const [expandedSubtasks, setExpandedSubtasks] = useState<Set<string>>(new Set());
|
||||
const { agents } = useAgentStore();
|
||||
|
||||
const toggleSubtask = useCallback((subtaskId: string) => {
|
||||
setExpandedSubtasks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(subtaskId)) {
|
||||
next.delete(subtaskId);
|
||||
} else {
|
||||
next.add(subtaskId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const completedCount = task.subtasks.filter((s) => s.status === 'done').length;
|
||||
const totalDuration = useMemo(() => {
|
||||
if (!task.completedAt) return null;
|
||||
const start = new Date(task.createdAt).getTime();
|
||||
const end = new Date(task.completedAt).getTime();
|
||||
return Math.round((end - start) / 1000);
|
||||
}, [task.createdAt, task.completedAt]);
|
||||
|
||||
const getAgentName = (agentId: string) => {
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
return agent?.name || agentId;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-lg overflow-hidden transition-all ${
|
||||
isSelected
|
||||
? 'border-blue-500 dark:border-blue-400 ring-2 ring-blue-500/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={onSelect}
|
||||
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TaskStatusBadge status={task.status} />
|
||||
<CommunicationStyleBadge style={task.communicationStyle} />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{task.description}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
{completedCount}/{task.subtasks.length} 子任务
|
||||
</span>
|
||||
{totalDuration !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{totalDuration}s
|
||||
</span>
|
||||
)}
|
||||
<span>{new Date(task.createdAt).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isSelected && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="p-4 space-y-3">
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
子任务
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{task.subtasks.map((subtask) => (
|
||||
<SubtaskItem
|
||||
key={subtask.id}
|
||||
subtask={subtask}
|
||||
agentName={getAgentName(subtask.assignedTo)}
|
||||
isExpanded={expandedSubtasks.has(subtask.id)}
|
||||
onToggle={() => toggleSubtask(subtask.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{task.finalResult && (
|
||||
<div className="mt-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<h4 className="text-xs font-medium text-green-700 dark:text-green-400 mb-1">
|
||||
最终结果
|
||||
</h4>
|
||||
<p className="text-sm text-green-600 dark:text-green-300 whitespace-pre-wrap">
|
||||
{task.finalResult}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateTaskForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: {
|
||||
onSubmit: (description: string, style: CommunicationStyle) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [description, setDescription] = useState('');
|
||||
const [style, setStyle] = useState<CommunicationStyle>('sequential');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (description.trim()) {
|
||||
onSubmit(description.trim(), style);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
任务描述
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="描述需要协作完成的任务..."
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
协作模式
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{(Object.keys(COMMUNICATION_STYLE_CONFIG) as CommunicationStyle[]).map((s) => {
|
||||
const config = COMMUNICATION_STYLE_CONFIG[s];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => setStyle(s)}
|
||||
className={`flex flex-col items-center gap-1 p-2 rounded-lg border transition-all ${
|
||||
style === s
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-xs">{config.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!description.trim()}
|
||||
className="px-4 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
创建任务
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// === Main Component ===
|
||||
|
||||
export function SwarmDashboard({ className = '', onTaskSelect }: SwarmDashboardProps) {
|
||||
const [swarm] = useState(() => new AgentSwarm());
|
||||
const [tasks, setTasks] = useState<SwarmTask[]>([]);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState<FilterType>('all');
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
// Load tasks from swarm history
|
||||
useEffect(() => {
|
||||
const history = swarm.getHistory();
|
||||
setTasks([...history].reverse()); // Most recent first
|
||||
}, [swarm]);
|
||||
|
||||
const filteredTasks = useMemo(() => {
|
||||
switch (filter) {
|
||||
case 'active':
|
||||
return tasks.filter((t) => ['planning', 'executing', 'aggregating'].includes(t.status));
|
||||
case 'completed':
|
||||
return tasks.filter((t) => t.status === 'done');
|
||||
case 'failed':
|
||||
return tasks.filter((t) => t.status === 'failed');
|
||||
default:
|
||||
return tasks;
|
||||
}
|
||||
}, [tasks, filter]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const active = tasks.filter((t) => ['planning', 'executing', 'aggregating'].includes(t.status)).length;
|
||||
const completed = tasks.filter((t) => t.status === 'done').length;
|
||||
const failed = tasks.filter((t) => t.status === 'failed').length;
|
||||
return { total: tasks.length, active, completed, failed };
|
||||
}, [tasks]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
// Simulate refresh delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const history = swarm.getHistory();
|
||||
setTasks([...history].reverse());
|
||||
setIsRefreshing(false);
|
||||
}, [swarm]);
|
||||
|
||||
const handleCreateTask = useCallback(
|
||||
(description: string, style: CommunicationStyle) => {
|
||||
const task = swarm.createTask(description, { communicationStyle: style });
|
||||
setTasks((prev) => [task, ...prev]);
|
||||
setSelectedTaskId(task.id);
|
||||
setShowCreateForm(false);
|
||||
onTaskSelect?.(task);
|
||||
|
||||
// Note: Actual execution should be triggered via chatStore.dispatchSwarmTask
|
||||
console.log('[SwarmDashboard] Task created:', task.id, 'Style:', style);
|
||||
},
|
||||
[swarm, onTaskSelect]
|
||||
);
|
||||
|
||||
const handleSelectTask = useCallback(
|
||||
(taskId: string) => {
|
||||
setSelectedTaskId((prev) => (prev === taskId ? null : taskId));
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (task && selectedTaskId !== taskId) {
|
||||
onTaskSelect?.(task);
|
||||
}
|
||||
},
|
||||
[tasks, onTaskSelect, selectedTaskId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full ${className}`}>
|
||||
{/* 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-2">
|
||||
<Users className="w-5 h-5 text-blue-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">协作任务</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50"
|
||||
title="刷新"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateForm((prev) => !prev)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
总计: <span className="font-medium text-gray-900 dark:text-gray-100">{stats.total}</span>
|
||||
</span>
|
||||
<span className="text-blue-600 dark:text-blue-400">
|
||||
活跃: <span className="font-medium">{stats.active}</span>
|
||||
</span>
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
完成: <span className="font-medium">{stats.completed}</span>
|
||||
</span>
|
||||
{stats.failed > 0 && (
|
||||
<span className="text-red-600 dark:text-red-400">
|
||||
失败: <span className="font-medium">{stats.failed}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-1 px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
{(['all', 'active', 'completed', 'failed'] as FilterType[]).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
filter === f
|
||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{f === 'all' ? '全部' : f === 'active' ? '活跃' : f === 'completed' ? '已完成' : '失败'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Create Form */}
|
||||
<AnimatePresence>
|
||||
{showCreateForm && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-b border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
<CreateTaskForm
|
||||
onSubmit={handleCreateTask}
|
||||
onCancel={() => setShowCreateForm(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
|
||||
<History className="w-8 h-8 mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
{filter === 'all'
|
||||
? '暂无协作任务'
|
||||
: filter === 'active'
|
||||
? '暂无活跃任务'
|
||||
: filter === 'completed'
|
||||
? '暂无已完成任务'
|
||||
: '暂无失败任务'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
className="mt-2 text-blue-500 hover:text-blue-600 text-sm"
|
||||
>
|
||||
创建第一个任务
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
isSelected={selectedTaskId === task.id}
|
||||
onSelect={() => handleSelectTask(task.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SwarmDashboard;
|
||||
Reference in New Issue
Block a user