import { AgentMessage, AgentMessageType } from "@vertesia/common"; import { Button, cn } from "@vertesia/ui/core"; import { EyeIcon, EyeOffIcon } from "lucide-react"; import React, { useEffect, useRef, useState } from "react"; import { useUITranslation } from '../../../i18n/index.js'; import { AnimatedThinkingDots, PulsatingCircle, PulsingMessageLoader } from "./AnimatedThinkingDots"; import MessageItem from "./ModernAgentOutput/MessageItem"; import { ThinkingMessages } from "./WaitingMessages"; interface SlidingThinkingIndicatorProps { messages: AgentMessage[]; isCompleted: boolean; thinkingMessageIndex: number; className?: string; renderMessage?: (message: AgentMessage) => React.ReactNode; // Optional props to control showDetails from parent showDetails?: boolean; onShowDetailsChange?: (show: boolean) => void; } type ViewMode = "sliding" | "stacked"; export function SlidingThinkingIndicator({ messages, isCompleted, thinkingMessageIndex, className, renderMessage, showDetails: externalShowDetails, onShowDetailsChange, }: SlidingThinkingIndicatorProps) { const { t } = useUITranslation(); // Use external state if provided, otherwise use internal state const [internalShowDetails, setInternalShowDetails] = useState(false); // Use external state if provided, otherwise use internal state const showDetails = externalShowDetails !== undefined ? externalShowDetails : internalShowDetails; // Function to update showDetails state (internal or external) const setShowDetails = (value: boolean) => { if (onShowDetailsChange) { onShowDetailsChange(value); } else { setInternalShowDetails(value); } }; const [viewMode, setViewMode] = useState(() => { // Initialize from localStorage if available if (typeof window !== "undefined") { const savedMode = window.localStorage.getItem("agent-view-mode"); return (savedMode === "stacked" ? "stacked" : "sliding") as ViewMode; } return "sliding"; }); const [visibleMessage, setVisibleMessage] = useState(null); const [isTransitioning, setIsTransitioning] = useState(false); const timeoutRef = useRef(null); // Listen for view mode changes from the top bar useEffect(() => { const handleViewModeChange = () => { if (typeof window !== "undefined") { const newMode = window.localStorage.getItem("agent-view-mode"); setViewMode(newMode === "stacked" ? "stacked" : "sliding"); } }; window.addEventListener("agent-view-mode-changed", handleViewModeChange); return () => { window.removeEventListener("agent-view-mode-changed", handleViewModeChange); }; }, []); // Filter for thinking-type messages const thinkingMessages = messages.filter( (msg) => msg.type === AgentMessageType.THOUGHT || msg.type === AgentMessageType.UPDATE || msg.type === AgentMessageType.PLAN, ); // Track recent messages for cascading display const [recentMessages, setRecentMessages] = useState([]); // Keep track of the previous permanent message count const [prevPermanentCount, setPrevPermanentCount] = useState(0); // Update messages to display when thinking messages change useEffect(() => { // Show details is enabled - don't need to manage sliding messages if (showDetails) { return; } // Get permanent messages to detect when a new permanent message arrives const permanentMessages = getPermanentMessages(messages); // Check if we received a new permanent message const receivedNewPermanentMessage = permanentMessages.length > prevPermanentCount; // Update previous permanent message count for next check if (permanentMessages.length !== prevPermanentCount) { setPrevPermanentCount(permanentMessages.length); } if (thinkingMessages.length > 0 && !isCompleted) { // Get sorted recent messages (latest 3) const sortedMessages = [...thinkingMessages] .sort((a, b) => { const timeA = typeof a.timestamp === "number" ? a.timestamp : new Date(a.timestamp).getTime(); const timeB = typeof b.timestamp === "number" ? b.timestamp : new Date(b.timestamp).getTime(); return timeB - timeA; }) .slice(0, 3); // Get the latest message const latestMessage = sortedMessages[0]; // Clear messages when we get a new permanent message (e.g., after a pause) if (receivedNewPermanentMessage) { setVisibleMessage(null); setRecentMessages([]); setIsTransitioning(false); // If there are new thinking messages after the permanent message, show them if (latestMessage) { setTimeout(() => { setVisibleMessage(latestMessage); setRecentMessages(sortedMessages); }, 500); // Small delay to ensure UI updates correctly } return; } // Check if we have new messages const hasNewMessage = !visibleMessage || (latestMessage && // Compare timestamps properly accounting for different formats (typeof latestMessage.timestamp === "number" || typeof visibleMessage.timestamp === "number" ? // If either is a number, convert both to numbers for comparison (typeof latestMessage.timestamp === "number" ? latestMessage.timestamp : new Date(latestMessage.timestamp).getTime()) !== (typeof visibleMessage.timestamp === "number" ? visibleMessage.timestamp : new Date(visibleMessage.timestamp).getTime()) : latestMessage.timestamp !== visibleMessage.timestamp)); if (hasNewMessage) { // If we already have a message, transition it out before showing the new one if (visibleMessage) { setIsTransitioning(true); // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current); } // Set timeout to switch to the new messages after transition timeoutRef.current = setTimeout(() => { setVisibleMessage(latestMessage); setRecentMessages(sortedMessages); // Small delay before showing the new message (for better visual effect) setTimeout(() => { setIsTransitioning(false); }, 50); }, 400); // Match with our animation duration } else { // No current message, just show the new ones setVisibleMessage(latestMessage); setRecentMessages(sortedMessages); // Add a small delay for animation to start from the correct state setTimeout(() => { setIsTransitioning(false); }, 10); } } else if (recentMessages.length === 0) { // Initial setup - populate recent messages setRecentMessages(sortedMessages); } } else if (isCompleted && !showDetails) { // When completed, don't remove messages if showDetails is true // Only fade out the message if we're completed and not showing details if (visibleMessage) { setIsTransitioning(true); // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current); } // Set timeout to remove the message after transition timeoutRef.current = setTimeout(() => { setVisibleMessage(null); setRecentMessages([]); }, 400); // Match with our animation duration } } // Cleanup timeout on unmount return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [messages, isCompleted, showDetails, prevPermanentCount]); // Choose the color based on message type (using valid color values) const getThinkingColor = (message: AgentMessage | null): "blue" | "purple" | "teal" | "green" => { if (!message) return "blue"; switch (message.type) { case AgentMessageType.THOUGHT: return "purple"; case AgentMessageType.PLAN: return "teal"; case AgentMessageType.UPDATE: default: return "blue"; } }; // Get the messageText with thinking message replacement const getMessageText = (message: AgentMessage | null): string => { if (!message) return ThinkingMessages[thinkingMessageIndex]; let messageText = message.message || ThinkingMessages[thinkingMessageIndex]; // Replace %thinking_message% placeholder with a thinking message if (messageText.includes("%thinking_message%")) { messageText = messageText.replace(/%thinking_message%/g, ThinkingMessages[thinkingMessageIndex]); } return messageText; }; // Using the modernized MessageItem component to render thinking messages const ThinkingMessageItem = React.memo(({ message }: { message: AgentMessage }) => { // Replace the textContent with the thinking message text if needed const modifiedMessage = { ...message, message: getMessageText(message), }; // Simple implementation with just MessageItem and PulsatingCircle return (
); }); // Use provided render function or our component const renderThinkingMessage = renderMessage || ((message: AgentMessage) => ); // Sort thinking messages by timestamp (reverse order - newest first) const sortedThinkingMessages = [...thinkingMessages].sort((a, b) => { const timeA = typeof a.timestamp === "number" ? a.timestamp : new Date(a.timestamp).getTime(); const timeB = typeof b.timestamp === "number" ? b.timestamp : new Date(b.timestamp).getTime(); return timeB - timeA; // Reversed order - newest first }); return (
{thinkingMessages.length > 0 && (
)} {/* Render based on view mode */} { (showDetails || thinkingMessages.length > 0) && (showDetails ? ( // Show details view - always show all thinking messages regardless of completion state
{sortedThinkingMessages.map((message, index) => (
{renderThinkingMessage(message)}
))}
) : !isCompleted ? ( // Not showing details and not completed - use sliding/stacked view viewMode === "sliding" ? ( // Progressive Fading View - Show recent messages with fading effect
{/* For each recent message, render with different opacity based on recency */} {recentMessages.map((message, index) => (
{/* Only show pulsating circle for the most recent message */} {index === 0 ? ( renderThinkingMessage(message) ) : (
{/* Add an indicator based on message type */}
{getThinkingColor(message) === "blue" && (
)} {getThinkingColor(message) === "purple" && ( )} {getThinkingColor(message) === "teal" && ( )}
{getMessageText(message)}
)}
))}
) : ( // Stacked View - Show all thinking messages
{sortedThinkingMessages.map((message, index) => (
{renderThinkingMessage(message)}
))}
) ) : null) // When completed and not showing details, don't show anything }
); } // Helper function to determine if a message is a thinking message (transient) export function isThinkingMessage(message: AgentMessage): boolean { return ( message.type === AgentMessageType.THOUGHT || message.type === AgentMessageType.UPDATE || message.type === AgentMessageType.PLAN || message.type === AgentMessageType.ERROR || message.type === AgentMessageType.WARNING || message.type === AgentMessageType.SYSTEM ); } // Helper function to determine if a message is a permanent message (always displayed) export function isPermanentMessage(message: AgentMessage): boolean { return ( message.type === AgentMessageType.ANSWER || message.type === AgentMessageType.QUESTION || message.type === AgentMessageType.COMPLETE || message.type === AgentMessageType.IDLE || message.type === AgentMessageType.REQUEST_INPUT ); } // Helper function to get all thinking messages export function getThinkingMessages(messages: AgentMessage[]): AgentMessage[] { return messages.filter(isThinkingMessage); } // Helper function to get all permanent messages export function getPermanentMessages(messages: AgentMessage[]): AgentMessage[] { return messages.filter(isPermanentMessage); }