mirror of https://github.com/kortix-ai/suna.git
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { UsagePreview } from './usage-preview';
|
|
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
|
|
import { isLocalMode } from '@/lib/config';
|
|
|
|
export interface ChatSnackProps {
|
|
// Tool preview props
|
|
toolCalls?: ToolCallInput[];
|
|
toolCallIndex?: number;
|
|
onExpandToolPreview?: () => void;
|
|
agentName?: string;
|
|
showToolPreview?: boolean;
|
|
|
|
// Usage preview props
|
|
showUsagePreview?: 'tokens' | 'upgrade' | false;
|
|
subscriptionData?: any;
|
|
onCloseUsage?: () => void;
|
|
onOpenUpgrade?: () => void;
|
|
|
|
// General props
|
|
isVisible?: boolean;
|
|
}
|
|
|
|
const SNACK_LAYOUT_ID = 'chat-snack-float';
|
|
const SNACK_CONTENT_LAYOUT_ID = 'chat-snack-content';
|
|
|
|
export const ChatSnack: React.FC<ChatSnackProps> = ({
|
|
toolCalls = [],
|
|
toolCallIndex = 0,
|
|
onExpandToolPreview,
|
|
agentName,
|
|
showToolPreview = false,
|
|
showUsagePreview = false,
|
|
subscriptionData,
|
|
onCloseUsage,
|
|
onOpenUpgrade,
|
|
isVisible = false,
|
|
}) => {
|
|
const [currentView, setCurrentView] = React.useState(0);
|
|
|
|
// Determine what notifications we have - match exact rendering conditions
|
|
const notifications = [];
|
|
|
|
// Tool notification: only if we have tool calls and showToolPreview is true
|
|
if (showToolPreview && toolCalls.length > 0) {
|
|
notifications.push('tool');
|
|
}
|
|
|
|
// Usage notification: must match ALL rendering conditions
|
|
if (showUsagePreview && !isLocalMode() && subscriptionData) {
|
|
notifications.push('usage');
|
|
}
|
|
|
|
const totalNotifications = notifications.length;
|
|
const hasMultiple = totalNotifications > 1;
|
|
|
|
// Reset currentView when notifications change
|
|
React.useEffect(() => {
|
|
if (currentView >= totalNotifications && totalNotifications > 0) {
|
|
setCurrentView(0);
|
|
}
|
|
}, [totalNotifications, currentView]);
|
|
|
|
// Auto-cycle through notifications
|
|
React.useEffect(() => {
|
|
if (!hasMultiple || !isVisible) return;
|
|
|
|
const interval = setInterval(() => {
|
|
setCurrentView((prev) => (prev + 1) % totalNotifications);
|
|
}, 20000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [hasMultiple, isVisible, totalNotifications, currentView]); // Reset timer when currentView changes
|
|
|
|
if (!isVisible || totalNotifications === 0) return null;
|
|
|
|
const currentNotification = notifications[currentView];
|
|
|
|
const renderContent = () => {
|
|
if (currentNotification === 'tool' && showToolPreview) {
|
|
return (
|
|
<FloatingToolPreview
|
|
toolCalls={toolCalls}
|
|
currentIndex={toolCallIndex}
|
|
onExpand={onExpandToolPreview || (() => { })}
|
|
agentName={agentName}
|
|
isVisible={true}
|
|
showIndicators={hasMultiple}
|
|
indicatorIndex={currentView}
|
|
indicatorTotal={totalNotifications}
|
|
onIndicatorClick={(index) => setCurrentView(index)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (currentNotification === 'usage' && showUsagePreview && !isLocalMode()) {
|
|
return (
|
|
<motion.div
|
|
layoutId={SNACK_LAYOUT_ID}
|
|
layout
|
|
transition={{
|
|
layout: {
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 30
|
|
}
|
|
}}
|
|
className="-mb-4 w-full"
|
|
style={{ pointerEvents: 'auto' }}
|
|
>
|
|
<motion.div
|
|
layoutId={SNACK_CONTENT_LAYOUT_ID}
|
|
className={cn(
|
|
"bg-card border border-border rounded-3xl p-2 w-full transition-all duration-200",
|
|
onOpenUpgrade && "cursor-pointer hover:shadow-md"
|
|
)}
|
|
whileHover={onOpenUpgrade ? { scale: 1.02 } : undefined}
|
|
whileTap={onOpenUpgrade ? { scale: 0.98 } : undefined}
|
|
onClick={(e) => {
|
|
// Don't trigger if clicking on indicators or close button
|
|
const target = e.target as HTMLElement;
|
|
const isIndicatorClick = target.closest('[data-indicator-click]');
|
|
const isCloseClick = target.closest('[data-close-click]');
|
|
|
|
if (!isIndicatorClick && !isCloseClick && onOpenUpgrade) {
|
|
onOpenUpgrade();
|
|
}
|
|
}}
|
|
>
|
|
<UsagePreview
|
|
type={showUsagePreview}
|
|
subscriptionData={subscriptionData}
|
|
onClose={() => {
|
|
if (onCloseUsage) onCloseUsage();
|
|
// If there are other notifications, switch to them
|
|
if (totalNotifications > 1) {
|
|
const remainingNotifications = notifications.filter(n => n !== 'usage');
|
|
if (remainingNotifications.length > 0) {
|
|
setCurrentView(0); // Switch to first remaining notification
|
|
}
|
|
}
|
|
}}
|
|
hasMultiple={hasMultiple}
|
|
showIndicators={hasMultiple}
|
|
currentIndex={currentView}
|
|
totalCount={totalNotifications}
|
|
onIndicatorClick={(index) => setCurrentView(index)}
|
|
onOpenUpgrade={onOpenUpgrade}
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence mode="wait">
|
|
{isVisible && (
|
|
<motion.div
|
|
key={currentNotification}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: 8 }}
|
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
>
|
|
{renderContent()}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
};
|