| // src/components/agents/AgentTaskCard.tsx | |
| import { useState } from "react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { | |
| Loader2, CheckCircle2, XCircle, Clock, ChevronDown, ChevronUp, | |
| Search, FileSearch, GitCompare, BookOpen, FileText, TrendingUp, | |
| Shield, Quote, BarChart3, Palette, MoreVertical, Copy, ExternalLink, | |
| } from "lucide-react"; | |
| import { AgentTask, AgentDefinition } from "@/types/multi-agent"; | |
| import { toast } from "react-toastify"; | |
| interface AgentTaskCardProps { | |
| task: AgentTask; | |
| agent: AgentDefinition | null; | |
| onRetry?: (taskId: string) => void; | |
| onViewDetails?: (task: AgentTask) => void; | |
| } | |
| const ROLE_ICONS: Record<string, any> = { | |
| orchestrator: MoreVertical, | |
| literature_reviewer: Search, | |
| data_extractor: FileSearch, | |
| cross_referencer: GitCompare, | |
| paper_analyzer: BookOpen, | |
| summarizer: FileText, | |
| fact_checker: Shield, | |
| citation_expert: Quote, | |
| statistical_analyst: BarChart3, | |
| visualization_expert: Palette, | |
| }; | |
| const ROLE_COLORS: Record<string, string> = { | |
| orchestrator: "border-purple-300 bg-purple-50 dark:bg-purple-900/10", | |
| literature_reviewer: "border-blue-300 bg-blue-50 dark:bg-blue-900/10", | |
| data_extractor: "border-green-300 bg-green-50 dark:bg-green-900/10", | |
| cross_referencer: "border-yellow-300 bg-yellow-50 dark:bg-yellow-900/10", | |
| paper_analyzer: "border-indigo-300 bg-indigo-50 dark:bg-indigo-900/10", | |
| summarizer: "border-gray-300 bg-gray-50 dark:bg-gray-800", | |
| }; | |
| export function AgentTaskCard({ task, agent, onRetry, onViewDetails }: AgentTaskCardProps) { | |
| const [expanded, setExpanded] = useState(false); | |
| const [showMenu, setShowMenu] = useState(false); | |
| const Icon = ROLE_ICONS[task.assignedAgent] ?? Search; | |
| const colorClass = ROLE_COLORS[task.assignedAgent] ?? ROLE_COLORS.literature_reviewer; | |
| const getStatusIcon = () => { | |
| switch (task.status) { | |
| case "completed": | |
| return <CheckCircle2 className="w-4 h-4 text-green-500" />; | |
| case "running": | |
| return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />; | |
| case "failed": | |
| return <XCircle className="w-4 h-4 text-red-500" />; | |
| default: | |
| return <Clock className="w-4 h-4 text-gray-400" />; | |
| } | |
| }; | |
| const getStatusText = () => { | |
| switch (task.status) { | |
| case "completed": return "Completed"; | |
| case "running": return "Running..."; | |
| case "failed": return "Failed"; | |
| default: return "Pending"; | |
| } | |
| }; | |
| const formatDuration = (startedAt?: number, completedAt?: number) => { | |
| if (!startedAt) return "Not started"; | |
| const end = completedAt || Date.now(); | |
| const duration = end - startedAt; | |
| if (duration < 1000) return `${duration}ms`; | |
| return `${(duration / 1000).toFixed(1)}s`; | |
| }; | |
| const handleCopy = () => { | |
| const text = `Task: ${agent?.name}\nStatus: ${getStatusText()}\nInput: ${task.input}\nOutput: ${task.output || "N/A"}`; | |
| navigator.clipboard.writeText(text); | |
| toast.success("Task details copied"); | |
| setShowMenu(false); | |
| }; | |
| return ( | |
| <div className="relative"> | |
| <button | |
| onClick={() => setExpanded(!expanded)} | |
| className={`w-64 p-3 rounded-xl border-2 text-left transition-all hover:shadow-md ${ | |
| task.status === "completed" | |
| ? "border-green-300 bg-green-50 dark:bg-green-900/10" | |
| : task.status === "running" | |
| ? "border-blue-300 bg-blue-50 dark:bg-blue-900/10" | |
| : task.status === "failed" | |
| ? "border-red-300 bg-red-50 dark:bg-red-900/10" | |
| : "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span>{getStatusIcon()}</span> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-1"> | |
| <Icon className="w-3 h-3 text-gray-500" /> | |
| <p className="text-xs font-semibold truncate">{agent?.name ?? task.assignedAgent.replace(/_/g, " ")}</p> | |
| </div> | |
| <p className="text-[10px] text-gray-500 mt-0.5">{getStatusText()}</p> | |
| </div> | |
| </div> | |
| <div className="mt-2 flex items-center justify-between text-[10px] text-gray-400"> | |
| <span>Retry: {task.retryCount}</span> | |
| <span>{formatDuration(task.startedAt, task.completedAt)}</span> | |
| </div> | |
| </button> | |
| {/* Menu button */} | |
| <button | |
| onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }} | |
| className="absolute -top-1 -right-1 p-1 rounded-full bg-white dark:bg-gray-800 shadow-md hover:bg-gray-100 dark:hover:bg-gray-700 transition" | |
| > | |
| <MoreVertical className="w-3 h-3 text-gray-500" /> | |
| </button> | |
| {/* Context menu */} | |
| <AnimatePresence> | |
| {showMenu && ( | |
| <> | |
| <div className="fixed inset-0 z-10" onClick={() => setShowMenu(false)} /> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.95 }} | |
| className="absolute top-6 right-0 z-20 w-36 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 py-1" | |
| > | |
| <button | |
| onClick={handleCopy} | |
| className="w-full text-left px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2" | |
| > | |
| <Copy className="w-3 h-3" /> Copy | |
| </button> | |
| {task.status === "failed" && onRetry && ( | |
| <button | |
| onClick={() => { onRetry(task.id); setShowMenu(false); }} | |
| className="w-full text-left px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2" | |
| > | |
| <Loader2 className="w-3 h-3" /> Retry | |
| </button> | |
| )} | |
| {onViewDetails && ( | |
| <button | |
| onClick={() => { onViewDetails(task); setShowMenu(false); }} | |
| className="w-full text-left px-3 py-1.5 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2" | |
| > | |
| <ExternalLink className="w-3 h-3" /> Details | |
| </button> | |
| )} | |
| </motion.div> | |
| </> | |
| )} | |
| </AnimatePresence> | |
| {/* Expanded log */} | |
| <AnimatePresence> | |
| {expanded && task.log.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: "auto" }} | |
| exit={{ opacity: 0, height: 0 }} | |
| className="absolute top-full mt-1 left-0 w-80 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-xl z-10 overflow-hidden" | |
| > | |
| <div className="p-3 max-h-48 overflow-y-auto"> | |
| <p className="text-[10px] font-semibold text-gray-500 uppercase mb-2 flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> Activity Log | |
| </p> | |
| <div className="space-y-1.5"> | |
| {task.log.map((entry, i) => ( | |
| <div key={i} className="text-[10px] flex items-start gap-1.5"> | |
| <span className={`mt-0.5 w-1.5 h-1.5 rounded-full ${ | |
| entry.type === "error" ? "bg-red-500" : | |
| entry.type === "tool_call" ? "bg-blue-500" : | |
| entry.type === "result" ? "bg-green-500" : | |
| "bg-gray-400" | |
| }`} /> | |
| <span className="text-gray-600 dark:text-gray-400 flex-1">{entry.message}</span> | |
| <span className="text-gray-400 text-[9px]"> | |
| {new Date(entry.timestamp).toLocaleTimeString([], { minute: "2-digit", second: "2-digit" })} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="border-t border-gray-100 dark:border-gray-700 px-3 py-1.5 bg-gray-50 dark:bg-gray-700/30"> | |
| <div className="flex items-center justify-between text-[9px] text-gray-400"> | |
| <span>Input: {task.input.substring(0, 50)}...</span> | |
| {task.output && <span className="text-green-500 truncate max-w-[150px]">{task.output.substring(0, 50)}...</span>} | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 8.49 kB
- Xet hash:
- a78899e999b04b85592ac9f3e9e4160a592da15a40f4358f50e80c7c2dabee8f
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.