sunatest / frontend /src /components /thread /chat-input /floating-tool-preview.tsx
llama1's picture
Upload 781 files
5da4770 verified
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CircleDashed, Maximize2 } from 'lucide-react';
import { getToolIcon, getUserFriendlyToolName } from '@/components/thread/utils';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
export interface ToolCallInput {
assistantCall: {
content?: string;
name?: string;
timestamp?: string;
};
toolResult?: {
content?: string;
isSuccess?: boolean;
timestamp?: string;
};
messages?: any[];
}
interface FloatingToolPreviewProps {
toolCalls: ToolCallInput[];
currentIndex: number;
onExpand: () => void;
agentName?: string;
isVisible: boolean;
}
const FLOATING_LAYOUT_ID = 'tool-panel-float';
const CONTENT_LAYOUT_ID = 'tool-panel-content';
const getToolResultStatus = (toolCall: any): boolean => {
const content = toolCall?.toolResult?.content;
if (!content) return toolCall?.toolResult?.isSuccess ?? true;
const safeParse = (data: any) => {
try { return typeof data === 'string' ? JSON.parse(data) : data; }
catch { return null; }
};
const parsed = safeParse(content);
if (!parsed) return toolCall?.toolResult?.isSuccess ?? true;
if (parsed.content) {
const inner = safeParse(parsed.content);
if (inner?.tool_execution?.result?.success !== undefined) {
return inner.tool_execution.result.success;
}
}
const success = parsed.tool_execution?.result?.success ??
parsed.result?.success ??
parsed.success;
return success !== undefined ? success : (toolCall?.toolResult?.isSuccess ?? true);
};
export const FloatingToolPreview: React.FC<FloatingToolPreviewProps> = ({
toolCalls,
currentIndex,
onExpand,
agentName,
isVisible,
}) => {
const [isExpanding, setIsExpanding] = React.useState(false);
const currentToolCall = toolCalls[currentIndex];
const totalCalls = toolCalls.length;
React.useEffect(() => {
if (isVisible) {
setIsExpanding(false);
}
}, [isVisible]);
if (!currentToolCall || totalCalls === 0) return null;
const toolName = currentToolCall.assistantCall?.name || 'Tool Call';
const CurrentToolIcon = getToolIcon(toolName);
const isStreaming = currentToolCall.toolResult?.content === 'STREAMING';
const isSuccess = isStreaming ? true : getToolResultStatus(currentToolCall);
const handleClick = () => {
setIsExpanding(true);
requestAnimationFrame(() => {
onExpand();
});
};
return (
<AnimatePresence>
{isVisible && (
<motion.div
layoutId={FLOATING_LAYOUT_ID}
layout
transition={{
layout: {
type: "spring",
stiffness: 300,
damping: 30
}
}}
className="-mb-4 w-full"
style={{ pointerEvents: 'auto' }}
>
<motion.div
layoutId={CONTENT_LAYOUT_ID}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="bg-card border border-border rounded-3xl p-2 w-full cursor-pointer group"
onClick={handleClick}
style={{ opacity: isExpanding ? 0 : 1 }}
>
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<motion.div
layoutId="tool-icon"
className={cn(
"w-10 h-10 rounded-2xl flex items-center justify-center",
isStreaming
? "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
: isSuccess
? "bg-green-50 dark:bg-green-900/20 border border-green-300 dark:border-green-800"
: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
)}
style={{ opacity: isExpanding ? 0 : 1 }}
>
{isStreaming ? (
<CircleDashed className="h-5 w-5 text-blue-500 dark:text-blue-400 animate-spin" style={{ opacity: isExpanding ? 0 : 1 }} />
) : (
<CurrentToolIcon className="h-5 w-5 text-foreground" style={{ opacity: isExpanding ? 0 : 1 }} />
)}
</motion.div>
</div>
<div className="flex-1 min-w-0" style={{ opacity: isExpanding ? 0 : 1 }}>
<motion.div layoutId="tool-title" className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-medium text-foreground truncate">
{getUserFriendlyToolName(toolName)}
</h4>
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">
{currentIndex + 1}/{totalCalls}
</span>
</div>
</motion.div>
<motion.div layoutId="tool-status" className="flex items-center gap-2">
<div className={cn(
"w-2 h-2 rounded-full",
isStreaming
? "bg-blue-500 animate-pulse"
: isSuccess
? "bg-green-500"
: "bg-red-500"
)} />
<span className="text-xs text-muted-foreground truncate">
{isStreaming
? `${agentName || 'Suna'} is working...`
: isSuccess
? "Success"
: "Failed"
}
</span>
</motion.div>
</div>
<Button value='ghost' className="bg-transparent hover:bg-transparent flex-shrink-0" style={{ opacity: isExpanding ? 0 : 1 }}>
<Maximize2 className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
</Button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};