Spaces:
Running
Running
| 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<PetMood>("curious"); | |
| const [currentMessage, setCurrentMessage] = useState<PetMessage>({ | |
| 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<HTMLDivElement>(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 */} | |
| <div | |
| ref={widgetRef} | |
| className={`fixed ${ | |
| isDragging ? "cursor-grabbing select-none" : "cursor-grab" | |
| }`} | |
| style={{ | |
| left: `${position.x}px`, | |
| top: `${position.y}px`, | |
| zIndex: 9999, | |
| pointerEvents: "auto", | |
| transition: isDragging ? "none" : "all 0.1s ease", | |
| }} | |
| > | |
| {/* Pet Widget - Always visible */} | |
| <div className="relative"> | |
| <div | |
| className={`w-14 h-14 bg-primary hover:bg-primary/90 rounded-full shadow-lg flex flex-col items-center justify-center cursor-pointer transition-all duration-200 hover:scale-110 relative ${ | |
| showTooltip ? "shadow-primary/50 shadow-2xl" : "" | |
| } ${isDragging ? "scale-110 shadow-2xl opacity-90" : ""} ${ | |
| isAnimating ? "animate-pulse" : "" | |
| } ${isExpanded ? "ring-2 ring-primary/50 ring-offset-2" : ""} ${ | |
| isAtChartCenter | |
| ? "ring-4 ring-blue-400/50 ring-offset-4 shadow-xl shadow-blue-500/30 scale-110" | |
| : "" | |
| }`} | |
| onMouseDown={handleMouseDown} | |
| onMouseEnter={handleMouseEnter} | |
| onMouseLeave={handleMouseLeave} | |
| onClick={toggleExpanded} | |
| style={{ | |
| animation: showTooltip | |
| ? "magical-glow 2s ease-in-out infinite" | |
| : undefined, | |
| transition: isDragging ? "none" : "all 0.2s ease", | |
| transform: `scale(${petScale})`, | |
| }} | |
| > | |
| {React.createElement(getMoodIcon(currentMood), { | |
| className: `h-5 w-5 ${getMoodColor( | |
| currentMood | |
| )} transition-colors duration-500`, | |
| })} | |
| </div> | |
| {/* Pet tooltip with mood-based messages - only show when collapsed */} | |
| {showTooltip && !isExpanded && ( | |
| <div className="absolute -top-16 -right-4 w-48 bg-black/90 text-white text-xs px-3 py-2 rounded-lg shadow-lg animate-bounce z-10"> | |
| <div className="text-center">{currentMessage.text}</div> | |
| <div className="absolute top-full right-6 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-black/90"></div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Expanded Panel - Positioned Separately */} | |
| {isExpanded && ( | |
| <div | |
| className="fixed" | |
| style={{ | |
| left: `${expandedPosition.x}px`, | |
| top: `${expandedPosition.y}px`, | |
| zIndex: 10000, | |
| pointerEvents: "auto", | |
| }} | |
| > | |
| <Card className="w-72 shadow-xl border-2 border-primary/20 bg-white/95 backdrop-blur-sm"> | |
| {/* Header with drag handle */} | |
| <div | |
| className="flex items-center justify-between p-3 bg-primary/5 border-b cursor-grab active:cursor-grabbing" | |
| onMouseDown={handleMouseDown} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <GripVertical className="h-4 w-4 text-muted-foreground" /> | |
| <span className="font-medium text-sm">Quick Actions</span> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={toggleExpanded} | |
| className="h-6 w-6 p-0" | |
| > | |
| <X className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| {/* New to AgentGraph Section */} | |
| <div className="px-4 py-3 bg-gradient-to-br from-primary/5 to-primary/10 border-b"> | |
| <div className="flex items-center gap-3 mb-3"> | |
| <div className="p-2 rounded-lg bg-primary/10"> | |
| <Sparkles className="h-4 w-4 text-primary" /> | |
| </div> | |
| <div className="flex-1"> | |
| <h3 className="font-semibold text-sm">New to AgentGraph?</h3> | |
| <p className="text-xs text-muted-foreground"> | |
| Learn how to get started | |
| </p> | |
| </div> | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="w-full gap-2 h-8" | |
| onClick={() => setIsWelcomeModalOpen(true)} | |
| > | |
| <HelpCircle className="h-3 w-3" /> | |
| <span className="text-xs">View Guide</span> | |
| </Button> | |
| </div> | |
| {/* Quick Actions */} | |
| <CardContent className="p-4 space-y-3"> | |
| {contextualActions.map((action) => ( | |
| <Button | |
| key={action.id} | |
| variant={action.variant || "default"} | |
| size="sm" | |
| onClick={action.onClick} | |
| className="w-full justify-start gap-2" | |
| > | |
| {React.createElement(action.icon, { className: "h-4 w-4" })} | |
| {action.label} | |
| </Button> | |
| ))} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| )} | |
| {/* Splitter Selection Modal */} | |
| <SplitterSelectionModal | |
| open={isSplitterModalOpen} | |
| onOpenChange={setIsSplitterModalOpen} | |
| onConfirm={handleSplitterConfirm} | |
| isLoading={isGenerating} | |
| /> | |
| {/* Welcome Guide Modal */} | |
| <WelcomeGuideModal | |
| open={isWelcomeModalOpen} | |
| onOpenChange={setIsWelcomeModalOpen} | |
| /> | |
| </> | |
| ); | |
| } | |