Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useState } from "react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { ChevronDown, Globe, Loader2, CheckCircle2, XCircle, Search } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| interface ToolInvocationProps { | |
| toolName: string; | |
| args: Record<string, unknown>; | |
| result?: string; | |
| status: "pending" | "running" | "complete" | "error"; | |
| } | |
| export function ToolInvocation({ | |
| toolName, | |
| args, | |
| result, | |
| status, | |
| }: ToolInvocationProps) { | |
| const [isExpanded, setIsExpanded] = useState(status === "running"); | |
| const getStatusIndicator = () => { | |
| switch (status) { | |
| case "pending": | |
| case "running": | |
| return ( | |
| <div className="relative"> | |
| <div className="w-5 h-5 rounded-full border-2 border-blue-500/30 border-t-blue-500 animate-spin" /> | |
| </div> | |
| ); | |
| case "complete": | |
| return ( | |
| <div className="w-5 h-5 rounded-full bg-green-500/10 flex items-center justify-center"> | |
| <CheckCircle2 className="w-3.5 h-3.5 text-green-500" /> | |
| </div> | |
| ); | |
| case "error": | |
| return ( | |
| <div className="w-5 h-5 rounded-full bg-red-500/10 flex items-center justify-center"> | |
| <XCircle className="w-3.5 h-3.5 text-red-500" /> | |
| </div> | |
| ); | |
| } | |
| }; | |
| const getDisplayInfo = () => { | |
| if (toolName === "web_search") { | |
| const query = args.query as string; | |
| return { | |
| icon: <Globe className="w-4 h-4" />, | |
| title: "Web Search", | |
| subtitle: query, | |
| }; | |
| } | |
| return { | |
| icon: <Search className="w-4 h-4" />, | |
| title: toolName, | |
| subtitle: JSON.stringify(args), | |
| }; | |
| }; | |
| const info = getDisplayInfo(); | |
| return ( | |
| <div className={cn( | |
| "mb-3 rounded-xl border overflow-hidden transition-all duration-200", | |
| status === "running" | |
| ? "border-blue-500/30 bg-blue-500/5" | |
| : status === "error" | |
| ? "border-red-500/30 bg-red-500/5" | |
| : "border-border bg-muted/30" | |
| )}> | |
| <button | |
| onClick={() => setIsExpanded(!isExpanded)} | |
| className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-accent/30 transition-colors" | |
| > | |
| {getStatusIndicator()} | |
| <div className={cn( | |
| "w-8 h-8 rounded-lg flex items-center justify-center", | |
| status === "running" ? "bg-blue-500/10 text-blue-500" : "bg-muted text-muted-foreground" | |
| )}> | |
| {info.icon} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm font-medium">{info.title}</p> | |
| <p className="text-xs text-muted-foreground truncate">{info.subtitle}</p> | |
| </div> | |
| <ChevronDown | |
| className={cn( | |
| "w-4 h-4 text-muted-foreground transition-transform duration-200 shrink-0", | |
| isExpanded && "rotate-180" | |
| )} | |
| /> | |
| </button> | |
| <AnimatePresence initial={false}> | |
| {isExpanded && ( | |
| <motion.div | |
| initial={{ height: 0 }} | |
| animate={{ height: "auto" }} | |
| exit={{ height: 0 }} | |
| transition={{ duration: 0.2, ease: "easeInOut" }} | |
| className="overflow-hidden" | |
| > | |
| <div className="px-4 pb-4 pt-2 border-t border-border/50"> | |
| {result && ( | |
| <div className="text-sm text-muted-foreground max-h-48 overflow-y-auto whitespace-pre-wrap rounded-lg bg-background/50 p-3"> | |
| {result} | |
| </div> | |
| )} | |
| {!result && status === "running" && ( | |
| <div className="flex items-center gap-2 text-sm text-blue-500"> | |
| <Loader2 className="w-4 h-4 animate-spin" /> | |
| <span>Fetching results...</span> | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| } | |