Datavision / frontend /src /components /MCPToolExecution.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
14.5 kB
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Loader2,
CheckCircle2,
XCircle,
ChevronDown,
ChevronUp,
Wrench,
Sparkles,
Zap
} from 'lucide-react';
interface MCPToolExecutionProps {
toolName: string;
toolIcon?: string;
action: string;
status: 'pending' | 'running' | 'success' | 'error' | 'permission_required';
progress?: number;
details?: string[];
result?: string;
error?: string;
onAllow?: () => void;
onDeny?: () => void;
duration?: number;
}
/**
* Claude-style MCP Tool Execution Component
* Shows animated tool execution with progress, details, and permission requests
*/
const MCPToolExecution: React.FC<MCPToolExecutionProps> = ({
toolName,
toolIcon = '🔧',
action,
status,
progress = 0,
details = [],
result,
error,
onAllow,
onDeny,
duration
}) => {
const [expanded, setExpanded] = useState(false);
const [currentDetailIndex, setCurrentDetailIndex] = useState(0);
// Animate through details when running
useEffect(() => {
if (status === 'running' && details.length > 1) {
const interval = setInterval(() => {
setCurrentDetailIndex(prev => (prev + 1) % details.length);
}, 1500);
return () => clearInterval(interval);
}
}, [status, details.length]);
const getStatusColor = () => {
switch (status) {
case 'running': return 'border-green-500 bg-green-500/10';
case 'success': return 'border-green-500 bg-green-500/10';
case 'error': return 'border-red-500 bg-red-500/10';
case 'permission_required': return 'border-amber-500 bg-amber-500/10';
default: return 'border-gray-600 bg-gray-800/50';
}
};
const getStatusIcon = () => {
switch (status) {
case 'running':
return <Loader2 className="w-4 h-4 text-green-400 animate-spin" />;
case 'success':
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
case 'error':
return <XCircle className="w-4 h-4 text-red-400" />;
case 'permission_required':
return <Sparkles className="w-4 h-4 text-amber-400" />;
default:
return <div className="w-4 h-4 rounded-full border-2 border-gray-500" />;
}
};
return (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.98 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className={`rounded-lg border p-3 mb-2 ${getStatusColor()} transition-all duration-300`}
>
{/* Header */}
<div className="flex items-center gap-3">
{/* Tool Icon with animation */}
<motion.div
className="flex-shrink-0 w-8 h-8 rounded-lg bg-gray-800 flex items-center justify-center"
animate={status === 'running' ? { rotate: [0, 10, -10, 0] } : {}}
transition={{ repeat: Infinity, duration: 2 }}
>
<span className="text-lg">{toolIcon}</span>
</motion.div>
{/* Main Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-gray-200">{toolName}</span>
{getStatusIcon()}
</div>
<motion.p
key={currentDetailIndex}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="text-xs text-gray-400 truncate"
>
{status === 'running' && details.length > 0
? details[currentDetailIndex]
: action}
</motion.p>
</div>
{/* Duration/Progress */}
{status === 'success' && duration && (
<div className="flex items-center gap-1 text-xs text-gray-500">
<Zap className="w-3 h-3" />
{(duration / 1000).toFixed(1)}s
</div>
)}
{/* Expand Button */}
{(details.length > 0 || result) && (
<button
onClick={() => setExpanded(!expanded)}
className="p-1 hover:bg-gray-700 rounded transition-colors"
>
{expanded ? (
<ChevronUp className="w-4 h-4 text-gray-400" />
) : (
<ChevronDown className="w-4 h-4 text-gray-400" />
)}
</button>
)}
</div>
{/* Progress Bar */}
{status === 'running' && (
<motion.div
className="mt-2 h-1 bg-gray-700 rounded-full overflow-hidden"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<motion.div
className="h-full bg-gradient-to-r from-green-500 to-cyan-400"
initial={{ width: '0%' }}
animate={{ width: progress > 0 ? `${progress}%` : '100%' }}
transition={progress > 0 ? { duration: 0.3 } : {
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut'
}}
style={progress === 0 ? {
animation: 'shimmer 1.5s infinite',
background: 'linear-gradient(90deg, transparent, rgba(45, 212, 191, 0.5), transparent)',
backgroundSize: '200% 100%'
} : {}}
/>
</motion.div>
)}
{/* Permission Request */}
{status === 'permission_required' && (
<motion.div
className="mt-3 flex items-center gap-2"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
>
<button
onClick={onAllow}
className="flex-1 py-1.5 px-3 bg-green-600 hover:bg-green-500 text-white text-sm font-medium rounded-md transition-colors flex items-center justify-center gap-2"
>
<CheckCircle2 className="w-4 h-4" />
Allow
</button>
<button
onClick={onDeny}
className="flex-1 py-1.5 px-3 bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm font-medium rounded-md transition-colors flex items-center justify-center gap-2"
>
<XCircle className="w-4 h-4" />
Deny
</button>
</motion.div>
)}
{/* Expanded Details */}
<AnimatePresence>
{expanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="mt-3 pt-3 border-t border-gray-700">
{/* Details List */}
{details.length > 0 && (
<div className="space-y-1 mb-2">
{details.map((detail, idx) => (
<div
key={idx}
className="flex items-center gap-2 text-xs text-gray-400"
>
<div className={`w-1.5 h-1.5 rounded-full ${idx <= currentDetailIndex && status === 'running'
? 'bg-green-400'
: status === 'success'
? 'bg-green-400'
: 'bg-gray-600'
}`} />
{detail}
</div>
))}
</div>
)}
{/* Result */}
{result && (
<div className="p-2 bg-gray-800 rounded text-xs text-gray-300 font-mono">
{result}
</div>
)}
{/* Error */}
{error && (
<div className="p-2 bg-red-900/20 border border-red-800 rounded text-xs text-red-400">
{error}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
/**
* MCP Tool Chain Component
* Shows multiple tools working together in sequence
*/
interface MCPToolChainProps {
tools: Array<{
name: string;
icon: string;
status: 'pending' | 'running' | 'success' | 'error';
action: string;
}>;
title?: string;
}
export const MCPToolChain: React.FC<MCPToolChainProps> = ({ tools, title }) => {
return (
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 mb-3">
{title && (
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-700">
<Wrench className="w-4 h-4 text-green-400" />
<span className="text-sm font-medium text-gray-200">{title}</span>
</div>
)}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
{tools.map((tool, idx) => (
<React.Fragment key={tool.name}>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: idx * 0.1 }}
className={`flex-shrink-0 flex items-center gap-2 px-3 py-1.5 rounded-full border ${tool.status === 'running'
? 'border-green-500 bg-green-500/10'
: tool.status === 'success'
? 'border-green-500 bg-green-500/10'
: tool.status === 'error'
? 'border-red-500 bg-red-500/10'
: 'border-gray-600 bg-gray-700/50'
}`}
>
<span className="text-sm">{tool.icon}</span>
<span className="text-xs text-gray-300">{tool.name}</span>
{tool.status === 'running' && (
<Loader2 className="w-3 h-3 text-green-400 animate-spin" />
)}
{tool.status === 'success' && (
<CheckCircle2 className="w-3 h-3 text-green-400" />
)}
</motion.div>
{/* Arrow between tools */}
{idx < tools.length - 1 && (
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: idx * 0.1 + 0.05 }}
className="text-gray-600"
>
</motion.div>
)}
</React.Fragment>
))}
</div>
</div>
);
};
/**
* MCP Execution Summary
* Shows after all tools complete
*/
interface MCPSummaryProps {
toolsUsed: number;
totalDuration: number;
results: string[];
}
export const MCPExecutionSummary: React.FC<MCPSummaryProps> = ({
toolsUsed,
totalDuration,
results
}) => {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-lg border border-green-600/30 bg-green-900/10 p-3 mb-3"
>
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center">
<CheckCircle2 className="w-4 h-4 text-green-400" />
</div>
<div>
<p className="text-sm font-medium text-green-400">Tools Completed</p>
<p className="text-xs text-gray-400">
{toolsUsed} tools • {(totalDuration / 1000).toFixed(1)}s total
</p>
</div>
</div>
{results.length > 0 && (
<div className="mt-2 pt-2 border-t border-green-800/30">
{results.map((r, idx) => (
<p key={idx} className="text-xs text-gray-400 flex items-center gap-2">
<span className="text-green-400"></span> {r}
</p>
))}
</div>
)}
</motion.div>
);
};
export default MCPToolExecution;