Analytics Insights and Tools Modal

This commit is contained in:
ajaysi
2025-09-07 08:42:37 +05:30
parent 7ac72c5382
commit 5ba19c097a
9 changed files with 367 additions and 90 deletions

View File

@@ -290,21 +290,48 @@ const PillarCard: React.FC<{
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CardContent sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Shooting star border animation */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 'inherit',
overflow: 'hidden',
zIndex: 1,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '2px',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent)',
animation: 'shootingStar 8s linear infinite',
},
'@keyframes shootingStar': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
}}
/>
<CardContent sx={{ p: 1.5, height: '100%', display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 2 }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5, position: 'relative' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1, position: 'relative' }}>
<Box
sx={{
p: 0.8,
p: 0.6,
borderRadius: '50%',
backgroundColor: 'rgba(255,255,255,0.2)',
mr: 1.2,
mr: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<IconComponent sx={{ fontSize: 18, color: 'white' }} />
<IconComponent sx={{ fontSize: 16, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
{pillar.title}
@@ -378,7 +405,7 @@ const PillarCard: React.FC<{
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 0.5 }}>
{pillar.id === 'plan' ? (
<>
<motion.div

View File

@@ -23,6 +23,7 @@ import ErrorDisplay from '../shared/ErrorDisplay';
import EmptyState from '../shared/EmptyState';
import ContentLifecyclePillars from './ContentLifecyclePillars';
import AnalyticsInsights from './components/AnalyticsInsights';
import ToolsModal from './components/ToolsModal';
// Shared types and utilities
import { Tool } from '../shared/types';
@@ -99,6 +100,12 @@ const MainDashboard: React.FC = () => {
// State to track if we need to start a newly generated workflow
const [shouldStartWorkflow, setShouldStartWorkflow] = React.useState(false);
// Tools Modal state
const [toolsModalOpen, setToolsModalOpen] = React.useState(false);
const [modalCategoryName, setModalCategoryName] = React.useState<string | null>(null);
const [modalCategory, setModalCategory] = React.useState<any>(null);
const [searchResults, setSearchResults] = React.useState<Tool[]>([]);
// Handle workflow start
const handleStartWorkflow = async () => {
try {
@@ -170,6 +177,56 @@ const MainDashboard: React.FC = () => {
showSnackbar(`Launching ${tool.name}...`, 'info');
};
// Handle category click to open modal
const handleCategoryClick = (categoryName: string | null, categoryData?: any) => {
setModalCategoryName(categoryName);
setModalCategory(categoryData);
setToolsModalOpen(true);
};
// Handle search to show results in modal with debouncing
React.useEffect(() => {
if (searchQuery && searchQuery.length >= 2) { // Only search after 2+ characters
const timeoutId = setTimeout(() => {
// Get all tools from all categories that match search
const allTools: Tool[] = [];
Object.values(toolCategories).forEach(category => {
if (category) {
const tools = getToolsForCategory(category, null);
allTools.push(...tools);
}
});
const filtered = allTools.filter(tool =>
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.features.some(feature => feature.toLowerCase().includes(searchQuery.toLowerCase()))
);
setSearchResults(filtered);
setModalCategoryName(null);
setModalCategory(null);
setToolsModalOpen(true);
}, 500); // 500ms delay
return () => clearTimeout(timeoutId);
} else if (searchQuery && searchQuery.length < 2) {
// Close modal if search query is too short
setToolsModalOpen(false);
}
}, [searchQuery, toolCategories]);
// Close modal and clear search
const handleCloseModal = () => {
setToolsModalOpen(false);
setModalCategoryName(null);
setModalCategory(null);
setSearchResults([]);
if (searchQuery) {
setSearchQuery('');
}
};
const filteredCategories = getFilteredCategories(
toolCategories,
selectedCategory,
@@ -245,9 +302,6 @@ const MainDashboard: React.FC = () => {
{/* Content Lifecycle Pillars - First Panel */}
<ContentLifecyclePillars />
{/* Analytics Insights - Good/Bad/Ugly */}
<AnalyticsInsights />
{/* Search and Filter */}
<SearchFilter
searchQuery={searchQuery}
@@ -259,60 +313,24 @@ const MainDashboard: React.FC = () => {
onSubCategoryChange={setSelectedSubCategory}
toolCategories={toolCategories}
theme={theme}
onCategoryClick={handleCategoryClick}
/>
{/* Enhanced Tools Grid */}
<Box sx={{ mb: 4 }}>
{Object.entries(filteredCategories).map(([categoryName, category], categoryIndex) => (
<motion.div
key={categoryName}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
>
<Box sx={{ mb: 5 }}>
{/* Show Category Header when no specific category is selected OR when searching across all categories */}
{(selectedCategory === null || searchQuery) && (
<CategoryHeader
categoryName={categoryName}
category={category}
theme={theme}
/>
)}
{/* Analytics Insights - Good/Bad/Ugly */}
<AnalyticsInsights />
<Grid container spacing={3}>
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={tool.name}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: (categoryIndex * 0.1) + (toolIndex * 0.05) }}
>
<ToolCard
tool={tool}
onToolClick={handleToolClick}
isFavorite={favorites.includes(tool.name)}
onToggleFavorite={toggleFavorite}
/>
</motion.div>
</Grid>
))}
</Grid>
</Box>
</motion.div>
))}
</Box>
{/* Empty State */}
{Object.keys(filteredCategories).length === 0 && (
<EmptyState
icon={<span>🔍</span>}
title="No tools found matching your criteria"
message="Try adjusting your search or category filter"
onClearFilters={clearFilters}
clearButtonText="Clear Filters"
/>
)}
{/* Tools Modal */}
<ToolsModal
open={toolsModalOpen}
onClose={handleCloseModal}
categoryName={modalCategoryName || undefined}
category={modalCategory}
searchQuery={searchQuery}
searchResults={searchResults}
onToolClick={handleToolClick}
favorites={favorites}
onToggleFavorite={toggleFavorite}
/>
</motion.div>
</AnimatePresence>

View File

@@ -92,11 +92,6 @@ const Badge = styled('span')(({ theme }) => ({
fontSize: '0.65rem'
}));
// Subtle shimmer animation for the title text
const shimmerText = keyframes`
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
`;
const mockData: AnalyticsData = {
theGood: [
@@ -268,23 +263,20 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
};
return (
<Box sx={{ mt: 2, mb: 2.5 }}>
<Typography
variant="h6"
sx={{
fontWeight: 800,
mb: 1.5,
fontSize: '1.1rem',
background: 'linear-gradient(90deg, rgba(255,255,255,0.35), rgba(255,255,255,0.9) 50%, rgba(255,255,255,0.35))',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
color: 'transparent',
backgroundSize: '200% 100%',
animation: `${shimmerText} 3.2s linear infinite`,
}}
>
Analytics Insights
</Typography>
<Box sx={{ mt: 2, mb: 2.9 }}>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<Typography
variant="h6"
sx={{
fontWeight: 800,
mb: 1.5,
fontSize: '1.1rem',
color: 'rgba(255,255,255,0.95)',
}}
>
Today's Analytics Insights
</Typography>
</Box>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
{columns.map((col) => {
const isHovered = hovered === col.key;

View File

@@ -128,19 +128,27 @@ const EnhancedTodayModal: React.FC<EnhancedTodayModalProps> = ({
};
const handleWorkflowComplete = async () => {
console.log('Workflow Complete clicked for pillar:', pillarId);
console.log('Current pillar tasks:', pillarTasks);
// Mark all remaining tasks in this pillar as completed
const incompleteTasks = pillarTasks.filter(task =>
task.status !== 'completed' && task.status !== 'skipped'
);
console.log('Incomplete tasks to complete:', incompleteTasks);
for (const task of incompleteTasks) {
try {
console.log('Completing task:', task.id);
await completeTask(task.id);
console.log('Task completed successfully:', task.id);
} catch (error) {
console.error(`Failed to complete task ${task.id}:`, error);
}
}
console.log('All tasks completed, closing modal');
// Close the modal
onClose();
};

View File

@@ -0,0 +1,221 @@
import React from 'react';
import {
Box,
Modal,
Typography,
IconButton,
Grid,
Stack,
Chip,
Divider
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
Close as CloseIcon,
Search as SearchIcon
} from '@mui/icons-material';
import ToolCard from '../../shared/ToolCard';
import { Tool } from '../../shared/types';
import { getToolsForCategory } from '../../shared/utils';
interface ToolsModalProps {
open: boolean;
onClose: () => void;
categoryName?: string;
category?: any;
searchQuery?: string;
searchResults?: Tool[];
onToolClick: (tool: Tool) => void;
favorites: string[];
onToggleFavorite: (toolName: string) => void;
}
const ToolsModal: React.FC<ToolsModalProps> = ({
open,
onClose,
categoryName,
category,
searchQuery,
searchResults,
onToolClick,
favorites,
onToggleFavorite
}) => {
const isSearchMode = !!searchQuery;
// Handle different modes: search, all tools, or specific category
let tools: Tool[] = [];
if (isSearchMode) {
tools = searchResults || [];
} else if (categoryName === null) {
// All Tools mode - get tools from all categories
const allTools: Tool[] = [];
if (category && typeof category === 'object') {
// category is the entire toolCategories object
Object.values(category).forEach((cat: any) => {
if (cat && typeof cat === 'object') {
// Check if this is a valid category with tools or subCategories
if ('tools' in cat || 'subCategories' in cat) {
const categoryTools = getToolsForCategory(cat, null);
if (categoryTools && Array.isArray(categoryTools)) {
allTools.push(...categoryTools);
}
}
}
});
}
tools = allTools;
} else {
// Specific category mode
const categoryTools = getToolsForCategory(category || null, null);
tools = categoryTools && Array.isArray(categoryTools) ? categoryTools : [];
}
// Ensure tools is always an array
if (!Array.isArray(tools)) {
tools = [];
}
const title = isSearchMode ? `Search Results for "${searchQuery}"` : categoryName || 'All Tools';
const subtitle = isSearchMode ? `${tools.length} tools found` : `${tools.length} tools available`;
return (
<Modal
open={open}
onClose={onClose}
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
p: 1,
mt: 2
}}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.3 }}
style={{ width: '100%', height: '100%' }}
>
<Box
sx={{
width: '98%',
maxWidth: 'none',
height: '95vh',
overflow: 'hidden',
background: 'linear-gradient(180deg, rgba(16,24,39,0.98) 0%, rgba(26,33,56,0.98) 100%)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: 2,
boxShadow: '0 32px 100px rgba(0,0,0,0.6)',
display: 'flex',
flexDirection: 'column',
backdropFilter: 'blur(20px)'
}}
>
{/* Header */}
<Box
sx={{
p: 3,
borderBottom: '1px solid rgba(255,255,255,0.15)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{isSearchMode ? (
<SearchIcon sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }} />
) : categoryName === null ? (
<SearchIcon sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }} />
) : (
category?.icon && (
<Box sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }}>
{category.icon}
</Box>
)
)}
<Box>
<Typography variant="h5" sx={{ color: 'rgba(255,255,255,0.95)', fontWeight: 800 }}>
{title}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{subtitle}
</Typography>
</Box>
</Box>
<IconButton
onClick={onClose}
sx={{
color: 'rgba(255,255,255,0.8)',
backgroundColor: 'rgba(255,255,255,0.1)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.2)'
}
}}
>
<CloseIcon />
</IconButton>
</Box>
{/* Content */}
<Box
sx={{
flex: 1,
overflow: 'auto',
p: 3
}}
>
{tools.length === 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
py: 8,
textAlign: 'center'
}}
>
<Typography variant="h6" sx={{ color: 'rgba(255,255,255,0.7)', mb: 2 }}>
{isSearchMode ? 'No tools found' : 'No tools available'}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.5)' }}>
{isSearchMode
? 'Try adjusting your search terms or browse categories'
: 'This category is currently empty'
}
</Typography>
</Box>
) : (
<Grid container spacing={2.5}>
<AnimatePresence>
{tools.map((tool: Tool, index: number) => (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2.4} key={tool.name}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, delay: index * 0.03 }}
style={{ height: '100%' }}
>
<ToolCard
tool={tool}
onToolClick={onToolClick}
isFavorite={favorites.includes(tool.name)}
onToggleFavorite={onToggleFavorite}
/>
</motion.div>
</Grid>
))}
</AnimatePresence>
</Grid>
)}
</Box>
</Box>
</motion.div>
</Modal>
);
};
export default ToolsModal;

View File

@@ -171,7 +171,9 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
animation: 'shimmer 2.5s infinite',
animation: workflowControls.completedTasks === workflowControls.totalTasks
? 'none'
: 'shimmer 2.5s infinite',
zIndex: 1,
},
'&::after': {

View File

@@ -24,7 +24,8 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
selectedSubCategory,
onSubCategoryChange,
toolCategories,
theme
theme,
onCategoryClick
}) => {
// Helper function to get tool count from a category
const getToolCount = (category: any): number => {
@@ -103,7 +104,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
<CategoryChip
label="All Tools"
onClick={() => onCategoryChange(null)}
onClick={() => onCategoryClick ? onCategoryClick(null, toolCategories) : onCategoryChange(null)}
active={selectedCategory === null}
theme={theme}
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
@@ -122,7 +123,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
>
<CategoryChip
label={category}
onClick={() => onCategoryChange(category)}
onClick={() => onCategoryClick ? onCategoryClick(category, cat) : onCategoryChange(category)}
active={selectedCategory === category}
theme={theme}
toolCount={getToolCount(cat)}

View File

@@ -73,6 +73,7 @@ export interface SearchFilterProps {
onSubCategoryChange: (subCategory: string | null) => void;
toolCategories: ToolCategories;
theme: any;
onCategoryClick?: (category: string | null, categoryData?: any) => void;
}
export interface DashboardHeaderProps {

View File

@@ -1,19 +1,26 @@
import { Category, Tool, ToolCategories } from './types';
// Utility functions for dashboard components
export const getToolsForCategory = (category: Category, selectedSubCategory: string | null): Tool[] => {
export const getToolsForCategory = (category: Category | null, selectedSubCategory: string | null): Tool[] => {
if (!category) {
return [];
}
if ('subCategories' in category) {
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
return category.subCategories[selectedSubCategory].tools;
const subCategory = category.subCategories[selectedSubCategory];
return subCategory && subCategory.tools ? subCategory.tools : [];
}
// When no subcategory is selected, return all tools from all subcategories
const allTools: Tool[] = [];
Object.values(category.subCategories).forEach(subCategory => {
allTools.push(...subCategory.tools);
if (subCategory && subCategory.tools && Array.isArray(subCategory.tools)) {
allTools.push(...subCategory.tools);
}
});
return allTools;
}
return category.tools;
return category.tools && Array.isArray(category.tools) ? category.tools : [];
};
export const getFilteredCategories = (