sempero / src /components /tool-invocation.tsx
armand0e's picture
gradio -> docker
250bf31
"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>
);
}