tahamajs/IE / Projects /front /src /components /agents /AgentTaskCard.tsx
tahamajs's picture
download
raw
8.49 kB
// 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.