AgentGraph / frontend /src /components /shared /FloatingActionWidget.tsx
wu981526092's picture
add
8e42f9d
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}
/>
</>
);
}