import React, { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { FileText, Plus, X, GripVertical, Sparkles, Zap, Coffee, Moon, Star, AlertCircle, HelpCircle, Upload, Edit, ArrowLeft, Settings, GitCompare, Clock, Play, Save, Download, } from "lucide-react"; import { WelcomeGuideModal } from "./WelcomeGuideModal"; import { useAgentGraph } from "@/context/AgentGraphContext"; import { SplitterSelectionModal, SplitterType, } from "./modals/SplitterSelectionModal"; import { useNotification } from "@/context/NotificationContext"; import { useNavigation } from "@/context/NavigationContext"; import { useTaskPolling } from "@/hooks/useTaskPolling"; import { api } from "@/lib/api"; // Add custom glow animation styles const glowStyle = ` @keyframes magical-glow { 0%, 100% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6), 0 0 40px rgba(59, 130, 246, 0.3); transform: scale(1); } 50% { box-shadow: 0 0 35px rgba(59, 130, 246, 0.9), 0 0 70px rgba(59, 130, 246, 0.5); transform: scale(1.05); } } `; // Inject styles into head if (typeof document !== "undefined") { const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = glowStyle; if (!document.head.querySelector("style[data-floating-widget]")) { styleSheet.setAttribute("data-floating-widget", "true"); document.head.appendChild(styleSheet); } } // FloatingActionWidget now determines actions based on current app state // Pet mood and behavior types type PetMood = "happy" | "sleepy" | "excited" | "curious" | "working" | "bored"; type PetMessage = { text: string; mood: PetMood; emoji: string; }; const PET_MESSAGES: PetMessage[] = [ { text: "🎉 Need help getting started?", mood: "excited", emoji: "🎉" }, { text: "☕ Time for a coffee break?", mood: "sleepy", emoji: "☕" }, { text: "🤔 Wondering what to do next?", mood: "curious", emoji: "🤔" }, { text: "⚡ Ready to process some data?", mood: "working", emoji: "⚡" }, { text: "🌟 You're doing great!", mood: "happy", emoji: "🌟" }, { text: "😴 Haven't seen you in a while...", mood: "bored", emoji: "😴" }, { text: "🚀 Let's analyze some traces!", mood: "excited", emoji: "🚀" }, { text: "💡 Click me for quick actions!", mood: "curious", emoji: "💡" }, ]; const getMoodIcon = (mood: PetMood) => { switch (mood) { case "happy": return Star; case "sleepy": return Moon; case "excited": return Zap; case "curious": return HelpCircle; case "working": return Coffee; case "bored": return AlertCircle; default: return Sparkles; } }; const getMoodColor = (mood: PetMood) => { switch (mood) { case "happy": return "text-yellow-400"; case "sleepy": return "text-blue-400"; case "excited": return "text-orange-400"; case "curious": return "text-purple-400"; case "working": return "text-green-400"; case "bored": return "text-gray-400"; default: return "text-white"; } }; // Define contextual actions based on current view interface ContextualAction { id: string; label: string; icon: React.ElementType; onClick: () => void; variant?: "default" | "outline" | "destructive"; } export function FloatingActionWidget() { const { state, actions } = useAgentGraph(); const { showNotification } = useNotification(); const { actions: navigationActions } = useNavigation(); // Add states for splitter modal and generation const [isSplitterModalOpen, setIsSplitterModalOpen] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(0); const [isExpanded, setIsExpanded] = useState(false); const [isDragging, setIsDragging] = useState(false); const [showTooltip, setShowTooltip] = useState(true); const [hasMoved, setHasMoved] = useState(false); const [currentMood, setCurrentMood] = useState("curious"); const [currentMessage, setCurrentMessage] = useState({ text: "💡 If you don't know what to do, click me!", mood: "curious", emoji: "💡", }); const [isAnimating, setIsAnimating] = useState(false); const [petScale, setPetScale] = useState(1); const [lastInteraction, setLastInteraction] = useState(Date.now()); const [position, setPosition] = useState({ x: typeof window !== "undefined" ? window.innerWidth - 80 : 20, // Bottom right corner y: typeof window !== "undefined" ? window.innerHeight - 80 : 20, }); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [isWelcomeModalOpen, setIsWelcomeModalOpen] = useState(false); const [isAtChartCenter, setIsAtChartCenter] = useState(false); const [chartCenterPosition, setChartCenterPosition] = useState<{ x: number; y: number; } | null>(null); const widgetRef = useRef(null); // Helper to show both toast and persistent notifications const showSystemNotification = React.useCallback( (notification: { type: "success" | "error" | "warning" | "info"; title: string; message: string; }) => { // Show toast notification showNotification(notification); // Add to persistent notifications for all types navigationActions.addNotification({ type: notification.type, title: notification.title, message: notification.message, }); }, [showNotification, navigationActions] ); // Task polling setup for knowledge graph generation const { pollTaskStatus } = useTaskPolling({ onSuccess: (taskId) => { console.log("Knowledge graph generation completed:", taskId); showSystemNotification({ type: "success", title: "Agent Graph Generation Complete", message: "Your agent graph has been generated successfully!", }); setIsGenerating(false); setGenerationProgress(0); // Refresh the current view if we're still on trace-kg if (state.activeView === "trace-kg") { // The TraceKnowledgeGraphView will handle refreshing its own data window.location.reload(); // Simple refresh for now } }, onError: (error, taskId) => { console.error("Knowledge graph generation failed:", error, taskId); // Check if this is a timeout vs a real failure const isTimeout = error.includes("timeout") || error.includes("timed out"); showSystemNotification({ type: isTimeout ? "warning" : "error", title: isTimeout ? "Generation Taking Longer Than Expected" : "Agent Graph Generation Failed", message: isTimeout ? `${error} You can refresh the page to check if it completed.` : error, }); // Only reset state if it's not a timeout (user might want to keep waiting) if (!isTimeout) { setIsGenerating(false); setGenerationProgress(0); } }, onProgress: (progress, message) => { setGenerationProgress(progress); if (message) { console.log("Generation progress:", progress, message); } }, maxAttempts: 180, // 15 minutes with exponential backoff interval: 5000, // Start with 5 seconds enableExponentialBackoff: true, }); // Handle splitter confirmation for graph generation const handleSplitterConfirm = async ( splitterType: SplitterType, methodName?: string, modelName?: string, chunkingConfig?: { min_chunk_size?: number; max_chunk_size?: number } ) => { const finalMethodName = methodName || "production"; const selectedTrace = state.selectedTrace; if (!selectedTrace) { showNotification({ type: "error", title: "No Trace Selected", message: "Please select a trace first.", }); return; } console.log("FloatingActionWidget: Using method name:", finalMethodName); console.log("FloatingActionWidget: Using chunking config:", chunkingConfig); setIsGenerating(true); setIsSplitterModalOpen(false); setGenerationProgress(0); try { const response = await api.traces.generateKnowledgeGraph( selectedTrace.trace_id, splitterType, true, // force_regenerate = true to allow generating new graphs even if existing ones exist finalMethodName, modelName || "gpt-5-mini", chunkingConfig ); showSystemNotification({ type: "info", title: "Agent Graph Generation Started", message: `Using ${getSplitterDisplayName( splitterType )} splitter with ${finalMethodName} method. This may take several minutes.`, }); // Start polling for task status if we got a task_id if (response.task_id) { pollTaskStatus(response.task_id); } else { // If no task_id, just refresh immediately and mark as complete setIsGenerating(false); showSystemNotification({ type: "success", title: "Agent Graph Generation Complete", message: "Your agent graph has been generated successfully!", }); // Refresh the page to show new graphs window.location.reload(); } } catch (error) { console.error("Failed to generate knowledge graph:", error); showNotification({ type: "error", title: "Agent Graph Generation Failed", message: "Failed to start agent graph generation. Please try again.", }); setIsGenerating(false); setGenerationProgress(0); } }; // Helper function to get display name for splitter type const getSplitterDisplayName = (splitterType: SplitterType) => { switch (splitterType) { case "agent_semantic": return "Agent Semantic"; case "json": return "JSON"; case "prompt_interaction": return "Prompt Interaction"; default: return splitterType; } }; // Hide tooltip after 6 seconds or when expanded useEffect(() => { const timer = setTimeout(() => { setShowTooltip(false); }, 6000); if (isExpanded) { setShowTooltip(false); } return () => clearTimeout(timer); }, [isExpanded]); // Smart positioning based on current view useEffect(() => { const updatePosition = () => { if (isDragging || isExpanded) return; // Check if we're on welcome view (which shows dashboard) const isDashboard = state.activeView === "welcome"; if (isDashboard) { // Try to find the risk distribution chart const chartElement = document.getElementById("risk-distribution-chart"); if (chartElement) { const chartRect = chartElement.getBoundingClientRect(); const centerX = chartRect.left + chartRect.width / 2 - 30; // Account for widget width const centerY = chartRect.top + chartRect.height / 2 - 30; // Account for widget height setChartCenterPosition({ x: centerX, y: centerY }); setPosition({ x: centerX, y: centerY }); setIsAtChartCenter(true); return; } } // Default position (bottom-right corner) setPosition({ x: window.innerWidth - 80, y: window.innerHeight - 80, }); setIsAtChartCenter(false); setChartCenterPosition(null); }; // Initial positioning updatePosition(); // Update on view changes const handleResize = () => { if (!isDragging && !isExpanded) { updatePosition(); } }; window.addEventListener("resize", handleResize); // Also update when DOM changes (chart appears/disappears) const observer = new MutationObserver(() => { if (!isDragging && !isExpanded) { setTimeout(updatePosition, 100); // Small delay to ensure DOM is updated } }); observer.observe(document.body, { childList: true, subtree: true }); return () => { window.removeEventListener("resize", handleResize); observer.disconnect(); }; }, [isDragging, isExpanded, state.activeView]); // Pet behavior system - random movements and mood changes useEffect(() => { const changeMoodAndMessage = () => { if (PET_MESSAGES.length === 0) return; const randomMessage = PET_MESSAGES[Math.floor(Math.random() * PET_MESSAGES.length)]; if (!randomMessage) return; setCurrentMessage(randomMessage); setCurrentMood(randomMessage.mood); setShowTooltip(true); // Animate scale for attention setIsAnimating(true); setPetScale(1.2); setTimeout(() => { setPetScale(1); setIsAnimating(false); }, 300); // Hide tooltip after showing message setTimeout(() => setShowTooltip(false), 4000); }; // Initial mood change after 3 seconds const initialTimer = setTimeout(changeMoodAndMessage, 3000); // Random mood changes every 15-45 seconds const moodInterval = setInterval(() => { const timeSinceLastInteraction = Date.now() - lastInteraction; // More frequent changes if user hasn't interacted recently if (timeSinceLastInteraction > 60000) { // 1 minute changeMoodAndMessage(); } else if (Math.random() < 0.3) { // 30% chance changeMoodAndMessage(); } }, 20000); return () => { clearTimeout(initialTimer); clearInterval(moodInterval); }; }, [lastInteraction]); // Random movement behavior (when not being dragged) useEffect(() => { if (isDragging || isExpanded) return; const randomMove = () => { const moveChance = Math.random(); if (moveChance < 0.2) { // 20% chance to move const margin = 100; const newX = margin + Math.random() * (window.innerWidth - 300 - margin); const newY = margin + Math.random() * (window.innerHeight - 200 - margin); setPosition({ x: newX, y: newY }); setIsAnimating(true); // Show a playful message during movement const playfulMessages = [ { text: "🏃‍♂️ Just stretching my legs!", mood: "excited" as PetMood, emoji: "🏃‍♂️", }, { text: "🔄 Change of scenery!", mood: "happy" as PetMood, emoji: "🔄", }, { text: "👀 Exploring your workspace!", mood: "curious" as PetMood, emoji: "👀", }, ]; const randomMsg = playfulMessages[Math.floor(Math.random() * playfulMessages.length)]; if (!randomMsg) return; setCurrentMessage(randomMsg); setCurrentMood(randomMsg.mood); setShowTooltip(true); setTimeout(() => { setIsAnimating(false); setShowTooltip(false); }, 2000); } }; // Check for random movement every 30-60 seconds const moveInterval = setInterval(randomMove, 45000); return () => clearInterval(moveInterval); }, [isDragging, isExpanded]); // Handle mouse down for dragging const handleMouseDown = (e: React.MouseEvent) => { if (!widgetRef.current) return; e.preventDefault(); e.stopPropagation(); // Calculate offset based on current position, not DOM rect setDragOffset({ x: e.clientX - position.x, y: e.clientY - position.y, }); // Reset movement flag and start dragging immediately for responsive feel setHasMoved(false); setIsDragging(true); }; // Handle mouse move for dragging with proper offset calculation useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isDragging) return; e.preventDefault(); // Calculate new position based on mouse position and initial offset const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; // Widget is always 56px regardless of expanded state // (expanded panel is positioned separately) const widgetWidth = 56; const widgetHeight = 56; const maxX = window.innerWidth - widgetWidth - 10; const maxY = window.innerHeight - widgetHeight - 10; const boundedX = Math.max(10, Math.min(newX, maxX)); const boundedY = Math.max(10, Math.min(newY, maxY)); // Update position state immediately setPosition({ x: boundedX, y: boundedY }); // Mark that widget has moved (for click vs drag detection) setHasMoved(true); }; const handleMouseUp = () => { setIsDragging(false); // Reset movement flag after a brief delay setTimeout(() => setHasMoved(false), 50); // Magnet effect - return to chart center if on dashboard if ( state.activeView === "welcome" && chartCenterPosition && !isExpanded ) { setTimeout(() => { setPosition(chartCenterPosition); setIsAtChartCenter(true); }, 2000); // Wait 2 seconds then snap back } }; if (isDragging) { document.addEventListener("mousemove", handleMouseMove, { passive: false, }); document.addEventListener("mouseup", handleMouseUp); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging, dragOffset, isExpanded]); // Toggle expanded state const toggleExpanded = () => { // Prevent toggle during dragging or if widget was just moved if (isDragging || hasMoved) return; setIsExpanded(!isExpanded); setLastInteraction(Date.now()); // Update interaction time }; // Show tooltip on hover after initial dismissal const handleMouseEnter = () => { if (!isExpanded && !showTooltip) { setShowTooltip(true); setTimeout(() => setShowTooltip(false), 3000); } }; // Dismiss tooltip when mouse leaves const handleMouseLeave = () => { if (showTooltip) { setTimeout(() => setShowTooltip(false), 1000); } }; // Calculate expanded panel position to avoid overlap const getExpandedPosition = () => { const panelWidth = 288; // w-72 = 288px const panelHeight = 400; // approximate height const margin = 10; // Try to position to the left first let expandedX = position.x - panelWidth - margin; let expandedY = position.y; // If it goes off the left edge, position to the right if (expandedX < margin) { expandedX = position.x + 56 + margin; // 56px is widget width } // If it goes off the right edge, center it if (expandedX + panelWidth > window.innerWidth - margin) { expandedX = Math.max(margin, (window.innerWidth - panelWidth) / 2); } // Adjust vertical position if needed if (expandedY + panelHeight > window.innerHeight - margin) { expandedY = window.innerHeight - panelHeight - margin; } if (expandedY < margin) { expandedY = margin; } return { x: expandedX, y: expandedY }; }; const expandedPosition = getExpandedPosition(); // Generate contextual actions based on current view and state const getContextualActions = (): ContextualAction[] => { const { activeView, selectedTrace, selectedKnowledgeGraph } = state; switch (activeView) { case "welcome": { const welcomeActions: ContextualAction[] = [ { id: "upload-trace", label: "Upload Trace", icon: Upload, onClick: () => actions.setActiveView("upload"), }, { id: "browse-examples", label: "Browse Examples", icon: FileText, onClick: () => actions.setActiveView("example-traces"), }, { id: "compare-graphs", label: "Compare Graphs", icon: GitCompare, onClick: () => actions.setActiveView("graph-comparison"), }, ]; return welcomeActions; } case "trace-kg": { const traceActions: ContextualAction[] = [ { id: "edit-trace", label: "Edit Trace", icon: Edit, onClick: () => actions.setActiveView("trace-editor"), }, { id: "back-traces", label: "Back to Traces", icon: ArrowLeft, onClick: () => actions.setActiveView("traces"), }, ]; // Add generate graph action if no graphs exist for this trace if (selectedTrace) { const hasGraphs = state.knowledgeGraphs.some( (kg) => kg.filename === selectedTrace.filename ); if (!hasGraphs) { traceActions.unshift({ id: "generate-graph", label: isGenerating ? generationProgress > 0 ? `Generating... ${Math.round(generationProgress)}%` : "Generating..." : "Generate Graph", icon: Plus, onClick: () => { // Use the same splitter selection flow as the trace information page if (!isGenerating) { setIsSplitterModalOpen(true); } }, }); } } return traceActions; } case "kg-visualizer": return [ { id: "advanced-processing", label: "Advanced Processing", icon: Settings, onClick: () => actions.setActiveView("advanced-processing"), }, { id: "temporal-view", label: "Temporal View", icon: Clock, onClick: () => actions.setActiveView("temporal-visualizer"), }, { id: "back-trace", label: "Back to Trace", icon: ArrowLeft, onClick: () => actions.setActiveView("trace-kg"), }, ]; case "graph-comparison": return [ { id: "clear-selection", label: "Clear Selection", icon: X, onClick: () => { // Clear comparison selection window.location.reload(); }, }, { id: "back-dashboard", label: "Back to Dashboard", icon: ArrowLeft, onClick: () => actions.setActiveView("welcome"), }, ]; case "traces": return [ { id: "upload-trace", label: "Upload Trace", icon: Upload, onClick: () => actions.setActiveView("upload"), }, { id: "browse-examples", label: "Browse Examples", icon: FileText, onClick: () => actions.setActiveView("example-traces"), }, { id: "back-dashboard", label: "Back to Dashboard", icon: ArrowLeft, onClick: () => actions.setActiveView("welcome"), }, ]; case "example-traces": return [ { id: "back-dashboard", label: "Back to Dashboard", icon: ArrowLeft, onClick: () => actions.setActiveView("welcome"), }, ]; case "trace-editor": return [ { id: "save-trace", label: "Save Changes", icon: Save, onClick: () => { // Save trace changes if (selectedTrace) { actions.updateTrace(selectedTrace.trace_id, { /* updated data */ }); } }, }, { id: "back-trace", label: "Back to Trace", icon: ArrowLeft, onClick: () => actions.setActiveView("trace-kg"), }, ]; case "advanced-processing": return [ { id: "run-processing", label: "Run Processing", icon: Play, onClick: () => { // Trigger advanced processing if (selectedKnowledgeGraph) { // Start processing pipeline console.log( "Starting advanced processing for KG:", selectedKnowledgeGraph.id ); } }, }, { id: "back-visualizer", label: "Back to Visualizer", icon: ArrowLeft, onClick: () => actions.setActiveView("kg-visualizer"), }, ]; case "temporal-visualizer": return [ { id: "export-data", label: "Export Data", icon: Download, onClick: () => { // Export temporal data if (state.selectedTemporalData) { console.log( "Exporting temporal data:", state.selectedTemporalData ); } }, }, { id: "back-visualizer", label: "Back to Visualizer", icon: ArrowLeft, onClick: () => actions.setActiveView("kg-visualizer"), }, ]; default: return []; } }; const contextualActions = getContextualActions(); return ( <> {/* Collapsed Widget */}
{/* Pet Widget - Always visible */}
{React.createElement(getMoodIcon(currentMood), { className: `h-5 w-5 ${getMoodColor( currentMood )} transition-colors duration-500`, })}
{/* Pet tooltip with mood-based messages - only show when collapsed */} {showTooltip && !isExpanded && (
{currentMessage.text}
)}
{/* Expanded Panel - Positioned Separately */} {isExpanded && (
{/* Header with drag handle */}
Quick Actions
{/* New to AgentGraph Section */}

New to AgentGraph?

Learn how to get started

{/* Quick Actions */} {contextualActions.map((action) => ( ))}
)} {/* Splitter Selection Modal */} {/* Welcome Guide Modal */} ); }