Alwrity today's tasks workflow implementation plan.
This commit is contained in:
273
docs/TODAYS_TASKS_WORKFLOW_IMPLEMENTATION_PLAN.md
Normal file
273
docs/TODAYS_TASKS_WORKFLOW_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Today's Tasks Workflow System - Implementation Plan
|
||||
|
||||
## 📋 **Overview**
|
||||
|
||||
The Today's Tasks Workflow System is designed to transform ALwrity's complex digital marketing platform into a guided, user-friendly daily workflow. This system addresses the challenge of navigating multiple social media platforms, website management, and analytics by providing a single glass pane view with actionable daily tasks.
|
||||
|
||||
## 🎯 **Core Vision**
|
||||
|
||||
### **Problem Statement**
|
||||
- Digital marketing is complex and daunting for non-technical users
|
||||
- Multiple platforms and tools create navigation confusion
|
||||
- Users need guidance on what actions to take daily
|
||||
- Lack of structured workflow leads to incomplete marketing activities
|
||||
|
||||
### **Solution Approach**
|
||||
- Present users with a curated set of daily actions via "Today's Tasks" in each pillar
|
||||
- Guide users through a structured workflow using the "ALwrity it" button
|
||||
- Automatically navigate users between tasks and platforms
|
||||
- Provide completion tracking and progress indicators
|
||||
- Hand-hold users through the entire marketing workflow
|
||||
|
||||
## 🏗️ **System Architecture**
|
||||
|
||||
### **Core Components**
|
||||
|
||||
#### **1. Task Management System**
|
||||
- Centralized task repository with status tracking
|
||||
- Task dependency management
|
||||
- Priority and time estimation system
|
||||
- Completion verification mechanisms
|
||||
|
||||
#### **2. Workflow Orchestrator**
|
||||
- Daily workflow generation and management
|
||||
- Task sequencing and dependency resolution
|
||||
- Progress tracking and state management
|
||||
- Auto-navigation between tasks
|
||||
|
||||
#### **3. User Interface Components**
|
||||
- Enhanced Today's Task modals with workflow features
|
||||
- Progress indicators and completion tracking
|
||||
- Seamless navigation between tasks
|
||||
- Task status visualization
|
||||
|
||||
#### **4. Intelligence Layer**
|
||||
- AI-powered task generation based on user behavior
|
||||
- Personalized task recommendations
|
||||
- Completion verification and validation
|
||||
- Analytics and insights generation
|
||||
|
||||
## 🔄 **Workflow Design**
|
||||
|
||||
### **Task Flow Sequence**
|
||||
1. **Plan Pillar**: Content strategy and calendar review
|
||||
2. **Generate Pillar**: Content creation tasks
|
||||
3. **Publish Pillar**: Social media and website publishing
|
||||
4. **Analyze Pillar**: Performance review and insights
|
||||
5. **Engage Pillar**: Community interaction and responses
|
||||
6. **Remarket Pillar**: Retargeting and follow-up campaigns
|
||||
|
||||
### **User Journey**
|
||||
1. User logs into ALwrity dashboard
|
||||
2. System presents Today's Tasks for each pillar
|
||||
3. User clicks "Start Today's Workflow" or individual task
|
||||
4. System guides user through task completion
|
||||
5. Auto-navigation to next task in sequence
|
||||
6. Progress tracking and completion celebration
|
||||
7. Daily workflow completion summary
|
||||
|
||||
## 📊 **Data Models**
|
||||
|
||||
### **Task Structure**
|
||||
- Unique task identifier
|
||||
- Pillar association and priority level
|
||||
- Task title, description, and estimated time
|
||||
- Status tracking (pending, in-progress, completed, skipped)
|
||||
- Dependencies and prerequisites
|
||||
- Action type and navigation details
|
||||
- Completion metadata and timestamps
|
||||
|
||||
### **Workflow State**
|
||||
- Daily workflow instance
|
||||
- Current task index and progress
|
||||
- Completed tasks count and percentage
|
||||
- Workflow status and user session data
|
||||
- Task completion history and analytics
|
||||
|
||||
## 🎨 **User Experience Design**
|
||||
|
||||
### **Visual Enhancements**
|
||||
- Workflow progress bar on main dashboard
|
||||
- Enhanced Today's Task modals with status indicators
|
||||
- Task completion animations and celebrations
|
||||
- Real-time progress updates across components
|
||||
- Mobile-responsive workflow interface
|
||||
|
||||
### **Interaction Patterns**
|
||||
- One-click task initiation
|
||||
- Guided navigation between platforms
|
||||
- Contextual help and tooltips
|
||||
- Task completion confirmation
|
||||
- Next task auto-suggestion
|
||||
|
||||
## 🚀 **Implementation Phases**
|
||||
|
||||
### **Phase 1: Foundation (Weeks 1-2)**
|
||||
**Objective**: Establish core workflow infrastructure
|
||||
|
||||
**Deliverables**:
|
||||
- TaskWorkflowOrchestrator service implementation
|
||||
- Basic task data structure and persistence
|
||||
- Enhanced Today's Task modal with workflow features
|
||||
- Workflow progress indicators on dashboard
|
||||
- Task status tracking system
|
||||
|
||||
**Key Features**:
|
||||
- Manual task creation and management
|
||||
- Basic progress tracking
|
||||
- Simple navigation between tasks
|
||||
- Task completion marking
|
||||
|
||||
### **Phase 2: Smart Navigation (Weeks 3-4)**
|
||||
**Objective**: Implement intelligent task flow and navigation
|
||||
|
||||
**Deliverables**:
|
||||
- Auto-navigation system between tasks
|
||||
- Task dependency management
|
||||
- Completion verification mechanisms
|
||||
- Task sequencing logic
|
||||
- Cross-platform navigation handling
|
||||
|
||||
**Key Features**:
|
||||
- Seamless transitions between ALwrity tools
|
||||
- Task prerequisite checking
|
||||
- Progress persistence across sessions
|
||||
- Error handling and fallback mechanisms
|
||||
|
||||
### **Phase 3: Intelligence Layer (Weeks 5-6)**
|
||||
**Objective**: Add AI-powered task generation and personalization
|
||||
|
||||
**Deliverables**:
|
||||
- AI-powered daily task generation
|
||||
- User behavior analysis and learning
|
||||
- Personalized task recommendations
|
||||
- Completion verification using platform APIs
|
||||
- Smart task prioritization
|
||||
|
||||
**Key Features**:
|
||||
- Dynamic task generation based on user activity
|
||||
- Learning from user completion patterns
|
||||
- Integration with existing ALwrity features
|
||||
- Intelligent task ordering and timing
|
||||
|
||||
### **Phase 4: Advanced Features (Weeks 7-8)**
|
||||
**Objective**: Enhance user experience and add advanced capabilities
|
||||
|
||||
**Deliverables**:
|
||||
- Gamification elements (points, streaks, achievements)
|
||||
- Team collaboration features
|
||||
- Advanced analytics and insights
|
||||
- Mobile optimization
|
||||
- A/B testing framework
|
||||
|
||||
**Key Features**:
|
||||
- User engagement and motivation systems
|
||||
- Multi-user workflow coordination
|
||||
- Performance analytics and reporting
|
||||
- Mobile-responsive design
|
||||
- Continuous improvement mechanisms
|
||||
|
||||
## 🎯 **Success Metrics**
|
||||
|
||||
### **User Engagement**
|
||||
- Daily workflow completion rate
|
||||
- Task completion time reduction
|
||||
- User retention and return visits
|
||||
- Feature adoption rates
|
||||
|
||||
### **Business Impact**
|
||||
- Marketing activity completion increase
|
||||
- Content publishing frequency improvement
|
||||
- Social media engagement growth
|
||||
- Overall platform usage enhancement
|
||||
|
||||
### **Technical Performance**
|
||||
- Task generation accuracy
|
||||
- Navigation success rate
|
||||
- System response times
|
||||
- Error rates and recovery
|
||||
|
||||
## 🔧 **Technical Considerations**
|
||||
|
||||
### **Integration Points**
|
||||
- Existing ALwrity platform components
|
||||
- Social media platform APIs
|
||||
- Analytics and tracking systems
|
||||
- User authentication and profiles
|
||||
- Content management systems
|
||||
|
||||
### **Scalability Requirements**
|
||||
- Support for multiple user workflows
|
||||
- Real-time progress synchronization
|
||||
- Offline task completion support
|
||||
- Performance optimization for large task sets
|
||||
|
||||
### **Security and Privacy**
|
||||
- User data protection and encryption
|
||||
- Secure API integrations
|
||||
- Privacy-compliant analytics
|
||||
- Access control and permissions
|
||||
|
||||
## 📈 **Future Enhancements**
|
||||
|
||||
### **Advanced AI Features**
|
||||
- Predictive task generation
|
||||
- Automated content suggestions
|
||||
- Performance optimization recommendations
|
||||
- Intelligent scheduling and timing
|
||||
|
||||
### **Collaboration Features**
|
||||
- Team workflow coordination
|
||||
- Task assignment and delegation
|
||||
- Progress sharing and reporting
|
||||
- Multi-user dashboard views
|
||||
|
||||
### **Integration Expansions**
|
||||
- Third-party tool integrations
|
||||
- Advanced analytics platforms
|
||||
- CRM and marketing automation
|
||||
- E-commerce platform connections
|
||||
|
||||
## 🎉 **Expected Outcomes**
|
||||
|
||||
### **User Benefits**
|
||||
- Simplified daily marketing workflow
|
||||
- Reduced cognitive load and decision fatigue
|
||||
- Increased marketing activity completion
|
||||
- Improved platform adoption and retention
|
||||
|
||||
### **Business Benefits**
|
||||
- Higher user engagement and satisfaction
|
||||
- Increased platform stickiness
|
||||
- Better marketing results for users
|
||||
- Competitive differentiation in the market
|
||||
|
||||
### **Technical Benefits**
|
||||
- Modular and extensible architecture
|
||||
- Reusable workflow components
|
||||
- Scalable task management system
|
||||
- Foundation for future AI features
|
||||
|
||||
## 📝 **Next Steps**
|
||||
|
||||
1. **Immediate Actions**:
|
||||
- Review and approve implementation plan
|
||||
- Set up development environment and tools
|
||||
- Create detailed technical specifications
|
||||
- Begin Phase 1 development
|
||||
|
||||
2. **Stakeholder Alignment**:
|
||||
- Present plan to development team
|
||||
- Gather feedback from product team
|
||||
- Validate approach with user research
|
||||
- Secure necessary resources and timeline
|
||||
|
||||
3. **Development Preparation**:
|
||||
- Create detailed user stories and acceptance criteria
|
||||
- Set up project tracking and milestone management
|
||||
- Establish testing and quality assurance processes
|
||||
- Plan for user feedback and iteration cycles
|
||||
|
||||
---
|
||||
|
||||
*This document serves as the foundation for implementing the Today's Tasks Workflow System. It should be reviewed and updated regularly as the project progresses and new insights are gained.*
|
||||
@@ -0,0 +1,517 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Paper,
|
||||
Modal,
|
||||
Button,
|
||||
IconButton,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
Avatar,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
RadioButtonUnchecked as UncheckedIcon,
|
||||
TrendingUp as TrendingUpIcon
|
||||
} from '@mui/icons-material';
|
||||
import GeneratePillarChips from './components/GeneratePillarChips';
|
||||
import PublishPillarChips from './components/PublishPillarChips';
|
||||
import AnalyzePillarChips from './components/AnalyzePillarChips';
|
||||
import EngagePillarChips from './components/EngagePillarChips';
|
||||
import EnhancedTodayChip from './components/EnhancedTodayChip';
|
||||
import OnboardingModal from './components/OnboardingModal';
|
||||
import { pillarData } from './components/PillarData';
|
||||
import { useWorkflowStore } from '../../stores/workflowStore';
|
||||
|
||||
|
||||
// Enhanced Glassomorphic Chip Component with Popping Effects
|
||||
const ChipWithTooltip: React.FC<{
|
||||
chip: any;
|
||||
delay?: number;
|
||||
onOnboardingClick?: () => void;
|
||||
}> = ({ chip, delay = 0, onOnboardingClick }) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % chip.bubbles.length);
|
||||
}, 2000 + delay * 300);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [chip.bubbles.length, delay]);
|
||||
|
||||
const IconComponent = chip.icon;
|
||||
|
||||
const handleClick = () => {
|
||||
if (chip.label === 'On-Boarding' && onOnboardingClick) {
|
||||
onOnboardingClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
{chip.label}
|
||||
</Typography>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: 'white' }}>
|
||||
{chip.bubbles[currentIndex]}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px) scale(1.05)',
|
||||
'& .chip-glow': {
|
||||
opacity: 1,
|
||||
transform: 'scale(1.2)'
|
||||
},
|
||||
'& .chip-shadow': {
|
||||
opacity: 0.6,
|
||||
transform: 'translateY(8px) scale(1.1)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Glow Effect */}
|
||||
<Box
|
||||
className="chip-glow"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
left: -8,
|
||||
right: -8,
|
||||
bottom: -8,
|
||||
background: chip.gradient || chip.color,
|
||||
borderRadius: '20px',
|
||||
opacity: 0,
|
||||
transition: 'all 0.4s ease',
|
||||
filter: 'blur(12px)',
|
||||
zIndex: -2
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Shadow Effect */}
|
||||
<Box
|
||||
className="chip-shadow"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
left: 2,
|
||||
right: -2,
|
||||
bottom: -4,
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: '16px',
|
||||
opacity: 0.3,
|
||||
transition: 'all 0.4s ease',
|
||||
filter: 'blur(8px)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main Chip */}
|
||||
<Chip
|
||||
icon={<IconComponent sx={{ fontSize: 14 }} />}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, fontSize: '0.7rem' }}>
|
||||
{chip.label}
|
||||
</Typography>
|
||||
{chip.value && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
color: chip.color,
|
||||
borderRadius: '50%',
|
||||
width: 16,
|
||||
height: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.6rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
>
|
||||
{chip.value}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(255,255,255,0.25) 0%,
|
||||
rgba(255,255,255,0.1) 50%,
|
||||
rgba(255,255,255,0.05) 100%)`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.3)',
|
||||
color: 'white',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)',
|
||||
transition: 'left 0.6s ease',
|
||||
zIndex: 1
|
||||
},
|
||||
'&:hover::before': {
|
||||
left: '100%'
|
||||
},
|
||||
'& .MuiChip-label': {
|
||||
px: 1,
|
||||
zIndex: 2,
|
||||
position: 'relative'
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
zIndex: 2,
|
||||
position: 'relative'
|
||||
},
|
||||
'&:hover': {
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(255,255,255,0.35) 0%,
|
||||
rgba(255,255,255,0.2) 50%,
|
||||
rgba(255,255,255,0.1) 100%)`,
|
||||
border: '1px solid rgba(255,255,255,0.5)',
|
||||
boxShadow: `0 8px 32px ${chip.color}40,
|
||||
0 4px 16px rgba(0,0,0,0.1),
|
||||
inset 0 1px 0 rgba(255,255,255,0.3)`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
// Enhanced Pillar Component with Progressive Disclosure
|
||||
const PillarCard: React.FC<{
|
||||
pillar: typeof pillarData[0];
|
||||
index: number;
|
||||
onOnboardingClick?: () => void;
|
||||
}> = ({ pillar, index, onOnboardingClick }) => {
|
||||
const IconComponent = pillar.icon;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { currentWorkflow } = useWorkflowStore();
|
||||
|
||||
// Use live workflow tasks if available
|
||||
const liveTasksForPillar = (currentWorkflow?.tasks && currentWorkflow.tasks.length > 0
|
||||
? currentWorkflow.tasks
|
||||
: pillar.todayTasks || []).filter((t: any) => t.pillarId === pillar.id);
|
||||
const totalForPillar = liveTasksForPillar.length;
|
||||
const doneForPillar = liveTasksForPillar.filter((t: any) => t.status === 'completed' || t.status === 'skipped').length;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5, scale: 1.02 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{
|
||||
height: isHovered ? 280 : 120, // Dynamic height based on hover state
|
||||
background: pillar.gradient,
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
// Large tick when pillar tasks complete (uses live store counts)
|
||||
'&::after': {
|
||||
content: doneForPillar > 0 && doneForPillar === totalForPillar ? '"✓"' : '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: '64px',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
textShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10, // Ensure tick is above all content
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, rgba(255,255,255,0.1) 0%, transparent 50%)',
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease'
|
||||
},
|
||||
'&:hover': {
|
||||
boxShadow: `0 12px 24px ${pillar.color}40`
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CardContent sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5, position: 'relative' }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
mr: 1.2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<IconComponent sx={{ fontSize: 18, color: 'white' }} />
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
|
||||
{pillar.title}
|
||||
</Typography>
|
||||
{/* Pillar task count badge */}
|
||||
<Box sx={{ ml: 1, position: 'relative' }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
color: pillar.color,
|
||||
borderRadius: '12px',
|
||||
px: 0.75,
|
||||
py: 0.1,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 800,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
>
|
||||
{totalForPillar}
|
||||
</Box>
|
||||
</Box>
|
||||
{/* More Options Indicator */}
|
||||
{!isHovered && (
|
||||
<motion.div
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.6rem', opacity: 0.7 }}>
|
||||
⋯
|
||||
</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Chips Layout with Progressive Disclosure */}
|
||||
{pillar.id === 'generate' ? (
|
||||
<GeneratePillarChips index={index} isHovered={isHovered} />
|
||||
) : pillar.id === 'publish' ? (
|
||||
<PublishPillarChips isHovered={isHovered} pillarColor={pillar.color} />
|
||||
) : pillar.id === 'analyze' ? (
|
||||
<AnalyzePillarChips isHovered={isHovered} pillarColor={pillar.color} />
|
||||
) : pillar.id === 'engage' ? (
|
||||
<EngagePillarChips isHovered={isHovered} pillarColor={pillar.color} />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
flexGrow: 1,
|
||||
justifyContent: isHovered ? 'center' : 'flex-start'
|
||||
}}
|
||||
>
|
||||
{/* Today Chip - Always Visible */}
|
||||
<EnhancedTodayChip
|
||||
pillarId={pillar.id}
|
||||
pillarTitle={pillar.title}
|
||||
pillarColor={pillar.color}
|
||||
tasks={pillar.todayTasks}
|
||||
delay={index * 5}
|
||||
/>
|
||||
|
||||
{/* Additional Chips - Progressive Disclosure */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
{pillar.id === 'plan' ? (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.onboarding} delay={index * 5 + 1} onOnboardingClick={onOnboardingClick} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.strategy} delay={index * 5 + 2} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.calendar} delay={index * 5 + 3} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.review} delay={index * 5 + 4} />
|
||||
</motion.div>
|
||||
</>
|
||||
) : pillar.id === 'remarket' ? (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.good} delay={index * 5 + 1} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.bad} delay={index * 5 + 2} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.ugly} delay={index * 5 + 3} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<ChipWithTooltip chip={pillar.chips.review} delay={index * 5 + 4} />
|
||||
</motion.div>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Content Lifecycle Pillars Component
|
||||
const ContentLifecyclePillars: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [onboardingModalOpen, setOnboardingModalOpen] = useState(false);
|
||||
|
||||
const handleOnboardingClick = () => {
|
||||
setOnboardingModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOnboardingModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
py: 3,
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderRadius: 2,
|
||||
mb: 4
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xl">
|
||||
{/* Pillars Grid */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: 'repeat(2, 1fr)',
|
||||
sm: 'repeat(3, 1fr)',
|
||||
md: 'repeat(6, 1fr)'
|
||||
},
|
||||
gap: 2,
|
||||
overflow: 'visible'
|
||||
}}
|
||||
>
|
||||
{pillarData.map((pillar, index) => (
|
||||
<PillarCard
|
||||
key={pillar.id}
|
||||
pillar={pillar}
|
||||
index={index}
|
||||
onOnboardingClick={handleOnboardingClick}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* Onboarding Modal */}
|
||||
<OnboardingModal
|
||||
open={onboardingModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentLifecyclePillars;
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import AskAlwrityIcon from '../../assets/images/AskAlwrity-min.ico';
|
||||
|
||||
// Shared components
|
||||
import DashboardHeader from '../shared/DashboardHeader';
|
||||
@@ -20,13 +21,15 @@ import CategoryHeader from '../shared/CategoryHeader';
|
||||
import LoadingSkeleton from '../shared/LoadingSkeleton';
|
||||
import ErrorDisplay from '../shared/ErrorDisplay';
|
||||
import EmptyState from '../shared/EmptyState';
|
||||
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
||||
|
||||
// Shared types and utilities
|
||||
import { Tool, Category } from '../shared/types';
|
||||
import { Tool } from '../shared/types';
|
||||
import { getFilteredCategories, getToolsForCategory } from '../shared/utils';
|
||||
|
||||
// Zustand store
|
||||
// Zustand stores
|
||||
import { useDashboardStore } from '../../stores/dashboardStore';
|
||||
import { useWorkflowStore } from '../../stores/workflowStore';
|
||||
|
||||
// Data
|
||||
import { toolCategories } from '../../data/toolCategories';
|
||||
@@ -34,7 +37,6 @@ import { toolCategories } from '../../data/toolCategories';
|
||||
// Main dashboard component
|
||||
const MainDashboard: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Zustand store hooks
|
||||
@@ -50,13 +52,114 @@ const MainDashboard: React.FC = () => {
|
||||
setSearchQuery,
|
||||
setSelectedCategory,
|
||||
setSelectedSubCategory,
|
||||
setError,
|
||||
setLoading,
|
||||
showSnackbar,
|
||||
hideSnackbar,
|
||||
clearFilters,
|
||||
} = useDashboardStore();
|
||||
|
||||
// Workflow store hooks
|
||||
const {
|
||||
currentWorkflow,
|
||||
workflowProgress,
|
||||
isLoading: workflowLoading,
|
||||
generateDailyWorkflow,
|
||||
startWorkflow,
|
||||
pauseWorkflow,
|
||||
stopWorkflow
|
||||
} = useWorkflowStore();
|
||||
|
||||
// Initialize workflow on component mount
|
||||
React.useEffect(() => {
|
||||
const initializeWorkflow = async () => {
|
||||
try {
|
||||
// Generate daily workflow for current user
|
||||
// In a real app, you'd get the actual user ID from auth context
|
||||
const userId = 'demo-user'; // Replace with actual user ID
|
||||
await generateDailyWorkflow(userId);
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeWorkflow();
|
||||
}, [generateDailyWorkflow]);
|
||||
|
||||
// Debug logging for workflow state
|
||||
React.useEffect(() => {
|
||||
console.log('Workflow Debug:', {
|
||||
currentWorkflow,
|
||||
workflowProgress,
|
||||
isWorkflowActive: currentWorkflow?.workflowStatus === 'in_progress',
|
||||
workflowStatus: currentWorkflow?.workflowStatus,
|
||||
hasWorkflow: !!currentWorkflow
|
||||
});
|
||||
}, [currentWorkflow, workflowProgress]);
|
||||
|
||||
// State to track if we need to start a newly generated workflow
|
||||
const [shouldStartWorkflow, setShouldStartWorkflow] = React.useState(false);
|
||||
|
||||
// Handle workflow start
|
||||
const handleStartWorkflow = async () => {
|
||||
try {
|
||||
if (currentWorkflow) {
|
||||
await startWorkflow(currentWorkflow.id);
|
||||
} else {
|
||||
// Generate workflow first, then mark that we should start it
|
||||
await generateDailyWorkflow('demo-user');
|
||||
setShouldStartWorkflow(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-start workflow after generation
|
||||
React.useEffect(() => {
|
||||
if (shouldStartWorkflow && currentWorkflow && currentWorkflow.workflowStatus === 'not_started') {
|
||||
const startGeneratedWorkflow = async () => {
|
||||
try {
|
||||
await startWorkflow(currentWorkflow.id);
|
||||
setShouldStartWorkflow(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to start generated workflow:', error);
|
||||
setShouldStartWorkflow(false);
|
||||
}
|
||||
};
|
||||
startGeneratedWorkflow();
|
||||
}
|
||||
}, [shouldStartWorkflow, currentWorkflow, startWorkflow]);
|
||||
|
||||
// Handle workflow pause
|
||||
const handlePauseWorkflow = async () => {
|
||||
if (currentWorkflow) {
|
||||
try {
|
||||
await pauseWorkflow(currentWorkflow.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to pause workflow:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle workflow stop
|
||||
const handleStopWorkflow = async () => {
|
||||
if (currentWorkflow) {
|
||||
try {
|
||||
await stopWorkflow(currentWorkflow.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to stop workflow:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Resume Plan modal from header In-Progress button
|
||||
const handleResumePlanModal = () => {
|
||||
// Programmatically click the Plan pillar Today chip
|
||||
const planChip = document.querySelector('[data-pillar-id="plan"]');
|
||||
if (planChip) {
|
||||
(planChip as HTMLElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToolClick = (tool: Tool) => {
|
||||
console.log('Navigating to tool:', tool.path);
|
||||
if (tool.path) {
|
||||
@@ -120,12 +223,27 @@ const MainDashboard: React.FC = () => {
|
||||
>
|
||||
{/* Dashboard Header */}
|
||||
<DashboardHeader
|
||||
title="🚀 Alwrity Content Hub"
|
||||
subtitle="Your AI-powered content creation suite"
|
||||
title="Alwrity Content Hub"
|
||||
subtitle=""
|
||||
statusChips={[]}
|
||||
rightContent={<SystemStatusIndicator />}
|
||||
customIcon={AskAlwrityIcon}
|
||||
workflowControls={{
|
||||
onStartWorkflow: handleStartWorkflow,
|
||||
onPauseWorkflow: handlePauseWorkflow,
|
||||
onStopWorkflow: handleStopWorkflow,
|
||||
onResumePlanModal: handleResumePlanModal,
|
||||
isWorkflowActive: currentWorkflow?.workflowStatus === 'in_progress',
|
||||
completedTasks: workflowProgress?.completedTasks || 0,
|
||||
totalTasks: workflowProgress?.totalTasks || 0,
|
||||
isLoading: workflowLoading
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{/* Content Lifecycle Pillars - First Panel */}
|
||||
<ContentLifecyclePillars />
|
||||
|
||||
{/* Search and Filter */}
|
||||
<SearchFilter
|
||||
searchQuery={searchQuery}
|
||||
@@ -149,12 +267,14 @@ const MainDashboard: React.FC = () => {
|
||||
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
|
||||
>
|
||||
<Box sx={{ mb: 5 }}>
|
||||
{/* Category Header */}
|
||||
<CategoryHeader
|
||||
categoryName={categoryName}
|
||||
category={category}
|
||||
theme={theme}
|
||||
/>
|
||||
{/* Only show Category Header when no specific category is selected (showing all tools) */}
|
||||
{selectedCategory === null && (
|
||||
<CategoryHeader
|
||||
categoryName={categoryName}
|
||||
category={category}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, useTheme } from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Facebook,
|
||||
LinkedIn,
|
||||
Twitter,
|
||||
Web,
|
||||
Analytics,
|
||||
Dashboard
|
||||
} from '@mui/icons-material';
|
||||
import EnhancedTodayChip from './EnhancedTodayChip';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
interface AnalyzePillarChipsProps {
|
||||
isHovered: boolean;
|
||||
pillarColor: string;
|
||||
}
|
||||
|
||||
const AnalyzePillarChips: React.FC<AnalyzePillarChipsProps> = ({
|
||||
isHovered,
|
||||
pillarColor
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Today's tasks for Analyze pillar
|
||||
const todayTasks: TodayTask[] = [
|
||||
{
|
||||
id: "analyze-content-performance",
|
||||
pillarId: "analyze",
|
||||
title: "Review content performance",
|
||||
description: "Analyze last week's content engagement metrics",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
icon: Analytics,
|
||||
color: "#9C27B0",
|
||||
enabled: true,
|
||||
action: () => navigate('/content-planning-dashboard')
|
||||
},
|
||||
{
|
||||
id: "analyze-strategy-alignment",
|
||||
pillarId: "analyze",
|
||||
title: "Check strategy alignment",
|
||||
description: "Review content strategy against performance data",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
icon: Dashboard,
|
||||
color: "#673AB7",
|
||||
enabled: true,
|
||||
action: () => navigate('/content-planning-dashboard')
|
||||
},
|
||||
{
|
||||
id: "analyze-update-dashboard",
|
||||
pillarId: "analyze",
|
||||
title: "Update analytics dashboard",
|
||||
description: "Refresh analytics data for all platforms",
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 30,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/analytics-dashboard',
|
||||
icon: Analytics,
|
||||
color: "#3F51B5",
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
];
|
||||
|
||||
const handlePlanDashboardClick = () => {
|
||||
navigate('/content-planning-dashboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, width: '100%' }}>
|
||||
{/* Today Chip - Always visible */}
|
||||
<EnhancedTodayChip
|
||||
pillarId="analyze"
|
||||
pillarTitle="Analyze"
|
||||
pillarColor={pillarColor}
|
||||
tasks={todayTasks}
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
{/* Progressive disclosure chips */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<>
|
||||
{/* Plan Dashboard Chip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Dashboard sx={{ fontSize: 16 }} />}
|
||||
label="Plan Dashboard"
|
||||
onClick={handlePlanDashboardClick}
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '2px solid #9C27B0',
|
||||
boxShadow: '0 4px 12px rgba(156, 39, 176, 0.3), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
boxShadow: '0 6px 20px rgba(156, 39, 176, 0.4), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Disabled Analytics Chips */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<LinkedIn sx={{ fontSize: 16 }} />}
|
||||
label="LinkedIn Analytics"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(0, 119, 181, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(0, 119, 181, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Facebook sx={{ fontSize: 16 }} />}
|
||||
label="Facebook Analytics"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(24, 119, 242, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(24, 119, 242, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Twitter sx={{ fontSize: 16 }} />}
|
||||
label="Twitter Analytics"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(29, 161, 242, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(29, 161, 242, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.5 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Web sx={{ fontSize: 16 }} />}
|
||||
label="Website Analytics"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(255, 107, 53, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(255, 107, 53, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Ellipsis indicator when not hovered */}
|
||||
{!isHovered && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: '1.2rem',
|
||||
animation: 'pulse 2s infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': { opacity: 0.6 },
|
||||
'50%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
⋯
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyzePillarChips;
|
||||
@@ -0,0 +1,235 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, useTheme } from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Facebook,
|
||||
LinkedIn,
|
||||
Twitter,
|
||||
Forum,
|
||||
Comment,
|
||||
Chat,
|
||||
Groups
|
||||
} from '@mui/icons-material';
|
||||
import EnhancedTodayChip from './EnhancedTodayChip';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
interface EngagePillarChipsProps {
|
||||
isHovered: boolean;
|
||||
pillarColor: string;
|
||||
}
|
||||
|
||||
const EngagePillarChips: React.FC<EngagePillarChipsProps> = ({
|
||||
isHovered,
|
||||
pillarColor
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Today's tasks for Engage pillar
|
||||
const todayTasks: TodayTask[] = [
|
||||
{
|
||||
id: "engage-blog-comment",
|
||||
pillarId: "engage",
|
||||
title: "Reply to blog comment",
|
||||
description: "Received comment on blog 'AI Persona for Content writing'",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 10,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
icon: Comment,
|
||||
color: "#E91E63",
|
||||
enabled: true,
|
||||
action: () => navigate('/content-planning-dashboard')
|
||||
},
|
||||
{
|
||||
id: "engage-twitter-mention",
|
||||
pillarId: "engage",
|
||||
title: "Respond to Twitter mention",
|
||||
description: "Reply to Twitter comment from @username",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 5,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
icon: Twitter,
|
||||
color: "#1DA1F2",
|
||||
enabled: true,
|
||||
action: () => navigate('/content-planning-dashboard')
|
||||
},
|
||||
{
|
||||
id: "engage-linkedin-post",
|
||||
pillarId: "engage",
|
||||
title: "Engage with LinkedIn post",
|
||||
description: "Respond to comments on latest LinkedIn post",
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/linkedin-engagement',
|
||||
icon: LinkedIn,
|
||||
color: "#0077B5",
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, width: '100%' }}>
|
||||
{/* Today Chip - Always visible */}
|
||||
<EnhancedTodayChip
|
||||
pillarId="engage"
|
||||
pillarTitle="Engage"
|
||||
pillarColor={pillarColor}
|
||||
tasks={todayTasks}
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
{/* Progressive disclosure chips */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<>
|
||||
{/* Disabled Engagement Chips */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<LinkedIn sx={{ fontSize: 16 }} />}
|
||||
label="LinkedIn Comments"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(0, 119, 181, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(0, 119, 181, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Facebook sx={{ fontSize: 16 }} />}
|
||||
label="Facebook Comments"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(24, 119, 242, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(24, 119, 242, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Groups sx={{ fontSize: 16 }} />}
|
||||
label="Community Engagement"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(233, 30, 99, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(233, 30, 99, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Chat sx={{ fontSize: 16 }} />}
|
||||
label="Live Chat Support"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(76, 175, 80, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(76, 175, 80, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.5 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Forum sx={{ fontSize: 16 }} />}
|
||||
label="Forum Discussions"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 120,
|
||||
background: 'rgba(255, 152, 0, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(255, 152, 0, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Ellipsis indicator when not hovered */}
|
||||
{!isHovered && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: '1.2rem',
|
||||
animation: 'pulse 2s infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': { opacity: 0.6 },
|
||||
'50%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
⋯
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EngagePillarChips;
|
||||
@@ -0,0 +1,233 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Today as TodayIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useWorkflowStore } from '../../../stores/workflowStore';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
import EnhancedTodayModal from './EnhancedTodayModal';
|
||||
|
||||
interface EnhancedTodayChipProps {
|
||||
pillarId: string;
|
||||
pillarTitle: string;
|
||||
pillarColor: string;
|
||||
tasks: TodayTask[];
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
|
||||
// Enhanced Today Chip Component
|
||||
const EnhancedTodayChip: React.FC<EnhancedTodayChipProps> = ({
|
||||
pillarId,
|
||||
pillarTitle,
|
||||
pillarColor,
|
||||
tasks,
|
||||
delay = 0
|
||||
}) => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [shouldShake, setShouldShake] = useState(false);
|
||||
const [userManuallyClosed, setUserManuallyClosed] = useState(false);
|
||||
const { workflowProgress, navigationState, currentWorkflow } = useWorkflowStore();
|
||||
|
||||
// Prefer live workflow tasks (to reflect updated statuses), fallback to props
|
||||
const liveTasks = currentWorkflow?.tasks && Array.isArray(currentWorkflow.tasks) && currentWorkflow.tasks.length > 0
|
||||
? currentWorkflow.tasks
|
||||
: tasks;
|
||||
|
||||
// Get pillar-specific progress
|
||||
const pillarTasks = liveTasks.filter(task => task.pillarId === pillarId);
|
||||
const completedPillarTasks = pillarTasks.filter(task => task.status === 'completed' || task.status === 'skipped').length;
|
||||
const pillarProgress = pillarTasks.length > 0 ? (completedPillarTasks / pillarTasks.length) * 100 : 0;
|
||||
const isPillarComplete = pillarTasks.length > 0 && completedPillarTasks === pillarTasks.length;
|
||||
|
||||
// Auto-shake animation (only when pillar is not complete)
|
||||
useEffect(() => {
|
||||
if (isPillarComplete) {
|
||||
setShouldShake(false); // Stop any ongoing animation
|
||||
return; // Don't animate if pillar is complete
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setShouldShake(true);
|
||||
setTimeout(() => setShouldShake(false), 600);
|
||||
}, 8000 + delay * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [delay, isPillarComplete, liveTasks]);
|
||||
|
||||
// Auto-open Plan pillar modal when workflow starts (only if user hasn't manually closed it AND tasks are incomplete)
|
||||
useEffect(() => {
|
||||
if (pillarId === 'plan' &&
|
||||
currentWorkflow?.workflowStatus === 'in_progress' &&
|
||||
!modalOpen &&
|
||||
!userManuallyClosed &&
|
||||
!isPillarComplete) { // Only auto-open if Plan pillar tasks are not complete
|
||||
// Small delay to ensure smooth transition
|
||||
const timer = setTimeout(() => {
|
||||
setModalOpen(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [currentWorkflow?.workflowStatus, pillarId, modalOpen, userManuallyClosed, isPillarComplete]);
|
||||
|
||||
const handleClick = () => {
|
||||
setModalOpen(true);
|
||||
setUserManuallyClosed(false); // Reset the flag when user manually opens
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
if (pillarId === 'plan') {
|
||||
setUserManuallyClosed(true); // Mark that user manually closed the Plan modal
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
animate={shouldShake && !isPillarComplete ? { x: [-2, 2, -2, 2, 0] } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<Tooltip title={`🎯 Today's ${pillarTitle} Tasks - Click to View!`} arrow>
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
data-pillar-id={pillarId}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
'&::before': {
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
}
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `linear-gradient(45deg, transparent 30%, ${pillarColor}20 50%, transparent 70%)`,
|
||||
opacity: 0,
|
||||
transform: 'translateX(-100%)',
|
||||
transition: 'all 0.6s ease',
|
||||
borderRadius: 'inherit',
|
||||
zIndex: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
icon={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<TodayIcon sx={{
|
||||
fontSize: 16,
|
||||
'@keyframes rotate': {
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' }
|
||||
},
|
||||
animation: 'rotate 3s linear infinite'
|
||||
}} />
|
||||
<motion.span
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 1, repeat: Infinity }}
|
||||
>
|
||||
⚡
|
||||
</motion.span>
|
||||
</Box>
|
||||
}
|
||||
label="Today"
|
||||
sx={{
|
||||
height: 32,
|
||||
minWidth: 110,
|
||||
background: `linear-gradient(135deg, ${pillarColor} 0%, ${pillarColor}CC 100%)`,
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.75rem',
|
||||
border: `2px solid ${pillarColor}`,
|
||||
boxShadow: `
|
||||
0 4px 12px ${pillarColor}40,
|
||||
0 0 0 1px rgba(255,255,255,0.1),
|
||||
inset 0 1px 0 rgba(255,255,255,0.2)
|
||||
`,
|
||||
backdropFilter: 'blur(25px)',
|
||||
position: 'relative',
|
||||
zIndex: 1, // Lower z-index to not cover the large tick
|
||||
'&:hover': {
|
||||
boxShadow: `
|
||||
0 6px 20px ${pillarColor}60,
|
||||
0 0 0 1px rgba(255,255,255,0.2),
|
||||
inset 0 1px 0 rgba(255,255,255,0.3)
|
||||
`,
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
left: -2,
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
background: `linear-gradient(45deg, ${pillarColor}, transparent, ${pillarColor})`,
|
||||
borderRadius: 'inherit',
|
||||
zIndex: -1,
|
||||
'@keyframes attention-ring': {
|
||||
'0%, 100%': { opacity: 0, transform: 'scale(1)' },
|
||||
'50%': { opacity: 0.3, transform: 'scale(1.1)' }
|
||||
},
|
||||
animation: 'attention-ring 2s ease-in-out infinite'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Progress indicator */}
|
||||
{pillarProgress > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: '50%',
|
||||
background: pillarProgress === 100 ? '#4CAF50' : pillarColor,
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.6rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
border: '2px solid white'
|
||||
}}
|
||||
>
|
||||
{pillarProgress === 100 ? '✓' : Math.round(pillarProgress)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
|
||||
{/* Enhanced Modal */}
|
||||
<EnhancedTodayModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
pillarId={pillarId}
|
||||
pillarTitle={pillarTitle}
|
||||
pillarColor={pillarColor}
|
||||
tasks={liveTasks}
|
||||
onPreventAutoReopen={() => setUserManuallyClosed(true)}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedTodayChip;
|
||||
@@ -0,0 +1,498 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Modal,
|
||||
Paper,
|
||||
Button,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Stack,
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
Card,
|
||||
CardContent
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Today as TodayIcon,
|
||||
Close as CloseIcon,
|
||||
AutoAwesome as AlwrityIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
PlayArrow as PlayIcon,
|
||||
SkipNext as SkipIcon,
|
||||
NavigateNext
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWorkflowStore } from '../../../stores/workflowStore';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
interface EnhancedTodayModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
pillarId: string;
|
||||
pillarTitle: string;
|
||||
pillarColor: string;
|
||||
tasks: TodayTask[];
|
||||
// When navigating away (Next), prevent the previous pillar modal from auto-reopening
|
||||
onPreventAutoReopen?: () => void;
|
||||
}
|
||||
|
||||
// Enhanced Today Modal with Workflow Integration
|
||||
const EnhancedTodayModal: React.FC<EnhancedTodayModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
pillarId,
|
||||
pillarTitle,
|
||||
pillarColor,
|
||||
tasks,
|
||||
onPreventAutoReopen
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
currentWorkflow,
|
||||
workflowProgress,
|
||||
navigationState,
|
||||
completeTask,
|
||||
skipTask,
|
||||
moveToNextTask,
|
||||
isLoading,
|
||||
isWorkflowComplete
|
||||
} = useWorkflowStore();
|
||||
|
||||
const [selectedTask, setSelectedTask] = useState<TodayTask | null>(null);
|
||||
|
||||
// Prefer live workflow tasks (to reflect updated statuses), fallback to props
|
||||
const liveTasks = currentWorkflow?.tasks && Array.isArray(currentWorkflow.tasks) && currentWorkflow.tasks.length > 0
|
||||
? currentWorkflow.tasks
|
||||
: tasks;
|
||||
|
||||
// Filter tasks for this pillar
|
||||
const pillarTasks = liveTasks.filter(task => task.pillarId === pillarId);
|
||||
const currentTask = navigationState?.currentTask;
|
||||
const isComplete = isWorkflowComplete();
|
||||
|
||||
const handleTaskAction = async (task: TodayTask) => {
|
||||
if (!task.enabled) return;
|
||||
|
||||
try {
|
||||
// Execute the task action
|
||||
if (task.action) {
|
||||
task.action();
|
||||
} else if (task.actionUrl) {
|
||||
navigate(task.actionUrl);
|
||||
}
|
||||
|
||||
// Mark task as completed in workflow
|
||||
if (currentWorkflow) {
|
||||
await completeTask(task.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipTask = async (task: TodayTask) => {
|
||||
if (currentWorkflow) {
|
||||
await skipTask(task.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartWorkflow = async () => {
|
||||
if (currentWorkflow) {
|
||||
await moveToNextTask();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPillar = async () => {
|
||||
// Close current modal
|
||||
onClose();
|
||||
|
||||
// Prevent auto-reopen of current modal during navigation
|
||||
if (onPreventAutoReopen) {
|
||||
onPreventAutoReopen();
|
||||
}
|
||||
|
||||
// Navigate to next pillar
|
||||
if (nextPillarId) {
|
||||
setTimeout(() => {
|
||||
// Trigger next pillar modal opening
|
||||
const nextChip = document.querySelector(`[data-pillar-id="${nextPillarId}"]`);
|
||||
if (nextChip) {
|
||||
(nextChip as HTMLElement).click();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkflowComplete = async () => {
|
||||
// Mark all remaining tasks in this pillar as completed
|
||||
const incompleteTasks = pillarTasks.filter(task =>
|
||||
task.status !== 'completed' && task.status !== 'skipped'
|
||||
);
|
||||
|
||||
for (const task of incompleteTasks) {
|
||||
try {
|
||||
await completeTask(task.id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to complete task ${task.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Check if all tasks in this pillar are completed or skipped
|
||||
const areAllTasksCompleted = pillarTasks.every(task =>
|
||||
task.status === 'completed' || task.status === 'skipped'
|
||||
);
|
||||
|
||||
// Check if this is the Plan pillar
|
||||
const isPlanPillar = pillarId === 'plan';
|
||||
|
||||
// Define pillar order for navigation
|
||||
const pillarOrder = ['plan', 'generate', 'publish', 'analyze', 'engage', 'remarket'];
|
||||
const currentPillarIndex = pillarOrder.indexOf(pillarId);
|
||||
const isLastPillar = currentPillarIndex === pillarOrder.length - 1;
|
||||
const nextPillarId = !isLastPillar ? pillarOrder[currentPillarIndex + 1] : null;
|
||||
|
||||
const getTaskStatus = (task: TodayTask) => {
|
||||
if (task.status === 'completed') return 'completed';
|
||||
if (task.status === 'in_progress') return 'active';
|
||||
if (task.status === 'skipped') return 'skipped';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const getTaskStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return '#4CAF50';
|
||||
case 'active': return '#2196F3';
|
||||
case 'skipped': return '#FF9800';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: { xs: 1.5, md: 3 }
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
width: { xs: '96vw', sm: '94vw', md: '90vw' },
|
||||
maxWidth: 1200,
|
||||
maxHeight: '92vh',
|
||||
overflow: 'auto',
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.96) 0%, rgba(250,250,252,0.92) 100%)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 30px 60px rgba(0,0,0,0.35)',
|
||||
border: '1px solid rgba(0,0,0,0.06)'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ p: { xs: 2, md: 3 }, borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
background: pillarColor,
|
||||
width: 48,
|
||||
height: 48
|
||||
}}
|
||||
>
|
||||
<TodayIcon sx={{ fontSize: 24, color: 'white' }} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, color: '#23252F', letterSpacing: 0.2 }}>
|
||||
Today's {pillarTitle} Tasks
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#5A5F6A' }}>
|
||||
Complete your daily marketing workflow
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<IconButton onClick={onClose} sx={{ color: '#6B7280' }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Workflow Progress - Circular in Header */}
|
||||
{workflowProgress && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: { xs: 2, md: 3 },
|
||||
borderBottom: '1px solid rgba(0,0,0,0.08)'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#5A5F6A', fontWeight: 600 }}>
|
||||
Overall Progress
|
||||
</Typography>
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={workflowProgress.completionPercentage}
|
||||
size={40}
|
||||
thickness={4}
|
||||
sx={{
|
||||
color: pillarColor,
|
||||
'& .MuiCircularProgress-circle': {
|
||||
strokeLinecap: 'round',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
color: '#5A5F6A',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
>
|
||||
{`${Math.round(workflowProgress.completionPercentage)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: '#5A5F6A', fontWeight: 600 }}>
|
||||
{workflowProgress.completedTasks} of {workflowProgress.totalTasks} tasks
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Tasks List */}
|
||||
<Box sx={{ p: { xs: 2, md: 3 } }}>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: '#23252F', fontWeight: 800 }}>
|
||||
{pillarTitle} Tasks
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{pillarTasks.map((task, index) => {
|
||||
const status = getTaskStatus(task);
|
||||
const statusColor = getTaskStatusColor(status);
|
||||
const isCurrentTask = currentTask?.id === task.id;
|
||||
const IconComponent = (typeof task.icon === 'function' ? task.icon : undefined) as any;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
border: isCurrentTask ? `2px solid ${pillarColor}` : '1px solid rgba(0,0,0,0.08)',
|
||||
background: isCurrentTask ? `${pillarColor}12` : 'white',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.08)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 2, md: 2.5 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
||||
{IconComponent && (
|
||||
<Avatar
|
||||
sx={{
|
||||
background: statusColor,
|
||||
width: 36,
|
||||
height: 36
|
||||
}}
|
||||
>
|
||||
<IconComponent sx={{ fontSize: 18, color: 'white' }} />
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#23252F' }}>
|
||||
{task.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#5A5F6A' }}>
|
||||
{task.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
label={status}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `${statusColor}18`,
|
||||
color: statusColor,
|
||||
border: `1px solid ${statusColor}40`,
|
||||
textTransform: 'capitalize'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="caption" sx={{ color: '#999' }}>
|
||||
{task.estimatedTime} min
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Task Actions */}
|
||||
<Box sx={{ display: 'flex', gap: 1.25, mt: 2 }}>
|
||||
{status === 'pending' && task.enabled && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<AlwrityIcon />}
|
||||
onClick={() => handleTaskAction(task)}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
background: pillarColor,
|
||||
'&:hover': {
|
||||
background: pillarColor,
|
||||
opacity: 0.9
|
||||
}
|
||||
}}
|
||||
>
|
||||
ALwrity it
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === 'active' && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<PlayIcon />}
|
||||
onClick={() => handleTaskAction(task)}
|
||||
disabled={isLoading}
|
||||
sx={{ borderColor: pillarColor, color: pillarColor }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === 'completed' && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<CheckIcon />}
|
||||
disabled
|
||||
sx={{ borderColor: '#4CAF50', color: '#4CAF50' }}
|
||||
>
|
||||
Completed
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === 'pending' && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<SkipIcon />}
|
||||
onClick={() => handleSkipTask(task)}
|
||||
disabled={isLoading}
|
||||
sx={{ color: '#FF9800' }}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<Box sx={{ p: { xs: 2, md: 3 }, borderTop: '1px solid rgba(0,0,0,0.08)' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2" sx={{ color: '#5A5F6A' }}>
|
||||
{isComplete ? '🎉 All tasks completed!' : `${pillarTasks.length} tasks in this pillar`}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
|
||||
{/* Next button for all pillars except the last one */}
|
||||
{!isLastPillar && (
|
||||
<>
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Tooltip
|
||||
title={areAllTasksCompleted
|
||||
? `All tasks completed! Click to proceed to ${nextPillarId ? nextPillarId.charAt(0).toUpperCase() + nextPillarId.slice(1) : 'next'} pillar`
|
||||
: "Complete or skip all tasks in this pillar to proceed"
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<NavigateNext />}
|
||||
onClick={handleNextPillar}
|
||||
disabled={!areAllTasksCompleted || isLoading}
|
||||
sx={{
|
||||
background: pillarColor,
|
||||
'&:hover': {
|
||||
background: pillarColor,
|
||||
opacity: 0.9
|
||||
},
|
||||
'&:disabled': {
|
||||
background: '#ccc',
|
||||
color: '#666'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Last pillar (Remarket) - Workflow Complete button acts as close */}
|
||||
{isLastPillar && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<CheckIcon />}
|
||||
onClick={handleWorkflowComplete}
|
||||
sx={{
|
||||
background: '#4CAF50',
|
||||
'&:hover': {
|
||||
background: '#45a049',
|
||||
opacity: 0.9
|
||||
}
|
||||
}}
|
||||
>
|
||||
Workflow Complete!
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedTodayModal;
|
||||
@@ -0,0 +1,609 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Modal,
|
||||
Paper,
|
||||
Button,
|
||||
IconButton,
|
||||
Divider,
|
||||
Avatar,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Today as TodayIcon,
|
||||
TextFields as TextIcon,
|
||||
Image as ImageIcon,
|
||||
AudioFile as AudioIcon,
|
||||
VideoFile as VideoIcon,
|
||||
Close as CloseIcon,
|
||||
Facebook as FacebookIcon,
|
||||
LinkedIn as LinkedInIcon,
|
||||
Language as WebsiteIcon,
|
||||
AutoAwesome as AlwrityIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import EnhancedTodayChip from './EnhancedTodayChip';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
// Today Modal Component
|
||||
const TodayModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}> = ({ open, onClose }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
id: 'facebook',
|
||||
title: "Post 'ALwrity AI Content Generation' on Facebook",
|
||||
platform: 'Facebook',
|
||||
icon: FacebookIcon,
|
||||
color: '#1877F2',
|
||||
enabled: true,
|
||||
action: () => navigate('/facebook-writer')
|
||||
},
|
||||
{
|
||||
id: 'website',
|
||||
title: 'Write a Blog on "AI Image generation prompts" for wix website',
|
||||
platform: 'Website',
|
||||
icon: WebsiteIcon,
|
||||
color: '#FF6B35',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
id: 'linkedin',
|
||||
title: "Write & Post on LinkedIn on 'AI Agents frameworks latest news'",
|
||||
platform: 'LinkedIn',
|
||||
icon: LinkedInIcon,
|
||||
color: '#0077B5',
|
||||
enabled: true,
|
||||
action: () => navigate('/linkedin-writer')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
width: { xs: '95%', sm: '90%', md: '600px' },
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
background: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
color: 'white',
|
||||
borderRadius: '12px 12px 0 0',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
color: 'white',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
mr: 2,
|
||||
width: 48,
|
||||
height: 48
|
||||
}}
|
||||
>
|
||||
<TodayIcon sx={{ fontSize: 24 }} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
Today's Tasks
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
AI-powered content generation for today
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, color: '#1565C0' }}>
|
||||
🚀 Ready to Generate Content
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{tasks.map((task, index) => {
|
||||
const IconComponent = task.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={task.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: 2,
|
||||
border: `2px solid ${task.enabled ? task.color : '#E0E0E0'}`,
|
||||
backgroundColor: task.enabled ? 'white' : '#F5F5F5',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 8px 24px ${task.color}20`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: task.enabled ? task.color : '#BDBDBD',
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
>
|
||||
<IconComponent sx={{ fontSize: 20, color: 'white' }} />
|
||||
</Avatar>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
{task.title}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={task.platform}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: task.enabled ? `${task.color}20` : '#E0E0E0',
|
||||
color: task.enabled ? task.color : '#757575',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AlwrityIcon />}
|
||||
onClick={task.action}
|
||||
disabled={!task.enabled}
|
||||
sx={{
|
||||
background: task.enabled
|
||||
? `linear-gradient(135deg, ${task.color} 0%, ${task.color}CC 100%)`
|
||||
: '#E0E0E0',
|
||||
color: 'white',
|
||||
px: 3,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
boxShadow: task.enabled
|
||||
? `0 4px 12px ${task.color}40`
|
||||
: 'none',
|
||||
'&:hover': task.enabled ? {
|
||||
background: `linear-gradient(135deg, ${task.color}CC 0%, ${task.color} 100%)`,
|
||||
boxShadow: `0 6px 16px ${task.color}50`
|
||||
} : {},
|
||||
'&:disabled': {
|
||||
backgroundColor: '#E0E0E0',
|
||||
color: '#9E9E9E'
|
||||
}
|
||||
}}
|
||||
>
|
||||
ALwrity it
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 2 }}>
|
||||
💡 Tip: Use ALwrity's AI to generate engaging content tailored to each platform
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// Enhanced Chip Component for Generate Pillar
|
||||
const GenerateChip: React.FC<{
|
||||
chip: any;
|
||||
delay?: number;
|
||||
onTodayClick?: () => void;
|
||||
}> = ({ chip, delay = 0, onTodayClick }) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (chip.bubbles && chip.bubbles.length > 0) {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % chip.bubbles.length);
|
||||
}, 2000 + delay * 300);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [chip.bubbles?.length, delay]);
|
||||
|
||||
const IconComponent = chip.icon;
|
||||
|
||||
const handleClick = () => {
|
||||
if (chip.label === 'Today' && onTodayClick) {
|
||||
onTodayClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
chip.bubbles && chip.bubbles.length > 0 ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
{chip.label}
|
||||
</Typography>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: 'white' }}>
|
||||
{chip.bubbles[currentIndex]}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
) : chip.label
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px) scale(1.05)',
|
||||
'& .chip-glow': {
|
||||
opacity: 1,
|
||||
transform: 'scale(1.2)'
|
||||
},
|
||||
'& .chip-shadow': {
|
||||
opacity: 0.6,
|
||||
transform: 'translateY(8px) scale(1.1)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Glow Effect */}
|
||||
<Box
|
||||
className="chip-glow"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
left: -8,
|
||||
right: -8,
|
||||
bottom: -8,
|
||||
background: chip.gradient || chip.color,
|
||||
borderRadius: '20px',
|
||||
opacity: 0,
|
||||
transition: 'all 0.4s ease',
|
||||
filter: 'blur(12px)',
|
||||
zIndex: -2
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Shadow Effect */}
|
||||
<Box
|
||||
className="chip-shadow"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
left: 2,
|
||||
right: -2,
|
||||
bottom: -4,
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: '16px',
|
||||
opacity: 0.3,
|
||||
transition: 'all 0.4s ease',
|
||||
filter: 'blur(8px)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main Chip */}
|
||||
<Chip
|
||||
icon={<IconComponent sx={{ fontSize: 14 }} />}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, fontSize: '0.7rem' }}>
|
||||
{chip.label}
|
||||
</Typography>
|
||||
{chip.value && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
color: chip.color,
|
||||
borderRadius: '50%',
|
||||
width: 16,
|
||||
height: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.6rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
>
|
||||
{chip.value}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(255,255,255,0.25) 0%,
|
||||
rgba(255,255,255,0.1) 50%,
|
||||
rgba(255,255,255,0.05) 100%)`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.3)',
|
||||
color: 'white',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)',
|
||||
transition: 'left 0.6s ease',
|
||||
zIndex: 1
|
||||
},
|
||||
'&:hover::before': {
|
||||
left: '100%'
|
||||
},
|
||||
'& .MuiChip-label': {
|
||||
px: 1,
|
||||
zIndex: 2,
|
||||
position: 'relative'
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
zIndex: 2,
|
||||
position: 'relative'
|
||||
},
|
||||
'&:hover': {
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(255,255,255,0.35) 0%,
|
||||
rgba(255,255,255,0.2) 50%,
|
||||
rgba(255,255,255,0.1) 100%)`,
|
||||
border: '1px solid rgba(255,255,255,0.5)',
|
||||
boxShadow: `0 8px 32px ${chip.color}40,
|
||||
0 4px 16px rgba(0,0,0,0.1),
|
||||
inset 0 1px 0 rgba(255,255,255,0.3)`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
// Generate Pillar Chips Component
|
||||
const GeneratePillarChips: React.FC<{
|
||||
index: number;
|
||||
isHovered?: boolean;
|
||||
}> = ({ index, isHovered = false }) => {
|
||||
// Generate pillar Today tasks
|
||||
const generateTodayTasks: TodayTask[] = [
|
||||
{
|
||||
id: 'facebook-post',
|
||||
pillarId: 'generate',
|
||||
title: "Post 'ALwrity AI Content Generation' on Facebook",
|
||||
description: 'Create and publish engaging Facebook content',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/facebook-writer',
|
||||
icon: FacebookIcon,
|
||||
color: '#1877F2',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to Facebook writer')
|
||||
},
|
||||
{
|
||||
id: 'blog-post',
|
||||
pillarId: 'generate',
|
||||
title: 'Write Blog on "AI Image Generation Prompts"',
|
||||
description: 'Create comprehensive blog post for website',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 30,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/blog-writer',
|
||||
icon: WebsiteIcon,
|
||||
color: '#FF6B35',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
id: 'linkedin-post',
|
||||
pillarId: 'generate',
|
||||
title: "Write & Post on LinkedIn 'AI Agents Frameworks'",
|
||||
description: 'Create professional LinkedIn content',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/linkedin-writer',
|
||||
icon: LinkedInIcon,
|
||||
color: '#0077B5',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to LinkedIn writer')
|
||||
}
|
||||
];
|
||||
|
||||
// Generate pillar chips data
|
||||
const generateChips = {
|
||||
text: {
|
||||
label: 'Text',
|
||||
icon: TextIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['Blog posts', 'Social media', 'Email content']
|
||||
},
|
||||
image: {
|
||||
label: 'Image',
|
||||
icon: ImageIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['Visual content', 'Infographics', 'Social images']
|
||||
},
|
||||
audio: {
|
||||
label: 'Audio',
|
||||
icon: AudioIcon,
|
||||
color: '#9C27B0',
|
||||
gradient: 'linear-gradient(135deg, #9C27B0 0%, #6A1B9A 100%)',
|
||||
bubbles: ['Podcast scripts', 'Voice content', 'Audio ads']
|
||||
},
|
||||
video: {
|
||||
label: 'Video',
|
||||
icon: VideoIcon,
|
||||
color: '#E91E63',
|
||||
gradient: 'linear-gradient(135deg, #E91E63 0%, #C2185B 100%)',
|
||||
bubbles: ['Video scripts', 'YouTube content', 'Social videos']
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
flexGrow: 1,
|
||||
justifyContent: isHovered ? 'center' : 'flex-start'
|
||||
}}
|
||||
>
|
||||
{/* Today Chip - Always Visible */}
|
||||
<EnhancedTodayChip
|
||||
pillarId="generate"
|
||||
pillarTitle="Generate"
|
||||
pillarColor="#1565C0"
|
||||
tasks={generateTodayTasks}
|
||||
delay={index * 5}
|
||||
/>
|
||||
|
||||
{/* More Options Indicator */}
|
||||
{!isHovered && (
|
||||
<motion.div
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
||||
style={{ alignSelf: 'center', marginTop: '8px' }}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.6rem', opacity: 0.7, color: 'white' }}>
|
||||
⋯
|
||||
</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Content Type Chips - Progressive Disclosure */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<GenerateChip chip={generateChips.text} delay={index * 5 + 1} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<GenerateChip chip={generateChips.image} delay={index * 5 + 2} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<GenerateChip chip={generateChips.audio} delay={index * 5 + 3} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<GenerateChip chip={generateChips.video} delay={index * 5 + 4} />
|
||||
</motion.div>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneratePillarChips;
|
||||
@@ -0,0 +1,324 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Modal,
|
||||
Paper,
|
||||
Button,
|
||||
IconButton,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
Avatar,
|
||||
Stack,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Settings as SettingsIcon,
|
||||
PersonAdd as OnboardingIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Psychology as PsychologyIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Onboarding Modal Component
|
||||
const OnboardingModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}> = ({ open, onClose }) => {
|
||||
// Mock onboarding data - in real app, this would come from database
|
||||
const onboardingData = {
|
||||
userProfile: {
|
||||
name: 'John Doe',
|
||||
company: 'TechCorp Inc.',
|
||||
role: 'Marketing Manager',
|
||||
completion: 85
|
||||
},
|
||||
preferences: {
|
||||
contentTypes: ['Blog Posts', 'Social Media', 'Email Campaigns'],
|
||||
platforms: ['LinkedIn', 'Facebook', 'Twitter'],
|
||||
tone: 'Professional',
|
||||
frequency: 'Daily'
|
||||
},
|
||||
goals: {
|
||||
primary: 'Increase brand awareness',
|
||||
secondary: 'Generate leads',
|
||||
metrics: ['Engagement Rate', 'Click-through Rate', 'Conversion Rate']
|
||||
},
|
||||
aiAnalysis: {
|
||||
score: 8.5,
|
||||
insights: [
|
||||
'Strong foundation with clear goals and preferences',
|
||||
'Content strategy well-aligned with target audience',
|
||||
'Consider expanding to Instagram for better reach',
|
||||
'Email campaigns could benefit from A/B testing'
|
||||
],
|
||||
recommendations: [
|
||||
'Set up automated content scheduling',
|
||||
'Implement advanced analytics tracking',
|
||||
'Create content templates for consistency',
|
||||
'Establish brand voice guidelines'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
width: { xs: '95%', sm: '90%', md: '80%', lg: '70%' },
|
||||
maxWidth: 800,
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
background: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
color: 'white',
|
||||
borderRadius: '12px 12px 0 0',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
color: 'white',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
mr: 2,
|
||||
width: 48,
|
||||
height: 48
|
||||
}}
|
||||
>
|
||||
<OnboardingIcon sx={{ fontSize: 24 }} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
Onboarding Status
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Complete your setup to unlock full potential
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
Overall Progress
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{onboardingData.userProfile.completion}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={onboardingData.userProfile.completion}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 4
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* User Profile Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: '#2E7D32' }}>
|
||||
👤 User Profile
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(2, 1fr)' }, gap: 2 }}>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Name</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.userProfile.name}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Company</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.userProfile.company}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Role</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.userProfile.role}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Completion</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, color: '#4CAF50' }}>{onboardingData.userProfile.completion}%</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Preferences Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: '#2E7D32' }}>
|
||||
⚙️ Preferences
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(2, 1fr)' }, gap: 2 }}>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>Content Types</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{onboardingData.preferences.contentTypes.map((type, idx) => (
|
||||
<Chip key={idx} label={type} size="small" sx={{ backgroundColor: '#E3F2FD', color: '#1976D2' }} />
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>Platforms</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{onboardingData.preferences.platforms.map((platform, idx) => (
|
||||
<Chip key={idx} label={platform} size="small" sx={{ backgroundColor: '#E8F5E8', color: '#2E7D32' }} />
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Tone</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.preferences.tone}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Frequency</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.preferences.frequency}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Goals Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: '#2E7D32' }}>
|
||||
🎯 Goals
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(2, 1fr)' }, gap: 2 }}>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Primary Goal</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.goals.primary}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 0.5 }}>Secondary Goal</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{onboardingData.goals.secondary}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, backgroundColor: 'white', borderRadius: 2, boxShadow: 1, gridColumn: { xs: '1', md: '1 / -1' } }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>Key Metrics</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{onboardingData.goals.metrics.map((metric, idx) => (
|
||||
<Chip key={idx} label={metric} size="small" sx={{ backgroundColor: '#FFF3E0', color: '#F57C00' }} />
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* AI Analysis Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, color: '#2E7D32' }}>
|
||||
🤖 AI Analysis
|
||||
</Typography>
|
||||
<Box sx={{ p: 3, backgroundColor: 'white', borderRadius: 2, boxShadow: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<PsychologyIcon sx={{ color: '#9C27B0', mr: 1 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Analysis Score: {onboardingData.aiAnalysis.score}/10
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: '#2E7D32' }}>
|
||||
Key Insights:
|
||||
</Typography>
|
||||
{onboardingData.aiAnalysis.insights.map((insight, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', alignItems: 'flex-start', mb: 1 }}>
|
||||
<CheckIcon sx={{ color: '#4CAF50', fontSize: 16, mr: 1, mt: 0.5 }} />
|
||||
<Typography variant="body2">{insight}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: '#2E7D32' }}>
|
||||
Recommendations:
|
||||
</Typography>
|
||||
{onboardingData.aiAnalysis.recommendations.map((rec, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', alignItems: 'flex-start', mb: 1 }}>
|
||||
<TrendingUpIcon sx={{ color: '#FF9800', fontSize: 16, mr: 1, mt: 0.5 }} />
|
||||
<Typography variant="body2">{rec}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Settings Button */}
|
||||
<Divider sx={{ my: 3 }} />
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<SettingsIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #9C27B0 0%, #6A1B9A 100%)',
|
||||
color: 'white',
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
borderRadius: 2,
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 4px 12px rgba(156, 39, 176, 0.3)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #6A1B9A 0%, #4A148C 100%)',
|
||||
boxShadow: '0 6px 16px rgba(156, 39, 176, 0.4)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Onboarding Data
|
||||
</Button>
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1, color: 'text.secondary' }}>
|
||||
Configure your preferences and goals in the Settings page
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingModal;
|
||||
576
frontend/src/components/MainDashboard/components/PillarData.tsx
Normal file
576
frontend/src/components/MainDashboard/components/PillarData.tsx
Normal file
@@ -0,0 +1,576 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
// Plan pillar icons
|
||||
Assignment as PlanIcon,
|
||||
PersonAdd as OnboardingIcon,
|
||||
Business as StrategyIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
RateReview as ReviewIcon,
|
||||
|
||||
// Generate pillar icons
|
||||
AutoAwesome as GenerateIcon,
|
||||
ThumbUp as GoodIcon,
|
||||
ThumbDown as BadIcon,
|
||||
Warning as UglyIcon,
|
||||
|
||||
// Publish pillar icons
|
||||
Publish as PublishIcon,
|
||||
|
||||
// Analyze pillar icons
|
||||
Analytics as AnalyzeIcon,
|
||||
|
||||
// Engage pillar icons
|
||||
Campaign as EngageIcon,
|
||||
|
||||
// Remarket pillar icons
|
||||
Psychology as RemarketIcon,
|
||||
|
||||
// Task icons
|
||||
Facebook as FacebookIcon,
|
||||
LinkedIn as LinkedInIcon,
|
||||
Language as WebsiteIcon,
|
||||
ChatBubbleOutline as ChatIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Share as ShareIcon,
|
||||
ThumbUp as ThumbUpIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Article as ArticleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
// Define the chip interface
|
||||
export interface PillarChip {
|
||||
label: string;
|
||||
icon: React.ComponentType<any>;
|
||||
color: string;
|
||||
gradient: string;
|
||||
bubbles: string[];
|
||||
value?: number | null;
|
||||
}
|
||||
|
||||
// Define the pillar data interface
|
||||
export interface PillarData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: React.ComponentType<any>;
|
||||
color: string;
|
||||
gradient: string;
|
||||
chips: {
|
||||
[key: string]: PillarChip;
|
||||
};
|
||||
todayTasks: TodayTask[];
|
||||
}
|
||||
|
||||
// Enhanced pillar data with Today tasks
|
||||
export const pillarData: PillarData[] = [
|
||||
{
|
||||
id: 'plan',
|
||||
title: 'Plan',
|
||||
icon: PlanIcon,
|
||||
color: '#2E7D32',
|
||||
gradient: 'linear-gradient(135deg, #2E7D32 0%, #1B5E20 100%)',
|
||||
chips: {
|
||||
onboarding: {
|
||||
label: 'On-Boarding',
|
||||
icon: OnboardingIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['User Profile Setup', 'Preferences Configured', 'Goals Defined'],
|
||||
value: 2
|
||||
},
|
||||
strategy: {
|
||||
label: 'Strategy',
|
||||
icon: StrategyIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Content Strategy Defined', 'Target Audience Identified', 'Brand Voice Established'],
|
||||
value: 7
|
||||
},
|
||||
calendar: {
|
||||
label: 'Calendar',
|
||||
icon: CalendarIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['Publishing Schedule Set', 'Content Calendar Created', 'Campaign Timeline Planned'],
|
||||
value: 11
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#9C27B0',
|
||||
gradient: 'linear-gradient(135deg, #9C27B0 0%, #6A1B9A 100%)',
|
||||
bubbles: ['Content Calendar Generated', 'SEO Strategy Optimized', 'Topic Clusters Identified'],
|
||||
value: null
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'content-calendar',
|
||||
pillarId: 'plan',
|
||||
title: 'Create Weekly Content Calendar',
|
||||
description: 'Plan and schedule content for the upcoming week',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
icon: CalendarIcon,
|
||||
color: '#2E7D32',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to content calendar')
|
||||
},
|
||||
{
|
||||
id: 'seo-strategy',
|
||||
pillarId: 'plan',
|
||||
title: 'Update SEO Strategy',
|
||||
description: 'Review and optimize SEO keywords and content strategy',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/seo-strategy',
|
||||
icon: AssessmentIcon,
|
||||
color: '#2196F3',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to SEO strategy')
|
||||
},
|
||||
{
|
||||
id: 'competitor-analysis',
|
||||
pillarId: 'plan',
|
||||
title: 'Competitor Analysis',
|
||||
description: 'Analyze competitor content and identify opportunities',
|
||||
status: 'pending' as const,
|
||||
priority: 'low' as const,
|
||||
estimatedTime: 30,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/competitor-analysis',
|
||||
icon: AnalyzeIcon,
|
||||
color: '#FF9800',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'generate',
|
||||
title: 'Generate',
|
||||
icon: GenerateIcon,
|
||||
color: '#1565C0',
|
||||
gradient: 'linear-gradient(135deg, #1565C0 0%, #0D47A1 100%)',
|
||||
chips: {
|
||||
good: {
|
||||
label: 'Quality Content',
|
||||
icon: GoodIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['SEO Optimized', 'Brand Voice Consistent', 'Engaging Headlines']
|
||||
},
|
||||
bad: {
|
||||
label: 'Content Issues',
|
||||
icon: BadIcon,
|
||||
color: '#F44336',
|
||||
gradient: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
|
||||
bubbles: ['Poor Grammar', 'Weak CTAs', 'Generic Content']
|
||||
},
|
||||
ugly: {
|
||||
label: 'Critical Problems',
|
||||
icon: UglyIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['No Brand Voice', 'Plagiarized Content', 'No SEO Optimization']
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Blog Post Generated', 'Social Media Content Created', 'Email Campaign Written']
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'facebook-post',
|
||||
pillarId: 'generate',
|
||||
title: "Post 'ALwrity AI Content Generation' on Facebook",
|
||||
description: 'Create and publish engaging Facebook content',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/facebook-writer',
|
||||
icon: FacebookIcon,
|
||||
color: '#1877F2',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to Facebook writer')
|
||||
},
|
||||
{
|
||||
id: 'blog-post',
|
||||
pillarId: 'generate',
|
||||
title: 'Write Blog on "AI Image Generation Prompts"',
|
||||
description: 'Create comprehensive blog post for website',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 45,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/blog-writer',
|
||||
icon: ArticleIcon,
|
||||
color: '#FF6B35',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
id: 'linkedin-post',
|
||||
pillarId: 'generate',
|
||||
title: "Write & Post on LinkedIn 'AI Agents Frameworks'",
|
||||
description: 'Create professional LinkedIn content',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/linkedin-writer',
|
||||
icon: LinkedInIcon,
|
||||
color: '#0077B5',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to LinkedIn writer')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'publish',
|
||||
title: 'Publish',
|
||||
icon: PublishIcon,
|
||||
color: '#E65100',
|
||||
gradient: 'linear-gradient(135deg, #E65100 0%, #BF360C 100%)',
|
||||
chips: {
|
||||
good: {
|
||||
label: 'Smooth Publishing',
|
||||
icon: GoodIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['Multi-Platform Sync', 'Optimal Timing', 'Auto-Scheduling']
|
||||
},
|
||||
bad: {
|
||||
label: 'Publishing Issues',
|
||||
icon: BadIcon,
|
||||
color: '#F44336',
|
||||
gradient: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
|
||||
bubbles: ['Manual Publishing', 'Poor Timing', 'Platform Errors']
|
||||
},
|
||||
ugly: {
|
||||
label: 'Critical Failures',
|
||||
icon: UglyIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['No Publishing Strategy', 'Content Not Published', 'Platform Disconnects']
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Content Published to LinkedIn', 'Facebook Post Scheduled', 'Twitter Thread Live']
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'schedule-posts',
|
||||
pillarId: 'publish',
|
||||
title: 'Schedule Today\'s Content',
|
||||
description: 'Schedule all content for optimal engagement times',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 10,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/publishing-scheduler',
|
||||
icon: PublishIcon,
|
||||
color: '#E65100',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to publishing scheduler')
|
||||
},
|
||||
{
|
||||
id: 'cross-platform',
|
||||
pillarId: 'publish',
|
||||
title: 'Cross-Platform Publishing',
|
||||
description: 'Publish content across all connected platforms',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/cross-platform-publisher',
|
||||
icon: ShareIcon,
|
||||
color: '#4CAF50',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to cross-platform publisher')
|
||||
},
|
||||
{
|
||||
id: 'publish-analytics',
|
||||
pillarId: 'publish',
|
||||
title: 'Publishing Analytics Review',
|
||||
description: 'Review publishing performance and optimize timing',
|
||||
status: 'pending' as const,
|
||||
priority: 'low' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/publishing-analytics',
|
||||
icon: AnalyzeIcon,
|
||||
color: '#2196F3',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'analyze',
|
||||
title: 'Analyze',
|
||||
icon: AnalyzeIcon,
|
||||
color: '#6A1B9A',
|
||||
gradient: 'linear-gradient(135deg, #6A1B9A 0%, #4A148C 100%)',
|
||||
chips: {
|
||||
good: {
|
||||
label: 'Great Analytics',
|
||||
icon: GoodIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['Real-time Tracking', 'Detailed Insights', 'ROI Measurement']
|
||||
},
|
||||
bad: {
|
||||
label: 'Analytics Gaps',
|
||||
icon: BadIcon,
|
||||
color: '#F44336',
|
||||
gradient: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
|
||||
bubbles: ['Limited Tracking', 'No ROI Data', 'Poor Reporting']
|
||||
},
|
||||
ugly: {
|
||||
label: 'No Analytics',
|
||||
icon: UglyIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['No Tracking Setup', 'No Performance Data', 'Blind Publishing']
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Engagement Rate: +25%', 'Click-Through Rate Improved', 'Social Shares Increased']
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'performance-report',
|
||||
pillarId: 'analyze',
|
||||
title: 'Generate Performance Report',
|
||||
description: 'Create comprehensive analytics report for this week',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 25,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/analytics-dashboard',
|
||||
icon: AnalyzeIcon,
|
||||
color: '#6A1B9A',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to analytics dashboard')
|
||||
},
|
||||
{
|
||||
id: 'engagement-analysis',
|
||||
pillarId: 'analyze',
|
||||
title: 'Engagement Analysis',
|
||||
description: 'Analyze engagement metrics and identify trends',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/engagement-analytics',
|
||||
icon: ThumbUpIcon,
|
||||
color: '#4CAF50',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to engagement analytics')
|
||||
},
|
||||
{
|
||||
id: 'roi-calculator',
|
||||
pillarId: 'analyze',
|
||||
title: 'ROI Calculator Update',
|
||||
description: 'Update ROI calculations and performance metrics',
|
||||
status: 'pending' as const,
|
||||
priority: 'low' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/roi-calculator',
|
||||
icon: AssessmentIcon,
|
||||
color: '#FF9800',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'engage',
|
||||
title: 'Engage',
|
||||
icon: EngageIcon,
|
||||
color: '#C2185B',
|
||||
gradient: 'linear-gradient(135deg, #C2185B 0%, #880E4F 100%)',
|
||||
chips: {
|
||||
good: {
|
||||
label: 'High Engagement',
|
||||
icon: GoodIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['Active Community', 'Quick Responses', 'Viral Content']
|
||||
},
|
||||
bad: {
|
||||
label: 'Low Engagement',
|
||||
icon: BadIcon,
|
||||
color: '#F44336',
|
||||
gradient: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
|
||||
bubbles: ['Slow Responses', 'Few Interactions', 'Poor Community']
|
||||
},
|
||||
ugly: {
|
||||
label: 'No Engagement',
|
||||
icon: UglyIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['No Community', 'No Responses', 'Ignored Content']
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Comments Responded Automatically', 'Community Engagement Boosted', 'Customer Queries Resolved']
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'respond-comments',
|
||||
pillarId: 'engage',
|
||||
title: 'Respond to Comments',
|
||||
description: 'Engage with audience comments across all platforms',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 30,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/comment-management',
|
||||
icon: ChatIcon,
|
||||
color: '#C2185B',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to comment management')
|
||||
},
|
||||
{
|
||||
id: 'community-building',
|
||||
pillarId: 'engage',
|
||||
title: 'Community Building',
|
||||
description: 'Foster community engagement and build relationships',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 25,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/community-tools',
|
||||
icon: ThumbUpIcon,
|
||||
color: '#4CAF50',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to community tools')
|
||||
},
|
||||
{
|
||||
id: 'engagement-strategy',
|
||||
pillarId: 'engage',
|
||||
title: 'Engagement Strategy Review',
|
||||
description: 'Review and optimize engagement strategies',
|
||||
status: 'pending' as const,
|
||||
priority: 'low' as const,
|
||||
estimatedTime: 20,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/engagement-strategy',
|
||||
icon: AssessmentIcon,
|
||||
color: '#FF9800',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'remarket',
|
||||
title: 'Remarket',
|
||||
icon: RemarketIcon,
|
||||
color: '#00695C',
|
||||
gradient: 'linear-gradient(135deg, #00695C 0%, #004D40 100%)',
|
||||
chips: {
|
||||
good: {
|
||||
label: 'Smart Remarketing',
|
||||
icon: GoodIcon,
|
||||
color: '#4CAF50',
|
||||
gradient: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
||||
bubbles: ['Targeted Campaigns', 'High Conversion', 'ROI Optimized']
|
||||
},
|
||||
bad: {
|
||||
label: 'Poor Remarketing',
|
||||
icon: BadIcon,
|
||||
color: '#F44336',
|
||||
gradient: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
|
||||
bubbles: ['Low Conversion', 'Poor Targeting', 'Wasted Budget']
|
||||
},
|
||||
ugly: {
|
||||
label: 'No Remarketing',
|
||||
icon: UglyIcon,
|
||||
color: '#FF9800',
|
||||
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
|
||||
bubbles: ['No Retargeting', 'Lost Opportunities', 'No Lead Nurturing']
|
||||
},
|
||||
review: {
|
||||
label: 'Review & Optimize',
|
||||
icon: ReviewIcon,
|
||||
color: '#2196F3',
|
||||
gradient: 'linear-gradient(135deg, #2196F3 0%, #1565C0 100%)',
|
||||
bubbles: ['Remarketing Campaign Launched', 'Content Amplified Successfully', 'Lead Nurturing Sequence Active']
|
||||
}
|
||||
},
|
||||
todayTasks: [
|
||||
{
|
||||
id: 'retargeting-campaign',
|
||||
pillarId: 'remarket',
|
||||
title: 'Launch Retargeting Campaign',
|
||||
description: 'Create and launch targeted remarketing campaigns',
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 35,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/remarketing-dashboard',
|
||||
icon: RemarketIcon,
|
||||
color: '#00695C',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to remarketing dashboard')
|
||||
},
|
||||
{
|
||||
id: 'lead-nurturing',
|
||||
pillarId: 'remarket',
|
||||
title: 'Lead Nurturing Sequence',
|
||||
description: 'Set up automated lead nurturing workflows',
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 30,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/lead-nurturing',
|
||||
icon: RefreshIcon,
|
||||
color: '#4CAF50',
|
||||
enabled: true,
|
||||
action: () => console.log('Navigate to lead nurturing')
|
||||
},
|
||||
{
|
||||
id: 'conversion-optimization',
|
||||
pillarId: 'remarket',
|
||||
title: 'Conversion Optimization',
|
||||
description: 'Optimize remarketing campaigns for better conversion',
|
||||
status: 'pending' as const,
|
||||
priority: 'low' as const,
|
||||
estimatedTime: 25,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/conversion-optimization',
|
||||
icon: AssessmentIcon,
|
||||
color: '#FF9800',
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default pillarData;
|
||||
@@ -0,0 +1,338 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, useTheme } from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Facebook,
|
||||
LinkedIn,
|
||||
Twitter,
|
||||
Instagram,
|
||||
YouTube,
|
||||
Article,
|
||||
CheckCircle
|
||||
} from '@mui/icons-material';
|
||||
import EnhancedTodayChip from './EnhancedTodayChip';
|
||||
import { TodayTask } from '../../../types/workflow';
|
||||
|
||||
interface PublishPillarChipsProps {
|
||||
isHovered: boolean;
|
||||
pillarColor: string;
|
||||
}
|
||||
|
||||
const PublishPillarChips: React.FC<PublishPillarChipsProps> = ({
|
||||
isHovered,
|
||||
pillarColor
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Today's tasks for Publish pillar
|
||||
const todayTasks: TodayTask[] = [
|
||||
{
|
||||
id: "publish-facebook-post",
|
||||
pillarId: "publish",
|
||||
title: "Publish reviewed Facebook post",
|
||||
description: "Post 'ALwrity AI Content Generation' on Facebook",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 10,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/facebook-writer',
|
||||
icon: Facebook,
|
||||
color: "#1877F2",
|
||||
enabled: true,
|
||||
action: () => navigate('/facebook-writer')
|
||||
},
|
||||
{
|
||||
id: "publish-linkedin-article",
|
||||
pillarId: "publish",
|
||||
title: "Schedule LinkedIn article",
|
||||
description: "Publish 'AI Agents frameworks latest news' on LinkedIn",
|
||||
status: 'pending' as const,
|
||||
priority: 'high' as const,
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/linkedin-writer',
|
||||
icon: LinkedIn,
|
||||
color: "#0077B5",
|
||||
enabled: true,
|
||||
action: () => navigate('/linkedin-writer')
|
||||
},
|
||||
{
|
||||
id: "publish-review-content",
|
||||
pillarId: "publish",
|
||||
title: "Review pending content",
|
||||
description: "Review 3 pending blog posts for website",
|
||||
status: 'pending' as const,
|
||||
priority: 'medium' as const,
|
||||
estimatedTime: 25,
|
||||
actionType: 'navigate' as const,
|
||||
actionUrl: '/content-review',
|
||||
icon: Article,
|
||||
color: "#FF6B35",
|
||||
enabled: false,
|
||||
action: () => {}
|
||||
}
|
||||
];
|
||||
|
||||
const handleChipClick = (platform: string) => {
|
||||
switch (platform) {
|
||||
case 'facebook':
|
||||
navigate('/facebook-writer');
|
||||
break;
|
||||
case 'linkedin':
|
||||
navigate('/linkedin-writer');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, width: '100%' }}>
|
||||
{/* Today Chip - Always visible */}
|
||||
<EnhancedTodayChip
|
||||
pillarId="publish"
|
||||
pillarTitle="Publish"
|
||||
pillarColor={pillarColor}
|
||||
tasks={todayTasks}
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
{/* Progressive disclosure chips */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<>
|
||||
{/* Reviewed Chip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Chip
|
||||
icon={<CheckCircle sx={{ fontSize: 16 }} />}
|
||||
label="Reviewed"
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '2px solid #4CAF50',
|
||||
boxShadow: '0 4px 12px rgba(76, 175, 80, 0.3), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
boxShadow: '0 6px 20px rgba(76, 175, 80, 0.4), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #FF6B35 0%, #F7931E 100%)',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 8px rgba(255, 107, 53, 0.4)',
|
||||
border: '2px solid white',
|
||||
}}
|
||||
>
|
||||
3
|
||||
</Box>
|
||||
</motion.div>
|
||||
|
||||
{/* Facebook Chip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Facebook sx={{ fontSize: 16 }} />}
|
||||
label="Facebook"
|
||||
onClick={() => handleChipClick('facebook')}
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'linear-gradient(135deg, #1877F2 0%, #166FE5 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '2px solid #1877F2',
|
||||
boxShadow: '0 4px 12px rgba(24, 119, 242, 0.3), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
boxShadow: '0 6px 20px rgba(24, 119, 242, 0.4), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* LinkedIn Chip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<LinkedIn sx={{ fontSize: 16 }} />}
|
||||
label="LinkedIn"
|
||||
onClick={() => handleChipClick('linkedin')}
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'linear-gradient(135deg, #0077B5 0%, #005885 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '2px solid #0077B5',
|
||||
boxShadow: '0 4px 12px rgba(0, 119, 181, 0.3), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
boxShadow: '0 6px 20px rgba(0, 119, 181, 0.4), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Disabled Social Media Chips */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Twitter sx={{ fontSize: 16 }} />}
|
||||
label="Twitter"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'rgba(29, 161, 242, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(29, 161, 242, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.5 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Instagram sx={{ fontSize: 16 }} />}
|
||||
label="Instagram"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'rgba(225, 48, 108, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(225, 48, 108, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.6 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<YouTube sx={{ fontSize: 16 }} />}
|
||||
label="YouTube"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'rgba(255, 0, 0, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(255, 0, 0, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 }}
|
||||
>
|
||||
<Chip
|
||||
icon={<Article sx={{ fontSize: 16 }} />}
|
||||
label="Wix/WordPress"
|
||||
disabled
|
||||
sx={{
|
||||
height: 28,
|
||||
minWidth: 100,
|
||||
background: 'rgba(255, 107, 53, 0.1)',
|
||||
color: 'rgba(255, 255, 255, 0.4)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
border: '1px solid rgba(255, 107, 53, 0.2)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Ellipsis indicator when not hovered */}
|
||||
{!isHovered && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: '1.2rem',
|
||||
animation: 'pulse 2s infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': { opacity: 0.6 },
|
||||
'50%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
⋯
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishPillarChips;
|
||||
@@ -0,0 +1,354 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
ArrowBack as BackIcon,
|
||||
ArrowForward as ForwardIcon,
|
||||
PlayArrow as PlayIcon,
|
||||
Pause as PauseIcon,
|
||||
SkipNext as SkipIcon,
|
||||
CheckCircle as CompleteIcon,
|
||||
Navigation as NavigationIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useWorkflowStore } from '../../../stores/workflowStore';
|
||||
import { taskNavigationService } from '../../../services/TaskNavigationService';
|
||||
import { taskDependencyManager } from '../../../services/TaskDependencyManager';
|
||||
|
||||
interface TaskNavigationControlsProps {
|
||||
compact?: boolean;
|
||||
showTaskInfo?: boolean;
|
||||
onTaskChange?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
const TaskNavigationControls: React.FC<TaskNavigationControlsProps> = ({
|
||||
compact = false,
|
||||
showTaskInfo = true,
|
||||
onTaskChange
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
currentWorkflow,
|
||||
navigationState,
|
||||
moveToNextTask,
|
||||
moveToPreviousTask,
|
||||
completeTask,
|
||||
skipTask,
|
||||
isLoading
|
||||
} = useWorkflowStore();
|
||||
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
const [navigationError, setNavigationError] = useState<string | null>(null);
|
||||
|
||||
// Navigation event listener
|
||||
useEffect(() => {
|
||||
const handleNavigationEvent = (event: any) => {
|
||||
console.log('Navigation event:', event);
|
||||
if (onTaskChange && event.detail?.taskId) {
|
||||
onTaskChange(event.detail.taskId);
|
||||
}
|
||||
};
|
||||
|
||||
taskNavigationService.addNavigationListener(handleNavigationEvent);
|
||||
|
||||
return () => {
|
||||
taskNavigationService.removeNavigationListener(handleNavigationEvent);
|
||||
};
|
||||
}, [onTaskChange]);
|
||||
|
||||
const handleNavigateToNext = async () => {
|
||||
if (!currentWorkflow || !navigationState?.nextTask) return;
|
||||
|
||||
setIsNavigating(true);
|
||||
setNavigationError(null);
|
||||
|
||||
try {
|
||||
await moveToNextTask();
|
||||
} catch (error) {
|
||||
setNavigationError(error instanceof Error ? error.message : 'Navigation failed');
|
||||
} finally {
|
||||
setIsNavigating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateBack = async () => {
|
||||
if (!currentWorkflow || !navigationState?.canGoBack) return;
|
||||
|
||||
setIsNavigating(true);
|
||||
setNavigationError(null);
|
||||
|
||||
try {
|
||||
await moveToPreviousTask();
|
||||
} catch (error) {
|
||||
setNavigationError(error instanceof Error ? error.message : 'Back navigation failed');
|
||||
} finally {
|
||||
setIsNavigating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompleteCurrentTask = async () => {
|
||||
if (!currentWorkflow || !navigationState?.currentTask) return;
|
||||
|
||||
try {
|
||||
await completeTask(navigationState.currentTask.id);
|
||||
} catch (error) {
|
||||
setNavigationError(error instanceof Error ? error.message : 'Task completion failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipCurrentTask = async () => {
|
||||
if (!currentWorkflow || !navigationState?.currentTask) return;
|
||||
|
||||
try {
|
||||
await skipTask(navigationState.currentTask.id);
|
||||
} catch (error) {
|
||||
setNavigationError(error instanceof Error ? error.message : 'Task skip failed');
|
||||
}
|
||||
};
|
||||
|
||||
const getReadyTasks = () => {
|
||||
if (!currentWorkflow) return [];
|
||||
return taskDependencyManager.getReadyTasks(currentWorkflow);
|
||||
};
|
||||
|
||||
const getBlockedTasks = () => {
|
||||
if (!currentWorkflow) return [];
|
||||
return taskDependencyManager.getBlockedTasks(currentWorkflow);
|
||||
};
|
||||
|
||||
if (!currentWorkflow || !navigationState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTask = navigationState.currentTask;
|
||||
const readyTasks = getReadyTasks();
|
||||
const blockedTasks = getBlockedTasks();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: compact ? 2 : 3 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<NavigationIcon sx={{ color: theme.palette.primary.main }} />
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
Task Navigation
|
||||
</Typography>
|
||||
{navigationError && (
|
||||
<Chip
|
||||
label={navigationError}
|
||||
color="error"
|
||||
size="small"
|
||||
sx={{ ml: 'auto' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Current Task Info */}
|
||||
{showTaskInfo && currentTask && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 1 }}>
|
||||
Current Task:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
p: 2,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ color: 'white', fontWeight: 600, mb: 0.5 }}>
|
||||
{currentTask.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
{currentTask.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||
<Chip
|
||||
label={currentTask.pillarId}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `${currentTask.color}20`,
|
||||
color: currentTask.color,
|
||||
border: `1px solid ${currentTask.color}40`
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
label={`${currentTask.estimatedTime} min`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: 'rgba(255,255,255,0.8)'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
{/* Back Button */}
|
||||
<Tooltip title="Go to Previous Task">
|
||||
<IconButton
|
||||
onClick={handleNavigateBack}
|
||||
disabled={!navigationState.canGoBack || isLoading || isNavigating}
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
background: 'rgba(255,255,255,0.2)'
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
color: 'rgba(255,255,255,0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BackIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* Complete Task Button */}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<CompleteIcon />}
|
||||
onClick={handleCompleteCurrentTask}
|
||||
disabled={!currentTask || isLoading}
|
||||
sx={{
|
||||
background: '#4CAF50',
|
||||
'&:hover': {
|
||||
background: '#45a049'
|
||||
},
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
Complete Task
|
||||
</Button>
|
||||
|
||||
{/* Skip Task Button */}
|
||||
<Tooltip title="Skip Current Task">
|
||||
<IconButton
|
||||
onClick={handleSkipCurrentTask}
|
||||
disabled={!currentTask || isLoading}
|
||||
sx={{
|
||||
background: 'rgba(255,152,0,0.2)',
|
||||
color: '#FF9800',
|
||||
'&:hover': {
|
||||
background: 'rgba(255,152,0,0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SkipIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* Forward Button */}
|
||||
<Tooltip title="Go to Next Task">
|
||||
<IconButton
|
||||
onClick={handleNavigateToNext}
|
||||
disabled={!navigationState.canGoForward || isLoading || isNavigating}
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
background: 'rgba(255,255,255,0.2)'
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
color: 'rgba(255,255,255,0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ForwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Task Status Summary */}
|
||||
{!compact && (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`${readyTasks.length} Ready`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(76,175,80,0.2)',
|
||||
color: '#4CAF50',
|
||||
border: '1px solid rgba(76,175,80,0.3)'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
label={`${blockedTasks.length} Blocked`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(244,67,54,0.2)',
|
||||
color: '#F44336',
|
||||
border: '1px solid rgba(244,67,54,0.3)'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
label={`${currentWorkflow.completedTasks}/${currentWorkflow.totalTasks} Complete`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(33,150,243,0.2)',
|
||||
color: '#2196F3',
|
||||
border: '1px solid rgba(33,150,243,0.3)'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
<AnimatePresence>
|
||||
{(isLoading || isNavigating) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'inherit'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: 'white' }}>
|
||||
{isNavigating ? 'Navigating...' : 'Loading...'}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskNavigationControls;
|
||||
@@ -0,0 +1,496 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Chip,
|
||||
Stack,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Collapse,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PlayArrow,
|
||||
Pause,
|
||||
Stop,
|
||||
Info,
|
||||
ExpandMore,
|
||||
ExpandLess,
|
||||
CheckCircle,
|
||||
Schedule,
|
||||
TrendingUp,
|
||||
NavigateNext,
|
||||
NavigateBefore,
|
||||
SkipNext,
|
||||
TaskAlt,
|
||||
Timer,
|
||||
Assignment
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useWorkflowStore } from '../../../stores/workflowStore';
|
||||
|
||||
interface WorkflowDemoProps {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const WorkflowDemo: React.FC<WorkflowDemoProps> = ({ compact = false }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const {
|
||||
currentWorkflow,
|
||||
workflowProgress,
|
||||
navigationState,
|
||||
isLoading,
|
||||
generateDailyWorkflow,
|
||||
startWorkflow,
|
||||
completeTask,
|
||||
skipTask,
|
||||
moveToNextTask,
|
||||
moveToPreviousTask,
|
||||
isWorkflowComplete
|
||||
} = useWorkflowStore();
|
||||
|
||||
const handleGenerateWorkflow = async () => {
|
||||
try {
|
||||
await generateDailyWorkflow('demo-user');
|
||||
} catch (error) {
|
||||
console.error('Failed to generate workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartWorkflow = async () => {
|
||||
if (currentWorkflow) {
|
||||
try {
|
||||
await startWorkflow(currentWorkflow.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to start workflow:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompleteTask = async (taskId: string) => {
|
||||
try {
|
||||
await completeTask(taskId);
|
||||
} catch (error) {
|
||||
console.error('Failed to complete task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipTask = async (taskId: string) => {
|
||||
try {
|
||||
await skipTask(taskId);
|
||||
} catch (error) {
|
||||
console.error('Failed to skip task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextTask = async () => {
|
||||
try {
|
||||
await moveToNextTask();
|
||||
} catch (error) {
|
||||
console.error('Failed to move to next task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousTask = async () => {
|
||||
try {
|
||||
await moveToPreviousTask();
|
||||
} catch (error) {
|
||||
console.error('Failed to move to previous task:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const isComplete = isWorkflowComplete();
|
||||
const hasWorkflow = !!currentWorkflow;
|
||||
const isInProgress = currentWorkflow?.workflowStatus === 'in_progress';
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Card sx={{
|
||||
background: 'linear-gradient(135deg, rgba(25, 118, 210, 0.1) 0%, rgba(25, 118, 210, 0.05) 100%)',
|
||||
border: '1px solid rgba(25, 118, 210, 0.2)',
|
||||
borderRadius: 2,
|
||||
mb: 2
|
||||
}}>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Schedule color="primary" />
|
||||
<Typography variant="h6" color="primary">
|
||||
Today's Workflow
|
||||
</Typography>
|
||||
{hasWorkflow && (
|
||||
<Chip
|
||||
label={isComplete ? 'Complete' : isInProgress ? 'In Progress' : 'Ready'}
|
||||
color={isComplete ? 'success' : isInProgress ? 'primary' : 'default'}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{!hasWorkflow && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<PlayArrow />}
|
||||
onClick={handleGenerateWorkflow}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
)}
|
||||
{hasWorkflow && !isInProgress && !isComplete && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<PlayArrow />}
|
||||
onClick={handleStartWorkflow}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{workflowProgress && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Progress: {workflowProgress.completedTasks} of {workflowProgress.totalTasks} tasks
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{currentWorkflow && (
|
||||
<Stack spacing={1}>
|
||||
{currentWorkflow.tasks.slice(0, 3).map((task) => (
|
||||
<Box
|
||||
key={task.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: 1,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 1,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{task.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{task.estimatedTime} min • {task.priority}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
{task.status === 'pending' && isInProgress && (
|
||||
<>
|
||||
<Tooltip title="Complete Task">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleCompleteTask(task.id)}
|
||||
sx={{ color: 'success.main' }}
|
||||
>
|
||||
<CheckCircle fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Skip Task">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleSkipTask(task.id)}
|
||||
sx={{ color: 'warning.main' }}
|
||||
>
|
||||
<Stop fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{task.status === 'completed' && (
|
||||
<CheckCircle color="success" fontSize="small" />
|
||||
)}
|
||||
{task.status === 'skipped' && (
|
||||
<Stop color="warning" fontSize="small" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (isComplete) return 'success';
|
||||
if (isInProgress) return 'primary';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
if (isComplete) return 'Workflow Complete! 🎉';
|
||||
if (isInProgress) return 'In Progress';
|
||||
if (!hasWorkflow) return 'No Workflow Generated';
|
||||
return 'Ready to Start';
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: 3,
|
||||
p: 3,
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
mb: 3,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{/* Header Section */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{isComplete ? <CheckCircle sx={{ color: 'success.main' }} /> :
|
||||
isInProgress ? <TrendingUp sx={{ color: 'primary.main' }} /> :
|
||||
<Schedule sx={{ color: 'grey.400' }} />}
|
||||
Today's Marketing Workflow
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={getStatusText()}
|
||||
size="small"
|
||||
color={getStatusColor()}
|
||||
sx={{
|
||||
background: `${getStatusColor() === 'success' ? 'success.main' : getStatusColor() === 'primary' ? 'primary.main' : 'grey.500'}20`,
|
||||
color: getStatusColor() === 'success' ? 'success.main' : getStatusColor() === 'primary' ? 'primary.main' : 'grey.500',
|
||||
border: `1px solid ${getStatusColor() === 'success' ? 'success.main' : getStatusColor() === 'primary' ? 'primary.main' : 'grey.500'}40`,
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
{/* Current Task Navigation */}
|
||||
{navigationState?.currentTask && isInProgress && (
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
mb: 3,
|
||||
background: 'rgba(76, 175, 80, 0.1)',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||
borderRadius: 2,
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<TaskAlt color="success" />
|
||||
<Typography variant="h6" color="success.main" sx={{ fontWeight: 600 }}>
|
||||
Current Task
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" fontWeight="medium" sx={{ mb: 1, color: 'white' }}>
|
||||
{navigationState.currentTask.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2, color: 'rgba(255,255,255,0.7)' }}>
|
||||
{navigationState.currentTask.description}
|
||||
</Typography>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<CheckCircle />}
|
||||
onClick={() => navigationState.currentTask && handleCompleteTask(navigationState.currentTask.id)}
|
||||
sx={{ background: 'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)' }}
|
||||
>
|
||||
Complete
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<SkipNext />}
|
||||
onClick={() => navigationState.currentTask && handleSkipTask(navigationState.currentTask.id)}
|
||||
sx={{ borderColor: 'warning.main', color: 'warning.main' }}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<NavigateNext />}
|
||||
onClick={handleNextTask}
|
||||
disabled={!navigationState.canGoForward}
|
||||
sx={{ borderColor: 'primary.main', color: 'primary.main' }}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<NavigateBefore />}
|
||||
onClick={handlePreviousTask}
|
||||
disabled={!navigationState.canGoBack}
|
||||
sx={{ borderColor: 'primary.main', color: 'primary.main' }}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Task List */}
|
||||
{currentWorkflow && (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'white', fontWeight: 600 }}>
|
||||
<Assignment color="primary" />
|
||||
Today's Tasks
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<AnimatePresence>
|
||||
{currentWorkflow.tasks.map((task, index) => (
|
||||
<Grid item xs={12} md={6} key={task.id}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
background: task.id === navigationState?.currentTask?.id
|
||||
? 'rgba(76, 175, 80, 0.1)'
|
||||
: 'rgba(255, 255, 255, 0.05)',
|
||||
border: task.id === navigationState?.currentTask?.id
|
||||
? '2px solid rgba(76, 175, 80, 0.5)'
|
||||
: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 2,
|
||||
height: '100%',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="subtitle1" fontWeight="medium" sx={{ flexGrow: 1, color: 'white' }}>
|
||||
{task.title}
|
||||
</Typography>
|
||||
{task.id === navigationState?.currentTask?.id && (
|
||||
<Chip label="Current" color="success" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ mb: 2, color: 'rgba(255,255,255,0.7)' }}>
|
||||
{task.description}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`${task.estimatedTime} min`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<Timer />}
|
||||
/>
|
||||
<Chip
|
||||
label={task.priority}
|
||||
size="small"
|
||||
color={task.priority === 'high' ? 'error' : task.priority === 'medium' ? 'warning' : 'default'}
|
||||
/>
|
||||
<Chip
|
||||
label={task.status}
|
||||
size="small"
|
||||
color={task.status === 'completed' ? 'success' : task.status === 'skipped' ? 'warning' : 'default'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
{task.status === 'pending' && isInProgress && (
|
||||
<>
|
||||
<Tooltip title="Complete Task">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleCompleteTask(task.id)}
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
'&:hover': { background: 'rgba(76, 175, 80, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<CheckCircle />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Skip Task">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleSkipTask(task.id)}
|
||||
sx={{
|
||||
color: 'warning.main',
|
||||
'&:hover': { background: 'rgba(255, 152, 0, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<Stop />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{task.status === 'completed' && (
|
||||
<CheckCircle color="success" />
|
||||
)}
|
||||
{task.status === 'skipped' && (
|
||||
<Stop color="warning" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Help Section */}
|
||||
{!hasWorkflow && (
|
||||
<Alert severity="info" sx={{ mt: 3, background: 'rgba(33, 150, 243, 0.1)', border: '1px solid rgba(33, 150, 243, 0.3)' }}>
|
||||
<AlertTitle>Getting Started</AlertTitle>
|
||||
Generate today's workflow to see your personalized marketing tasks. The system will guide you through each task with clear instructions and navigation controls.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDemo;
|
||||
@@ -0,0 +1,251 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
PlayArrow,
|
||||
Pause,
|
||||
CheckCircle,
|
||||
Schedule,
|
||||
TrendingUp
|
||||
} from '@mui/icons-material';
|
||||
import { useWorkflowStore } from '../../../stores/workflowStore';
|
||||
|
||||
interface WorkflowProgressBarProps {
|
||||
onStartWorkflow?: () => void;
|
||||
onPauseWorkflow?: () => void;
|
||||
onResumeWorkflow?: () => void;
|
||||
showControls?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const WorkflowProgressBar: React.FC<WorkflowProgressBarProps> = ({
|
||||
onStartWorkflow,
|
||||
onPauseWorkflow,
|
||||
onResumeWorkflow,
|
||||
showControls = true,
|
||||
compact = false
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
currentWorkflow,
|
||||
workflowProgress,
|
||||
navigationState,
|
||||
isLoading,
|
||||
startWorkflow,
|
||||
isWorkflowComplete,
|
||||
getCompletionPercentage,
|
||||
generateDailyWorkflow
|
||||
} = useWorkflowStore();
|
||||
|
||||
const completionPercentage = getCompletionPercentage();
|
||||
const isComplete = isWorkflowComplete();
|
||||
const currentTask = navigationState?.currentTask;
|
||||
|
||||
// Always show the progress bar, even if no workflow exists yet
|
||||
|
||||
const handleStartWorkflow = async () => {
|
||||
try {
|
||||
if (currentWorkflow) {
|
||||
await startWorkflow(currentWorkflow.id);
|
||||
onStartWorkflow?.();
|
||||
} else {
|
||||
// Generate a new workflow if none exists
|
||||
await generateDailyWorkflow('demo-user');
|
||||
onStartWorkflow?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (isComplete) return theme.palette.success.main;
|
||||
if (currentWorkflow?.workflowStatus === 'in_progress') return theme.palette.primary.main;
|
||||
return theme.palette.grey[500];
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
if (isComplete) return 'Workflow Complete! 🎉';
|
||||
if (currentWorkflow?.workflowStatus === 'in_progress') return 'In Progress';
|
||||
if (!currentWorkflow) return 'No Workflow Generated';
|
||||
return 'Ready to Start';
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: 2,
|
||||
p: compact ? 2 : 3,
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
mb: 3
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography
|
||||
variant={compact ? "h6" : "h5"}
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{isComplete ? <CheckCircle sx={{ color: theme.palette.success.main }} /> :
|
||||
currentWorkflow?.workflowStatus === 'in_progress' ? <TrendingUp sx={{ color: theme.palette.primary.main }} /> :
|
||||
<Schedule sx={{ color: theme.palette.grey[400] }} />}
|
||||
Today's Marketing Workflow
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={getStatusText()}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `${getStatusColor()}20`,
|
||||
color: getStatusColor(),
|
||||
border: `1px solid ${getStatusColor()}40`,
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Controls */}
|
||||
{showControls && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{(currentWorkflow?.workflowStatus === 'not_started' || !currentWorkflow) && (
|
||||
<Tooltip title={currentWorkflow ? "Start Today's Workflow" : "Generate & Start Workflow"}>
|
||||
<IconButton
|
||||
onClick={handleStartWorkflow}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
background: theme.palette.primary.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
background: theme.palette.primary.dark,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlayArrow />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{currentWorkflow?.workflowStatus === 'in_progress' && (
|
||||
<Tooltip title="Pause Workflow">
|
||||
<IconButton
|
||||
onClick={onPauseWorkflow}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
background: theme.palette.warning.main,
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
background: theme.palette.warning.dark,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Pause />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
Progress
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
{workflowProgress?.completedTasks || 0} of {workflowProgress?.totalTasks || 0} tasks
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentWorkflow ? completionPercentage : 0}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: isComplete
|
||||
? `linear-gradient(90deg, ${theme.palette.success.main} 0%, ${theme.palette.success.light} 100%)`
|
||||
: `linear-gradient(90deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.light} 100%)`,
|
||||
borderRadius: 4,
|
||||
boxShadow: `0 0 10px ${isComplete ? theme.palette.success.main : theme.palette.primary.main}40`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
mt: 0.5,
|
||||
display: 'block',
|
||||
textAlign: 'right'
|
||||
}}
|
||||
>
|
||||
{currentWorkflow ? `${completionPercentage}% complete` : 'No workflow active'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Current Task Info */}
|
||||
{currentTask && !isComplete && (
|
||||
<Box
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
p: 2,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
|
||||
Current Task:
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
{currentTask.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
{currentTask.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Time Information */}
|
||||
{workflowProgress && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
Time Spent: {workflowProgress.actualTimeSpent} min
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
Est. Remaining: {workflowProgress.estimatedTimeRemaining} min
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowProgressBar;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Chip } from '@mui/material';
|
||||
import { Box, Typography, Chip, Button, CircularProgress } from '@mui/material';
|
||||
import { PlayArrow, Pause, Stop } from '@mui/icons-material';
|
||||
import { ShimmerHeader } from './styled';
|
||||
import { DashboardHeaderProps } from './types';
|
||||
|
||||
@@ -7,32 +8,226 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
statusChips = [],
|
||||
rightContent
|
||||
rightContent,
|
||||
customIcon,
|
||||
workflowControls
|
||||
}) => {
|
||||
return (
|
||||
<ShimmerHeader sx={{ mb: 5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="h2" component="h1" sx={{
|
||||
fontWeight: 800,
|
||||
color: 'white',
|
||||
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
mb: 1,
|
||||
fontSize: { xs: '2rem', md: '3rem' },
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontWeight: 400,
|
||||
fontSize: { xs: '1rem', md: '1.25rem' },
|
||||
}}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
<ShimmerHeader sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{customIcon && (
|
||||
<Box
|
||||
component="img"
|
||||
src={customIcon}
|
||||
alt="Alwrity Logo"
|
||||
sx={{
|
||||
width: { xs: 40, md: 48 },
|
||||
height: { xs: 40, md: 48 },
|
||||
filter: 'drop-shadow(0 4px 8px rgba(0,0,0,0.3))',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h2" component="h1" sx={{
|
||||
fontWeight: 800,
|
||||
color: 'white',
|
||||
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
mb: subtitle ? 1 : 0,
|
||||
fontSize: { xs: '2rem', md: '3rem' },
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="h5" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontWeight: 400,
|
||||
fontSize: { xs: '1rem', md: '1.25rem' },
|
||||
}}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Workflow Controls */}
|
||||
{workflowControls && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{/* Workflow Control Buttons */}
|
||||
{!workflowControls.isWorkflowActive ? (
|
||||
/* Start Button with Badge and Lightning Glow */
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<PlayArrow />}
|
||||
onClick={workflowControls.onStartWorkflow}
|
||||
disabled={workflowControls.isLoading}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)',
|
||||
border: '2px solid transparent',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #388e3c 0%, #2e7d32 100%)',
|
||||
},
|
||||
minWidth: 'auto',
|
||||
px: 2,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
|
||||
animation: 'shimmer 2.5s infinite',
|
||||
zIndex: 1,
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
left: -2,
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
background: 'linear-gradient(45deg, #4caf50, #8bc34a, #4caf50, #8bc34a)',
|
||||
backgroundSize: '400% 400%',
|
||||
borderRadius: 'inherit',
|
||||
zIndex: -1,
|
||||
animation: 'borderGlow 3s ease-in-out infinite',
|
||||
},
|
||||
'@keyframes shimmer': {
|
||||
'0%': { left: '-100%' },
|
||||
'100%': { left: '100%' },
|
||||
},
|
||||
'@keyframes borderGlow': {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'50%': { backgroundPosition: '100% 50%' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
borderRadius: '12px',
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.3)'
|
||||
}}
|
||||
>
|
||||
{`${workflowControls.completedTasks}/${workflowControls.totalTasks}`}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
/* In-Progress/Completed Controls with Enhanced Styling */
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{/* In-Progress/Completed Status with Lightning Glow */}
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={workflowControls.onResumePlanModal}
|
||||
disabled={workflowControls.isLoading}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? 'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)'
|
||||
: 'linear-gradient(135deg, #2196f3 0%, #1976d2 100%)',
|
||||
color: 'white',
|
||||
minWidth: 'auto',
|
||||
px: 2,
|
||||
border: '2px solid transparent',
|
||||
boxShadow: workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? '0 8px 25px rgba(76, 175, 80, 0.4), 0 0 0 1px rgba(255,255,255,0.2)'
|
||||
: '0 8px 25px rgba(33, 150, 243, 0.4), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
'&:hover': {
|
||||
background: workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? 'linear-gradient(135deg, #388e3c 0%, #2e7d32 100%)'
|
||||
: 'linear-gradient(135deg, #1976d2 0%, #1565c0 100%)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? '0 12px 35px rgba(76, 175, 80, 0.6), 0 0 0 1px rgba(255,255,255,0.3)'
|
||||
: '0 12px 35px rgba(33, 150, 243, 0.6), 0 0 0 1px rgba(255,255,255,0.3)',
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
|
||||
animation: 'shimmer 2.5s infinite',
|
||||
zIndex: 1,
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
left: -2,
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
background: workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? 'linear-gradient(45deg, #4caf50, #8bc34a, #4caf50, #8bc34a)'
|
||||
: 'linear-gradient(45deg, #2196f3, #64b5f6, #2196f3, #64b5f6)',
|
||||
backgroundSize: '400% 400%',
|
||||
borderRadius: 'inherit',
|
||||
zIndex: -1,
|
||||
animation: 'borderGlow 3s ease-in-out infinite',
|
||||
},
|
||||
'@keyframes shimmer': {
|
||||
'0%': { left: '-100%' },
|
||||
'100%': { left: '100%' },
|
||||
},
|
||||
'@keyframes borderGlow': {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'50%': { backgroundPosition: '100% 50%' },
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
title={workflowControls.completedTasks === workflowControls.totalTasks
|
||||
? '🎉 All tasks completed! Click to review workflow progress.'
|
||||
: 'Workflow in progress. Click to resume or check current tasks.'}
|
||||
>
|
||||
{workflowControls.completedTasks === workflowControls.totalTasks ? 'Completed' : 'In Progress'}
|
||||
</Button>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
borderRadius: '12px',
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.3)'
|
||||
}}
|
||||
>
|
||||
{`${workflowControls.completedTasks}/${workflowControls.totalTasks}`}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
{statusChips.length > 0 && (
|
||||
|
||||
@@ -26,6 +26,15 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
toolCategories,
|
||||
theme
|
||||
}) => {
|
||||
// Helper function to get tool count from a category
|
||||
const getToolCount = (category: any): number => {
|
||||
if ('tools' in category) {
|
||||
return category.tools.length;
|
||||
} else if ('subCategories' in category) {
|
||||
return Object.values(category.subCategories).reduce((total: number, subCat: any) => total + subCat.tools.length, 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return (
|
||||
<SearchContainer>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
|
||||
@@ -74,13 +83,14 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Enhanced Category Filter */}
|
||||
{/* Enhanced Category Filter with Tool Count Badges */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<CategoryChip
|
||||
label="All Tools"
|
||||
onClick={() => onCategoryChange(null)}
|
||||
active={selectedCategory === null}
|
||||
theme={theme}
|
||||
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
|
||||
/>
|
||||
{Object.keys(toolCategories).map((category) => (
|
||||
<CategoryChip
|
||||
@@ -89,6 +99,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
onClick={() => onCategoryChange(category)}
|
||||
active={selectedCategory === category}
|
||||
theme={theme}
|
||||
toolCount={getToolCount(toolCategories[category])}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -96,15 +96,24 @@ export const SearchContainer = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export const CategoryChip = styled(Chip, {
|
||||
shouldForwardProp: (prop) => prop !== 'active',
|
||||
})<{ active?: boolean }>(({ theme, active }) => ({
|
||||
background: active ? 'rgba(255, 255, 255, 0.25)' : 'rgba(255, 255, 255, 0.1)',
|
||||
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'toolCount',
|
||||
})<{ active?: boolean; toolCount?: number }>(({ theme, active, toolCount }) => ({
|
||||
background: active
|
||||
? 'linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.2) 100%)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontWeight: active ? 700 : 600,
|
||||
fontSize: '0.9rem',
|
||||
padding: theme.spacing(1, 2),
|
||||
border: `1px solid ${active ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 0.2)'}`,
|
||||
border: active
|
||||
? '2px solid rgba(255, 255, 255, 0.6)'
|
||||
: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
boxShadow: active
|
||||
? '0 6px 20px rgba(255, 255, 255, 0.2), 0 0 0 1px rgba(255,255,255,0.1)'
|
||||
: 'none',
|
||||
transform: active ? 'translateY(-2px) scale(1.05)' : 'none',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.25)',
|
||||
transform: 'translateY(-2px)',
|
||||
@@ -112,7 +121,29 @@ export const CategoryChip = styled(Chip, {
|
||||
},
|
||||
'& .MuiChip-label': {
|
||||
padding: theme.spacing(0.5, 1),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
// Tool count badge
|
||||
...(toolCount && {
|
||||
'&::after': {
|
||||
content: `"${toolCount}"`,
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
backgroundColor: active ? '#4caf50' : 'rgba(255, 255, 255, 0.8)',
|
||||
color: active ? 'white' : 'rgba(0, 0, 0, 0.8)',
|
||||
borderRadius: '10px',
|
||||
padding: '2px 6px',
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
minWidth: '18px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
|
||||
border: active ? '1px solid rgba(255,255,255,0.3)' : '1px solid rgba(0,0,0,0.1)',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
export const EnhancedGlassCard = styled(GlassCard)(({ theme }) => ({
|
||||
|
||||
@@ -77,13 +77,24 @@ export interface SearchFilterProps {
|
||||
|
||||
export interface DashboardHeaderProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
subtitle?: string;
|
||||
statusChips?: Array<{
|
||||
label: string;
|
||||
color: string;
|
||||
icon: React.ReactElement;
|
||||
}>;
|
||||
rightContent?: React.ReactNode;
|
||||
customIcon?: string;
|
||||
workflowControls?: {
|
||||
onStartWorkflow: () => void;
|
||||
onPauseWorkflow?: () => void;
|
||||
onStopWorkflow?: () => void;
|
||||
onResumePlanModal?: () => void;
|
||||
isWorkflowActive: boolean;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoadingSkeletonProps {
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import {
|
||||
Article as ArticleIcon,
|
||||
Search as SearchIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Campaign as CampaignIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Psychology as PsychologyIcon,
|
||||
|
||||
426
frontend/src/services/TaskCompletionVerifier.ts
Normal file
426
frontend/src/services/TaskCompletionVerifier.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
TodayTask
|
||||
} from '../types/workflow';
|
||||
|
||||
interface VerificationResult {
|
||||
isCompleted: boolean;
|
||||
confidence: number; // 0-1 scale
|
||||
evidence: string[];
|
||||
warnings: string[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
interface VerificationRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
pillarId: string;
|
||||
actionType: string;
|
||||
verifier: (task: TodayTask, context?: any) => Promise<VerificationResult>;
|
||||
weight: number; // Importance weight for confidence calculation
|
||||
}
|
||||
|
||||
interface VerificationContext {
|
||||
userId: string;
|
||||
timestamp: Date;
|
||||
platformData?: Record<string, any>;
|
||||
userActivity?: Record<string, any>;
|
||||
}
|
||||
|
||||
class TaskCompletionVerifier {
|
||||
private verificationRules: Map<string, VerificationRule> = new Map();
|
||||
private verificationHistory: Map<string, VerificationResult[]> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.initializeDefaultRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a task has been completed
|
||||
*/
|
||||
async verifyTaskCompletion(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
try {
|
||||
const rule = this.verificationRules.get(`${task.pillarId}-${task.actionType}`);
|
||||
|
||||
if (!rule) {
|
||||
// Fallback to basic verification
|
||||
return this.basicVerification(task, context);
|
||||
}
|
||||
|
||||
const result = await rule.verifier(task, context);
|
||||
|
||||
// Store verification history
|
||||
this.storeVerificationResult(task.id, result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Verification failed for task ${task.id}:`, error);
|
||||
|
||||
return {
|
||||
isCompleted: false,
|
||||
confidence: 0,
|
||||
evidence: [],
|
||||
warnings: [`Verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
suggestions: ['Try completing the task again or contact support']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify multiple tasks at once
|
||||
*/
|
||||
async verifyMultipleTasks(
|
||||
tasks: TodayTask[],
|
||||
context?: VerificationContext
|
||||
): Promise<Map<string, VerificationResult>> {
|
||||
const results = new Map<string, VerificationResult>();
|
||||
|
||||
// Verify tasks in parallel for better performance
|
||||
const verificationPromises = tasks.map(async (task) => {
|
||||
const result = await this.verifyTaskCompletion(task, context);
|
||||
results.set(task.id, result);
|
||||
});
|
||||
|
||||
await Promise.all(verificationPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verification history for a task
|
||||
*/
|
||||
getVerificationHistory(taskId: string): VerificationResult[] {
|
||||
return this.verificationHistory.get(taskId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom verification rule
|
||||
*/
|
||||
addVerificationRule(rule: VerificationRule): void {
|
||||
const key = `${rule.pillarId}-${rule.actionType}`;
|
||||
this.verificationRules.set(key, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove verification rule
|
||||
*/
|
||||
removeVerificationRule(pillarId: string, actionType: string): void {
|
||||
const key = `${pillarId}-${actionType}`;
|
||||
this.verificationRules.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all verification rules
|
||||
*/
|
||||
getVerificationRules(): VerificationRule[] {
|
||||
return Array.from(this.verificationRules.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default verification rules
|
||||
*/
|
||||
private initializeDefaultRules(): void {
|
||||
// Plan pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'plan-navigate',
|
||||
name: 'Content Planning Navigation',
|
||||
description: 'Verify user navigated to content planning dashboard',
|
||||
pillarId: 'plan',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'generate-navigate',
|
||||
name: 'Content Generation Navigation',
|
||||
description: 'Verify user navigated to content generation tools',
|
||||
pillarId: 'generate',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Publish pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'publish-navigate',
|
||||
name: 'Content Publishing Navigation',
|
||||
description: 'Verify user navigated to publishing tools',
|
||||
pillarId: 'publish',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'analyze-navigate',
|
||||
name: 'Analytics Navigation',
|
||||
description: 'Verify user navigated to analytics dashboard',
|
||||
pillarId: 'analyze',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Engage pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'engage-navigate',
|
||||
name: 'Engagement Navigation',
|
||||
description: 'Verify user navigated to engagement tools',
|
||||
pillarId: 'engage',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
|
||||
// Remarket pillar rules
|
||||
this.addVerificationRule({
|
||||
id: 'remarket-navigate',
|
||||
name: 'Remarketing Navigation',
|
||||
description: 'Verify user navigated to remarketing tools',
|
||||
pillarId: 'remarket',
|
||||
actionType: 'navigate',
|
||||
weight: 0.8,
|
||||
verifier: async (task, context) => {
|
||||
return this.verifyNavigation(task, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify navigation-based tasks
|
||||
*/
|
||||
private async verifyNavigation(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
const evidence: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let confidence = 0;
|
||||
|
||||
try {
|
||||
// Check if user is currently on the target page
|
||||
if (typeof window !== 'undefined' && task.actionUrl) {
|
||||
const currentPath = window.location.pathname;
|
||||
const targetPath = task.actionUrl;
|
||||
|
||||
if (currentPath === targetPath) {
|
||||
evidence.push(`User is currently on target page: ${targetPath}`);
|
||||
confidence += 0.4;
|
||||
} else {
|
||||
warnings.push(`User is not on target page. Current: ${currentPath}, Expected: ${targetPath}`);
|
||||
suggestions.push('Navigate to the correct page to complete this task');
|
||||
}
|
||||
}
|
||||
|
||||
// Check user activity (if available)
|
||||
if (context?.userActivity) {
|
||||
const activity = context.userActivity;
|
||||
const taskStartTime = task.startedAt?.getTime() || 0;
|
||||
const recentActivity = Object.entries(activity)
|
||||
.filter(([_, timestamp]) => typeof timestamp === 'number' && timestamp > taskStartTime);
|
||||
|
||||
if (recentActivity.length > 0) {
|
||||
evidence.push(`User activity detected after task start: ${recentActivity.length} actions`);
|
||||
confidence += 0.3;
|
||||
} else {
|
||||
warnings.push('No user activity detected after task start');
|
||||
}
|
||||
}
|
||||
|
||||
// Check platform data (if available)
|
||||
if (context?.platformData) {
|
||||
const platformData = context.platformData;
|
||||
if (platformData.lastActivity && platformData.lastActivity > (task.startedAt?.getTime() || 0)) {
|
||||
evidence.push('Platform activity detected after task start');
|
||||
confidence += 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Time-based verification
|
||||
if (task.startedAt && task.completedAt) {
|
||||
const timeSpent = task.completedAt.getTime() - task.startedAt.getTime();
|
||||
const estimatedTime = task.estimatedTime * 60 * 1000; // Convert to milliseconds
|
||||
|
||||
if (timeSpent >= estimatedTime * 0.5) { // At least 50% of estimated time
|
||||
evidence.push(`Task took ${Math.round(timeSpent / 60000)} minutes (estimated: ${task.estimatedTime} minutes)`);
|
||||
confidence += 0.2;
|
||||
} else {
|
||||
warnings.push(`Task completed too quickly (${Math.round(timeSpent / 60000)} minutes vs ${task.estimatedTime} estimated)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cap confidence at 1.0
|
||||
confidence = Math.min(confidence, 1.0);
|
||||
|
||||
return {
|
||||
isCompleted: confidence >= 0.6, // Threshold for completion
|
||||
confidence,
|
||||
evidence,
|
||||
warnings,
|
||||
suggestions
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
isCompleted: false,
|
||||
confidence: 0,
|
||||
evidence: [],
|
||||
warnings: [`Navigation verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
suggestions: ['Try navigating to the target page again']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic verification fallback
|
||||
*/
|
||||
private async basicVerification(
|
||||
task: TodayTask,
|
||||
context?: VerificationContext
|
||||
): Promise<VerificationResult> {
|
||||
const evidence: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let confidence = 0;
|
||||
|
||||
// Check if task has completion timestamp
|
||||
if (task.completedAt) {
|
||||
evidence.push('Task has completion timestamp');
|
||||
confidence += 0.5;
|
||||
} else {
|
||||
warnings.push('No completion timestamp found');
|
||||
}
|
||||
|
||||
// Check if task was started
|
||||
if (task.startedAt) {
|
||||
evidence.push('Task was started');
|
||||
confidence += 0.3;
|
||||
} else {
|
||||
warnings.push('No start timestamp found');
|
||||
}
|
||||
|
||||
// Check time spent
|
||||
if (task.startedAt && task.completedAt) {
|
||||
const timeSpent = task.completedAt.getTime() - task.startedAt.getTime();
|
||||
if (timeSpent > 0) {
|
||||
evidence.push(`Task took ${Math.round(timeSpent / 60000)} minutes`);
|
||||
confidence += 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isCompleted: confidence >= 0.5,
|
||||
confidence,
|
||||
evidence,
|
||||
warnings,
|
||||
suggestions: suggestions.length > 0 ? suggestions : ['Complete the task to verify completion']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store verification result in history
|
||||
*/
|
||||
private storeVerificationResult(taskId: string, result: VerificationResult): void {
|
||||
const history = this.verificationHistory.get(taskId) || [];
|
||||
history.push(result);
|
||||
|
||||
// Keep only last 10 verification results
|
||||
if (history.length > 10) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
this.verificationHistory.set(taskId, history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verification statistics
|
||||
*/
|
||||
getVerificationStats(): {
|
||||
totalVerifications: number;
|
||||
averageConfidence: number;
|
||||
completionRate: number;
|
||||
mostCommonWarnings: string[];
|
||||
} {
|
||||
const allResults = Array.from(this.verificationHistory.values()).flat();
|
||||
|
||||
if (allResults.length === 0) {
|
||||
return {
|
||||
totalVerifications: 0,
|
||||
averageConfidence: 0,
|
||||
completionRate: 0,
|
||||
mostCommonWarnings: []
|
||||
};
|
||||
}
|
||||
|
||||
const totalVerifications = allResults.length;
|
||||
const averageConfidence = allResults.reduce((sum, result) => sum + result.confidence, 0) / totalVerifications;
|
||||
const completionRate = allResults.filter(result => result.isCompleted).length / totalVerifications;
|
||||
|
||||
// Count warning frequency
|
||||
const warningCounts = new Map<string, number>();
|
||||
allResults.forEach(result => {
|
||||
result.warnings.forEach(warning => {
|
||||
warningCounts.set(warning, (warningCounts.get(warning) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const mostCommonWarnings = Array.from(warningCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([warning]) => warning);
|
||||
|
||||
return {
|
||||
totalVerifications,
|
||||
averageConfidence,
|
||||
completionRate,
|
||||
mostCommonWarnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear verification history
|
||||
*/
|
||||
clearVerificationHistory(): void {
|
||||
this.verificationHistory.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export verification data
|
||||
*/
|
||||
exportVerificationData(): {
|
||||
rules: VerificationRule[];
|
||||
history: Record<string, VerificationResult[]>;
|
||||
stats: {
|
||||
totalVerifications: number;
|
||||
averageConfidence: number;
|
||||
completionRate: number;
|
||||
mostCommonWarnings: string[];
|
||||
};
|
||||
} {
|
||||
return {
|
||||
rules: this.getVerificationRules(),
|
||||
history: Object.fromEntries(this.verificationHistory),
|
||||
stats: this.getVerificationStats()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskCompletionVerifier = new TaskCompletionVerifier();
|
||||
export default TaskCompletionVerifier;
|
||||
433
frontend/src/services/TaskDependencyManager.ts
Normal file
433
frontend/src/services/TaskDependencyManager.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
|
||||
interface DependencyGraph {
|
||||
[taskId: string]: {
|
||||
dependencies: string[];
|
||||
dependents: string[];
|
||||
status: 'ready' | 'blocked' | 'completed' | 'skipped';
|
||||
};
|
||||
}
|
||||
|
||||
interface DependencyValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
readyTasks: string[];
|
||||
blockedTasks: string[];
|
||||
}
|
||||
|
||||
class TaskDependencyManager {
|
||||
private dependencyGraph: DependencyGraph = {};
|
||||
|
||||
/**
|
||||
* Build dependency graph from workflow tasks
|
||||
*/
|
||||
buildDependencyGraph(workflow: DailyWorkflow): DependencyGraph {
|
||||
this.dependencyGraph = {};
|
||||
|
||||
// Initialize all tasks in the graph
|
||||
workflow.tasks.forEach(task => {
|
||||
this.dependencyGraph[task.id] = {
|
||||
dependencies: task.dependencies || [],
|
||||
dependents: [],
|
||||
status: this.getTaskStatus(task, workflow)
|
||||
};
|
||||
});
|
||||
|
||||
// Build dependent relationships
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
if (this.dependencyGraph[depId]) {
|
||||
this.dependencyGraph[depId].dependents.push(taskId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return this.dependencyGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate dependency graph for issues
|
||||
*/
|
||||
validateDependencyGraph(workflow: DailyWorkflow): DependencyValidationResult {
|
||||
const result: DependencyValidationResult = {
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
readyTasks: [],
|
||||
blockedTasks: []
|
||||
};
|
||||
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
// Check for circular dependencies
|
||||
const circularDeps = this.detectCircularDependencies();
|
||||
if (circularDeps.length > 0) {
|
||||
result.isValid = false;
|
||||
result.errors.push(`Circular dependencies detected: ${circularDeps.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for missing dependencies
|
||||
const missingDeps = this.detectMissingDependencies(workflow);
|
||||
if (missingDeps.length > 0) {
|
||||
result.isValid = false;
|
||||
result.errors.push(`Missing dependencies: ${missingDeps.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for orphaned tasks (tasks with no dependencies or dependents)
|
||||
const orphanedTasks = this.detectOrphanedTasks();
|
||||
if (orphanedTasks.length > 0) {
|
||||
result.warnings.push(`Orphaned tasks detected: ${orphanedTasks.join(', ')}`);
|
||||
}
|
||||
|
||||
// Categorize tasks by readiness
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task.status === 'ready') {
|
||||
result.readyTasks.push(taskId);
|
||||
} else if (task.status === 'blocked') {
|
||||
result.blockedTasks.push(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that are ready to be executed
|
||||
*/
|
||||
getReadyTasks(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
return workflow.tasks.filter(task => {
|
||||
const graphTask = this.dependencyGraph[task.id];
|
||||
return graphTask && graphTask.status === 'ready' && task.status === 'pending';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that are blocked by dependencies
|
||||
*/
|
||||
getBlockedTasks(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
return workflow.tasks.filter(task => {
|
||||
const graphTask = this.dependencyGraph[task.id];
|
||||
return graphTask && graphTask.status === 'blocked';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that depend on a specific task
|
||||
*/
|
||||
getDependentTasks(taskId: string): string[] {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
return task ? [...task.dependents] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks that a specific task depends on
|
||||
*/
|
||||
getDependencyTasks(taskId: string): string[] {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
return task ? [...task.dependencies] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task can be executed (all dependencies satisfied)
|
||||
*/
|
||||
canExecuteTask(taskId: string, workflow: DailyWorkflow): boolean {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return task.status === 'ready';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the optimal execution order for tasks
|
||||
*/
|
||||
getOptimalExecutionOrder(workflow: DailyWorkflow): TodayTask[] {
|
||||
this.buildDependencyGraph(workflow);
|
||||
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const executionOrder: TodayTask[] = [];
|
||||
|
||||
const visit = (taskId: string) => {
|
||||
if (visiting.has(taskId)) {
|
||||
throw new WorkflowError({
|
||||
code: 'CIRCULAR_DEPENDENCY',
|
||||
message: `Circular dependency detected involving task ${taskId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
if (visited.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(taskId);
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task) {
|
||||
// Visit dependencies first
|
||||
task.dependencies.forEach(depId => {
|
||||
visit(depId);
|
||||
});
|
||||
}
|
||||
|
||||
visiting.delete(taskId);
|
||||
visited.add(taskId);
|
||||
|
||||
// Add task to execution order
|
||||
const workflowTask = workflow.tasks.find(t => t.id === taskId);
|
||||
if (workflowTask) {
|
||||
executionOrder.push(workflowTask);
|
||||
}
|
||||
};
|
||||
|
||||
// Visit all tasks
|
||||
workflow.tasks.forEach(task => {
|
||||
if (!visited.has(task.id)) {
|
||||
visit(task.id);
|
||||
}
|
||||
});
|
||||
|
||||
return executionOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status in dependency graph
|
||||
*/
|
||||
updateTaskStatus(taskId: string, status: 'completed' | 'skipped' | 'in_progress'): void {
|
||||
if (this.dependencyGraph[taskId]) {
|
||||
// Update status of the task
|
||||
this.dependencyGraph[taskId].status = status === 'in_progress' ? 'ready' : status;
|
||||
|
||||
// Update status of dependent tasks
|
||||
this.updateDependentTasksStatus(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency chain for a task (all tasks that must be completed first)
|
||||
*/
|
||||
getDependencyChain(taskId: string): string[] {
|
||||
const chain: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const buildChain = (currentTaskId: string) => {
|
||||
if (visited.has(currentTaskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(currentTaskId);
|
||||
const task = this.dependencyGraph[currentTaskId];
|
||||
|
||||
if (task) {
|
||||
task.dependencies.forEach(depId => {
|
||||
buildChain(depId);
|
||||
if (!chain.includes(depId)) {
|
||||
chain.push(depId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
buildChain(taskId);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get impact of completing a task (what tasks become available)
|
||||
*/
|
||||
getCompletionImpact(taskId: string): string[] {
|
||||
const impact: string[] = [];
|
||||
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task) {
|
||||
task.dependents.forEach(dependentId => {
|
||||
const dependent = this.dependencyGraph[dependentId];
|
||||
if (dependent && dependent.status === 'blocked') {
|
||||
// Check if all dependencies are now satisfied
|
||||
const allDepsSatisfied = dependent.dependencies.every(depId => {
|
||||
const depTask = this.dependencyGraph[depId];
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
if (allDepsSatisfied) {
|
||||
impact.push(dependentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return impact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect circular dependencies in the graph
|
||||
*/
|
||||
private detectCircularDependencies(): string[] {
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const circular: string[] = [];
|
||||
|
||||
const visit = (taskId: string, path: string[] = []) => {
|
||||
if (visiting.has(taskId)) {
|
||||
const cycleStart = path.indexOf(taskId);
|
||||
if (cycleStart !== -1) {
|
||||
circular.push(...path.slice(cycleStart), taskId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (visited.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(taskId);
|
||||
const task = this.dependencyGraph[taskId];
|
||||
|
||||
if (task) {
|
||||
task.dependencies.forEach(depId => {
|
||||
visit(depId, [...path, taskId]);
|
||||
});
|
||||
}
|
||||
|
||||
visiting.delete(taskId);
|
||||
visited.add(taskId);
|
||||
};
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
if (!visited.has(taskId)) {
|
||||
visit(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(circular)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect missing dependencies
|
||||
*/
|
||||
private detectMissingDependencies(workflow: DailyWorkflow): string[] {
|
||||
const missing: string[] = [];
|
||||
const taskIds = new Set(workflow.tasks.map(t => t.id));
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
if (!taskIds.has(depId)) {
|
||||
missing.push(`${taskId} -> ${depId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect orphaned tasks
|
||||
*/
|
||||
private detectOrphanedTasks(): string[] {
|
||||
const orphaned: string[] = [];
|
||||
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
if (task.dependencies.length === 0 && task.dependents.length === 0) {
|
||||
orphaned.push(taskId);
|
||||
}
|
||||
});
|
||||
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dependent tasks status when a dependency is completed
|
||||
*/
|
||||
private updateDependentTasksStatus(completedTaskId: string): void {
|
||||
const task = this.dependencyGraph[completedTaskId];
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
task.dependents.forEach(dependentId => {
|
||||
const dependent = this.dependencyGraph[dependentId];
|
||||
if (dependent && dependent.status === 'blocked') {
|
||||
// Check if all dependencies are now satisfied
|
||||
const allDepsSatisfied = dependent.dependencies.every(depId => {
|
||||
const depTask = this.dependencyGraph[depId];
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
if (allDepsSatisfied) {
|
||||
dependent.status = 'ready';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task status based on dependencies
|
||||
*/
|
||||
private getTaskStatus(task: TodayTask, workflow: DailyWorkflow): 'ready' | 'blocked' | 'completed' | 'skipped' {
|
||||
if (task.status === 'completed' || task.status === 'skipped') {
|
||||
return task.status;
|
||||
}
|
||||
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return 'ready';
|
||||
}
|
||||
|
||||
// Check if all dependencies are satisfied
|
||||
const allDepsSatisfied = task.dependencies.every(depId => {
|
||||
const depTask = workflow.tasks.find(t => t.id === depId);
|
||||
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
|
||||
});
|
||||
|
||||
return allDepsSatisfied ? 'ready' : 'blocked';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency graph visualization data
|
||||
*/
|
||||
getDependencyGraphData(): {
|
||||
nodes: Array<{ id: string; label: string; status: string }>;
|
||||
edges: Array<{ from: string; to: string; type: string }>;
|
||||
} {
|
||||
const nodes = Object.keys(this.dependencyGraph).map(taskId => ({
|
||||
id: taskId,
|
||||
label: taskId,
|
||||
status: this.dependencyGraph[taskId].status
|
||||
}));
|
||||
|
||||
const edges: Array<{ from: string; to: string; type: string }> = [];
|
||||
Object.keys(this.dependencyGraph).forEach(taskId => {
|
||||
const task = this.dependencyGraph[taskId];
|
||||
task.dependencies.forEach(depId => {
|
||||
edges.push({
|
||||
from: depId,
|
||||
to: taskId,
|
||||
type: 'dependency'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskDependencyManager = new TaskDependencyManager();
|
||||
export default TaskDependencyManager;
|
||||
469
frontend/src/services/TaskNavigationService.ts
Normal file
469
frontend/src/services/TaskNavigationService.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
NavigationState,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
|
||||
interface NavigationConfig {
|
||||
autoNavigate: boolean;
|
||||
delayBeforeNavigation: number; // milliseconds
|
||||
showNavigationConfirmation: boolean;
|
||||
enableBackNavigation: boolean;
|
||||
persistNavigationState: boolean;
|
||||
}
|
||||
|
||||
interface NavigationEvent {
|
||||
type: 'task_started' | 'task_completed' | 'task_skipped' | 'navigation_requested';
|
||||
taskId: string;
|
||||
workflowId: string;
|
||||
timestamp: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class TaskNavigationService {
|
||||
private config: NavigationConfig;
|
||||
private navigationHistory: NavigationEvent[] = [];
|
||||
private currentNavigationState: NavigationState | null = null;
|
||||
private navigationListeners: Array<(event: NavigationEvent) => void> = [];
|
||||
|
||||
constructor(config: NavigationConfig = {
|
||||
autoNavigate: true,
|
||||
delayBeforeNavigation: 2000,
|
||||
showNavigationConfirmation: false,
|
||||
enableBackNavigation: true,
|
||||
persistNavigationState: true
|
||||
}) {
|
||||
this.config = config;
|
||||
this.loadNavigationHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific task
|
||||
*/
|
||||
async navigateToTask(
|
||||
task: TodayTask,
|
||||
workflow: DailyWorkflow,
|
||||
options: {
|
||||
skipConfirmation?: boolean;
|
||||
trackNavigation?: boolean;
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Validate task and workflow
|
||||
if (!this.validateTaskForNavigation(task, workflow)) {
|
||||
throw new WorkflowError({
|
||||
code: 'INVALID_NAVIGATION_TARGET',
|
||||
message: `Cannot navigate to task ${task.id}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Check task dependencies and status'
|
||||
});
|
||||
}
|
||||
|
||||
// Show confirmation if required
|
||||
if (this.config.showNavigationConfirmation && !options.skipConfirmation) {
|
||||
const confirmed = await this.showNavigationConfirmation(task);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute navigation based on action type
|
||||
const navigationSuccess = await this.executeNavigation(task);
|
||||
|
||||
if (navigationSuccess) {
|
||||
// Update navigation state
|
||||
this.updateNavigationState(task, workflow);
|
||||
|
||||
// Track navigation event
|
||||
if (options.trackNavigation !== false) {
|
||||
this.trackNavigationEvent({
|
||||
type: 'navigation_requested',
|
||||
taskId: task.id,
|
||||
workflowId: workflow.id,
|
||||
timestamp: new Date(),
|
||||
metadata: { actionType: task.actionType, actionUrl: task.actionUrl }
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-navigate to next task if enabled
|
||||
if (this.config.autoNavigate && task.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
this.autoNavigateToNextTask(workflow);
|
||||
}, this.config.delayBeforeNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
return navigationSuccess;
|
||||
} catch (error) {
|
||||
console.error('Navigation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-navigate to the next available task
|
||||
*/
|
||||
async autoNavigateToNextTask(workflow: DailyWorkflow): Promise<TodayTask | null> {
|
||||
try {
|
||||
const nextTask = this.getNextAvailableTask(workflow);
|
||||
|
||||
if (nextTask) {
|
||||
await this.navigateToTask(nextTask, workflow, {
|
||||
skipConfirmation: true,
|
||||
trackNavigation: true
|
||||
});
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Auto-navigation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back to previous task
|
||||
*/
|
||||
async navigateBack(workflow: DailyWorkflow): Promise<TodayTask | null> {
|
||||
if (!this.config.enableBackNavigation) {
|
||||
throw new WorkflowError({
|
||||
code: 'BACK_NAVIGATION_DISABLED',
|
||||
message: 'Back navigation is disabled',
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const previousTask = this.getPreviousTask(workflow);
|
||||
|
||||
if (previousTask) {
|
||||
await this.navigateToTask(previousTask, workflow, {
|
||||
skipConfirmation: true,
|
||||
trackNavigation: true
|
||||
});
|
||||
return previousTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Back navigation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available task in the workflow
|
||||
*/
|
||||
getNextAvailableTask(workflow: DailyWorkflow): TodayTask | null {
|
||||
const currentIndex = workflow.currentTaskIndex;
|
||||
const remainingTasks = workflow.tasks.slice(currentIndex + 1);
|
||||
|
||||
// Find next task that's not completed and has dependencies satisfied
|
||||
for (const task of remainingTasks) {
|
||||
if (task.status === 'pending' && this.areDependenciesSatisfied(task, workflow)) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous task in the workflow
|
||||
*/
|
||||
getPreviousTask(workflow: DailyWorkflow): TodayTask | null {
|
||||
const currentIndex = workflow.currentTaskIndex;
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return workflow.tasks[currentIndex - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if task dependencies are satisfied
|
||||
*/
|
||||
areDependenciesSatisfied(task: TodayTask, workflow: DailyWorkflow): boolean {
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return task.dependencies.every(depId => {
|
||||
const depTask = workflow.tasks.find(t => t.id === depId);
|
||||
return depTask && depTask.status === 'completed';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual navigation based on task action type
|
||||
*/
|
||||
private async executeNavigation(task: TodayTask): Promise<boolean> {
|
||||
try {
|
||||
switch (task.actionType) {
|
||||
case 'navigate':
|
||||
return await this.navigateToInternalPage(task);
|
||||
case 'modal':
|
||||
return await this.openModal(task);
|
||||
case 'external':
|
||||
return await this.navigateToExternalUrl(task);
|
||||
default:
|
||||
throw new WorkflowError({
|
||||
code: 'UNKNOWN_ACTION_TYPE',
|
||||
message: `Unknown action type: ${task.actionType}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Check task configuration'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Navigation execution failed for task ${task.id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to internal ALwrity page
|
||||
*/
|
||||
private async navigateToInternalPage(task: TodayTask): Promise<boolean> {
|
||||
if (!task.actionUrl) {
|
||||
throw new WorkflowError({
|
||||
code: 'MISSING_ACTION_URL',
|
||||
message: `Task ${task.id} is missing action URL`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Configure action URL for the task'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Use React Router navigation
|
||||
if (typeof window !== 'undefined' && window.history) {
|
||||
window.history.pushState(null, '', task.actionUrl);
|
||||
|
||||
// Dispatch custom event for React Router to handle
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Internal navigation failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal for task
|
||||
*/
|
||||
private async openModal(task: TodayTask): Promise<boolean> {
|
||||
try {
|
||||
// Dispatch custom event to open modal
|
||||
const modalEvent = new CustomEvent('openTaskModal', {
|
||||
detail: { task }
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(modalEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Modal opening failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to external URL
|
||||
*/
|
||||
private async navigateToExternalUrl(task: TodayTask): Promise<boolean> {
|
||||
if (!task.actionUrl) {
|
||||
throw new WorkflowError({
|
||||
code: 'MISSING_ACTION_URL',
|
||||
message: `Task ${task.id} is missing external URL`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Configure external URL for the task'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open(task.actionUrl, '_blank', 'noopener,noreferrer');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('External navigation failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if task can be navigated to
|
||||
*/
|
||||
private validateTaskForNavigation(task: TodayTask, workflow: DailyWorkflow): boolean {
|
||||
// Check if task exists in workflow
|
||||
const workflowTask = workflow.tasks.find(t => t.id === task.id);
|
||||
if (!workflowTask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task is enabled
|
||||
if (!task.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
if (!this.areDependenciesSatisfied(task, workflow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show navigation confirmation dialog
|
||||
*/
|
||||
private async showNavigationConfirmation(task: TodayTask): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// In a real implementation, this would show a confirmation dialog
|
||||
// For now, we'll use a simple confirm dialog
|
||||
const confirmed = window.confirm(
|
||||
`Navigate to: ${task.title}\n\n${task.description}\n\nContinue?`
|
||||
);
|
||||
resolve(confirmed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation state
|
||||
*/
|
||||
private updateNavigationState(task: TodayTask, workflow: DailyWorkflow): void {
|
||||
const currentIndex = workflow.tasks.findIndex(t => t.id === task.id);
|
||||
const previousTask = currentIndex > 0 ? workflow.tasks[currentIndex - 1] : null;
|
||||
const nextTask = currentIndex < workflow.tasks.length - 1 ? workflow.tasks[currentIndex + 1] : null;
|
||||
|
||||
this.currentNavigationState = {
|
||||
currentTask: task,
|
||||
previousTask,
|
||||
nextTask,
|
||||
canGoBack: currentIndex > 0,
|
||||
canGoForward: currentIndex < workflow.tasks.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Track navigation event
|
||||
*/
|
||||
private trackNavigationEvent(event: NavigationEvent): void {
|
||||
this.navigationHistory.push(event);
|
||||
|
||||
// Notify listeners
|
||||
this.navigationListeners.forEach(listener => {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
console.error('Navigation listener error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Persist navigation history
|
||||
if (this.config.persistNavigationState) {
|
||||
this.persistNavigationHistory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add navigation event listener
|
||||
*/
|
||||
addNavigationListener(listener: (event: NavigationEvent) => void): void {
|
||||
this.navigationListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove navigation event listener
|
||||
*/
|
||||
removeNavigationListener(listener: (event: NavigationEvent) => void): void {
|
||||
const index = this.navigationListeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.navigationListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current navigation state
|
||||
*/
|
||||
getCurrentNavigationState(): NavigationState | null {
|
||||
return this.currentNavigationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation history
|
||||
*/
|
||||
getNavigationHistory(): NavigationEvent[] {
|
||||
return [...this.navigationHistory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear navigation history
|
||||
*/
|
||||
clearNavigationHistory(): void {
|
||||
this.navigationHistory = [];
|
||||
this.persistNavigationHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist navigation history to localStorage
|
||||
*/
|
||||
private persistNavigationHistory(): void {
|
||||
try {
|
||||
localStorage.setItem('task-navigation-history', JSON.stringify(this.navigationHistory));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist navigation history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load navigation history from localStorage
|
||||
*/
|
||||
private loadNavigationHistory(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem('task-navigation-history');
|
||||
if (stored) {
|
||||
this.navigationHistory = JSON.parse(stored).map((event: any) => ({
|
||||
...event,
|
||||
timestamp: new Date(event.timestamp)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load navigation history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<NavigationConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): NavigationConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskNavigationService = new TaskNavigationService();
|
||||
export default TaskNavigationService;
|
||||
611
frontend/src/services/TaskWorkflowOrchestrator.ts
Normal file
611
frontend/src/services/TaskWorkflowOrchestrator.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
WorkflowProgress,
|
||||
TaskCompletionData,
|
||||
TaskGenerationContext,
|
||||
WorkflowOrchestratorConfig,
|
||||
NavigationState,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
import { taskNavigationService } from './TaskNavigationService';
|
||||
import { taskDependencyManager } from './TaskDependencyManager';
|
||||
import { taskCompletionVerifier } from './TaskCompletionVerifier';
|
||||
|
||||
class TaskWorkflowOrchestrator {
|
||||
private workflows: Map<string, DailyWorkflow> = new Map();
|
||||
private config: WorkflowOrchestratorConfig;
|
||||
|
||||
constructor(config: WorkflowOrchestratorConfig = {
|
||||
autoNavigate: true,
|
||||
showProgress: true,
|
||||
enableNotifications: true,
|
||||
persistProgress: true,
|
||||
allowTaskSkipping: true
|
||||
}) {
|
||||
this.config = config;
|
||||
this.loadPersistedWorkflows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new daily workflow for a user
|
||||
*/
|
||||
async generateDailyWorkflow(
|
||||
userId: string,
|
||||
date: string = new Date().toISOString().split('T')[0],
|
||||
context?: TaskGenerationContext
|
||||
): Promise<DailyWorkflow> {
|
||||
try {
|
||||
// Check if workflow already exists for this date
|
||||
const existingWorkflow = this.getWorkflow(userId, date);
|
||||
if (existingWorkflow) {
|
||||
return existingWorkflow;
|
||||
}
|
||||
|
||||
// Generate tasks based on context or default configuration
|
||||
const tasks = await this.generateTasksForDate(userId, date, context);
|
||||
|
||||
// Create new workflow
|
||||
const workflow: DailyWorkflow = {
|
||||
id: `${userId}-${date}`,
|
||||
date,
|
||||
userId,
|
||||
tasks,
|
||||
currentTaskIndex: 0,
|
||||
completedTasks: 0,
|
||||
totalTasks: tasks.length,
|
||||
workflowStatus: 'not_started',
|
||||
totalEstimatedTime: tasks.reduce((sum, task) => sum + task.estimatedTime, 0),
|
||||
actualTimeSpent: 0
|
||||
};
|
||||
|
||||
// Save workflow
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
this.persistWorkflow(workflow);
|
||||
|
||||
return workflow;
|
||||
} catch (error) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_GENERATION_FAILED',
|
||||
message: `Failed to generate workflow for user ${userId} on ${date}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: true,
|
||||
suggestedAction: 'Retry workflow generation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow for a specific user and date
|
||||
*/
|
||||
getWorkflow(userId: string, date: string): DailyWorkflow | null {
|
||||
const workflowId = `${userId}-${date}`;
|
||||
return this.workflows.get(workflowId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a workflow
|
||||
*/
|
||||
async startWorkflow(workflowId: string): Promise<DailyWorkflow> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
workflow.workflowStatus = 'in_progress';
|
||||
workflow.startedAt = new Date();
|
||||
|
||||
// Mark first task as in progress
|
||||
if (workflow.tasks.length > 0) {
|
||||
workflow.tasks[0].status = 'in_progress';
|
||||
workflow.tasks[0].startedAt = new Date();
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a specific task
|
||||
*/
|
||||
async completeTask(
|
||||
workflowId: string,
|
||||
taskId: string,
|
||||
completionData?: Partial<TaskCompletionData>
|
||||
): Promise<WorkflowProgress> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const task = workflow.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
throw new WorkflowError({
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task ${taskId} not found in workflow ${workflowId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Verify task completion
|
||||
await taskCompletionVerifier.verifyTaskCompletion(task, {
|
||||
userId: workflow.userId,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Mark task as completed
|
||||
task.status = 'completed';
|
||||
task.completedAt = new Date();
|
||||
|
||||
// Calculate time spent
|
||||
if (task.startedAt) {
|
||||
const timeSpent = Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / (1000 * 60));
|
||||
workflow.actualTimeSpent += timeSpent;
|
||||
}
|
||||
|
||||
// Update dependency manager
|
||||
taskDependencyManager.updateTaskStatus(taskId, 'completed');
|
||||
|
||||
// Update workflow progress
|
||||
workflow.completedTasks++;
|
||||
|
||||
// Check if workflow is complete
|
||||
if (workflow.completedTasks === workflow.totalTasks) {
|
||||
workflow.workflowStatus = 'completed';
|
||||
workflow.completedAt = new Date();
|
||||
}
|
||||
|
||||
// Auto-navigate to next task if enabled
|
||||
if (this.config.autoNavigate) {
|
||||
const nextTask = taskDependencyManager.getReadyTasks(workflow)[0];
|
||||
if (nextTask) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await taskNavigationService.navigateToTask(nextTask, workflow);
|
||||
} catch (error) {
|
||||
console.warn('Auto-navigation failed:', error);
|
||||
}
|
||||
}, 2000); // 2 second delay
|
||||
}
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return this.getWorkflowProgress(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip a task
|
||||
*/
|
||||
async skipTask(workflowId: string, taskId: string): Promise<WorkflowProgress> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const task = workflow.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
throw new WorkflowError({
|
||||
code: 'TASK_NOT_FOUND',
|
||||
message: `Task ${taskId} not found in workflow ${workflowId}`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
task.status = 'skipped';
|
||||
workflow.completedTasks++;
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return this.getWorkflowProgress(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current workflow progress
|
||||
*/
|
||||
getWorkflowProgress(workflowId: string): WorkflowProgress {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const currentTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
const nextTask = workflow.tasks[workflow.currentTaskIndex + 1];
|
||||
const remainingTasks = workflow.tasks.slice(workflow.currentTaskIndex + 1);
|
||||
const estimatedTimeRemaining = remainingTasks.reduce((sum, task) => sum + task.estimatedTime, 0);
|
||||
|
||||
return {
|
||||
completedTasks: workflow.completedTasks,
|
||||
totalTasks: workflow.totalTasks,
|
||||
completionPercentage: Math.round((workflow.completedTasks / workflow.totalTasks) * 100),
|
||||
currentTask,
|
||||
nextTask,
|
||||
estimatedTimeRemaining,
|
||||
actualTimeSpent: workflow.actualTimeSpent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation state for current workflow
|
||||
*/
|
||||
getNavigationState(workflowId: string): NavigationState {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
const currentTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
const previousTask = workflow.currentTaskIndex > 0 ? workflow.tasks[workflow.currentTaskIndex - 1] : null;
|
||||
const nextTask = workflow.currentTaskIndex < workflow.tasks.length - 1 ? workflow.tasks[workflow.currentTaskIndex + 1] : null;
|
||||
|
||||
return {
|
||||
currentTask,
|
||||
previousTask,
|
||||
nextTask,
|
||||
canGoBack: workflow.currentTaskIndex > 0,
|
||||
canGoForward: workflow.currentTaskIndex < workflow.tasks.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next task in workflow
|
||||
*/
|
||||
async moveToNextTask(workflowId: string): Promise<TodayTask | null> {
|
||||
const workflow = this.workflows.get(workflowId);
|
||||
if (!workflow) {
|
||||
throw new WorkflowError({
|
||||
code: 'WORKFLOW_NOT_FOUND',
|
||||
message: `Workflow ${workflowId} not found`,
|
||||
timestamp: new Date(),
|
||||
recoverable: false
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.currentTaskIndex < workflow.tasks.length - 1) {
|
||||
workflow.currentTaskIndex++;
|
||||
const nextTask = workflow.tasks[workflow.currentTaskIndex];
|
||||
|
||||
// Mark next task as in progress
|
||||
if (nextTask.status === 'pending') {
|
||||
nextTask.status = 'in_progress';
|
||||
nextTask.startedAt = new Date();
|
||||
}
|
||||
|
||||
this.persistWorkflow(workflow);
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tasks for a specific date (enhanced with dependency management)
|
||||
*/
|
||||
private async generateTasksForDate(
|
||||
userId: string,
|
||||
date: string,
|
||||
context?: TaskGenerationContext
|
||||
): Promise<TodayTask[]> {
|
||||
// This is a placeholder implementation
|
||||
// In Phase 3, this will be replaced with AI-powered task generation
|
||||
|
||||
const defaultTasks: TodayTask[] = [
|
||||
{
|
||||
id: `${userId}-${date}-plan-1`,
|
||||
pillarId: 'plan',
|
||||
title: 'Review content strategy',
|
||||
description: 'Check and update your content strategy for the week',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 15,
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Business',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-plan-2`,
|
||||
pillarId: 'plan',
|
||||
title: 'Update content calendar',
|
||||
description: 'Review and update your content calendar',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 10,
|
||||
dependencies: [`${userId}-${date}-plan-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/content-planning-dashboard',
|
||||
enabled: true,
|
||||
icon: 'CalendarMonth',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-generate-1`,
|
||||
pillarId: 'generate',
|
||||
title: 'Create social media content',
|
||||
description: 'Generate content for your social media platforms',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 30,
|
||||
dependencies: [`${userId}-${date}-plan-1`, `${userId}-${date}-plan-2`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/facebook-writer',
|
||||
enabled: true,
|
||||
icon: 'AutoAwesome',
|
||||
color: '#2196F3'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-generate-2`,
|
||||
pillarId: 'generate',
|
||||
title: 'Create blog content',
|
||||
description: 'Write blog posts for your website',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 45,
|
||||
dependencies: [`${userId}-${date}-plan-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/blog-writer',
|
||||
enabled: true,
|
||||
icon: 'Article',
|
||||
color: '#2196F3'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-publish-1`,
|
||||
pillarId: 'publish',
|
||||
title: 'Publish social media content',
|
||||
description: 'Publish your created content to social media',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 10,
|
||||
dependencies: [`${userId}-${date}-generate-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/facebook-writer',
|
||||
enabled: true,
|
||||
icon: 'Publish',
|
||||
color: '#FF9800'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-publish-2`,
|
||||
pillarId: 'publish',
|
||||
title: 'Publish blog content',
|
||||
description: 'Publish blog posts to your website',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 15,
|
||||
dependencies: [`${userId}-${date}-generate-2`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/blog-writer',
|
||||
enabled: true,
|
||||
icon: 'Publish',
|
||||
color: '#FF9800'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-analyze-1`,
|
||||
pillarId: 'analyze',
|
||||
title: 'Review content performance',
|
||||
description: 'Analyze performance of published content',
|
||||
status: 'pending',
|
||||
priority: 'low',
|
||||
estimatedTime: 20,
|
||||
dependencies: [`${userId}-${date}-publish-1`, `${userId}-${date}-publish-2`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/analytics-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Analytics',
|
||||
color: '#9C27B0'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-engage-1`,
|
||||
pillarId: 'engage',
|
||||
title: 'Respond to comments',
|
||||
description: 'Engage with comments on your content',
|
||||
status: 'pending',
|
||||
priority: 'low',
|
||||
estimatedTime: 15,
|
||||
dependencies: [`${userId}-${date}-publish-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/engagement-dashboard',
|
||||
enabled: true,
|
||||
icon: 'ChatBubbleOutline',
|
||||
color: '#E91E63'
|
||||
},
|
||||
// Engage pillar tasks
|
||||
{
|
||||
id: `${userId}-${date}-engage-1`,
|
||||
pillarId: 'engage',
|
||||
title: 'Reply to blog comment',
|
||||
description: 'Respond to comments on your latest blog post',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 10,
|
||||
dependencies: [`${userId}-${date}-analyze-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/engagement-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Comment',
|
||||
color: '#E91E63'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-engage-2`,
|
||||
pillarId: 'engage',
|
||||
title: 'Respond to Twitter mention',
|
||||
description: 'Reply to Twitter mentions and engage with followers',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 5,
|
||||
dependencies: [`${userId}-${date}-engage-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/engagement-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Twitter',
|
||||
color: '#1DA1F2'
|
||||
},
|
||||
// Remarket pillar tasks
|
||||
{
|
||||
id: `${userId}-${date}-remarket-1`,
|
||||
pillarId: 'remarket',
|
||||
title: 'Launch Retargeting Campaign',
|
||||
description: 'Create and launch targeted remarketing campaigns',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedTime: 35,
|
||||
dependencies: [`${userId}-${date}-engage-2`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/remarketing-dashboard',
|
||||
enabled: true,
|
||||
icon: 'Psychology',
|
||||
color: '#00695C'
|
||||
},
|
||||
{
|
||||
id: `${userId}-${date}-remarket-2`,
|
||||
pillarId: 'remarket',
|
||||
title: 'Lead Nurturing Sequence',
|
||||
description: 'Set up automated lead nurturing workflows',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedTime: 30,
|
||||
dependencies: [`${userId}-${date}-remarket-1`],
|
||||
actionType: 'navigate',
|
||||
actionUrl: '/lead-nurturing',
|
||||
enabled: true,
|
||||
icon: 'Refresh',
|
||||
color: '#4CAF50'
|
||||
}
|
||||
];
|
||||
|
||||
// Validate dependencies and get optimal execution order
|
||||
const tempWorkflow: DailyWorkflow = {
|
||||
id: `${userId}-${date}`,
|
||||
date,
|
||||
userId,
|
||||
tasks: defaultTasks,
|
||||
currentTaskIndex: 0,
|
||||
completedTasks: 0,
|
||||
totalTasks: defaultTasks.length,
|
||||
workflowStatus: 'not_started',
|
||||
totalEstimatedTime: defaultTasks.reduce((sum, task) => sum + task.estimatedTime, 0),
|
||||
actualTimeSpent: 0
|
||||
};
|
||||
|
||||
// Validate dependency graph
|
||||
const validation = taskDependencyManager.validateDependencyGraph(tempWorkflow);
|
||||
if (!validation.isValid) {
|
||||
console.warn('Dependency validation failed:', validation.errors);
|
||||
// Return tasks without dependencies if validation fails
|
||||
return defaultTasks.map(task => ({ ...task, dependencies: [] }));
|
||||
}
|
||||
|
||||
// Get optimal execution order
|
||||
const orderedTasks = taskDependencyManager.getOptimalExecutionOrder(tempWorkflow);
|
||||
|
||||
return orderedTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist workflow to localStorage
|
||||
*/
|
||||
private persistWorkflow(workflow: DailyWorkflow): void {
|
||||
if (this.config.persistProgress) {
|
||||
try {
|
||||
localStorage.setItem(`workflow-${workflow.id}`, JSON.stringify(workflow));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist workflow:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persisted workflows from localStorage
|
||||
*/
|
||||
private loadPersistedWorkflows(): void {
|
||||
if (this.config.persistProgress) {
|
||||
try {
|
||||
const keys = Object.keys(localStorage).filter(key => key.startsWith('workflow-'));
|
||||
keys.forEach(key => {
|
||||
const workflowData = localStorage.getItem(key);
|
||||
if (workflowData) {
|
||||
try {
|
||||
const workflow = JSON.parse(workflowData) as DailyWorkflow;
|
||||
|
||||
// Ensure workflow has required properties
|
||||
if (!workflow.id || !workflow.date || !workflow.userId) {
|
||||
console.warn(`Invalid workflow data for key ${key}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure tasks array exists and is valid
|
||||
if (!workflow.tasks || !Array.isArray(workflow.tasks)) {
|
||||
console.warn(`Invalid tasks array for workflow ${workflow.id}, initializing empty array`);
|
||||
workflow.tasks = [];
|
||||
}
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
if (workflow.startedAt) workflow.startedAt = new Date(workflow.startedAt);
|
||||
if (workflow.completedAt) workflow.completedAt = new Date(workflow.completedAt);
|
||||
|
||||
// Process tasks with null checks
|
||||
workflow.tasks.forEach(task => {
|
||||
if (task && typeof task === 'object') {
|
||||
if (task.startedAt) task.startedAt = new Date(task.startedAt);
|
||||
if (task.completedAt) task.completedAt = new Date(task.completedAt);
|
||||
}
|
||||
});
|
||||
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
} catch (parseError) {
|
||||
console.warn(`Failed to parse workflow data for key ${key}:`, parseError);
|
||||
// Remove corrupted data
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load persisted workflows:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear completed workflows (cleanup)
|
||||
*/
|
||||
clearCompletedWorkflows(): void {
|
||||
const completedWorkflows = Array.from(this.workflows.values())
|
||||
.filter(workflow => workflow.workflowStatus === 'completed');
|
||||
|
||||
completedWorkflows.forEach(workflow => {
|
||||
this.workflows.delete(workflow.id);
|
||||
if (this.config.persistProgress) {
|
||||
localStorage.removeItem(`workflow-${workflow.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const taskWorkflowOrchestrator = new TaskWorkflowOrchestrator();
|
||||
export default TaskWorkflowOrchestrator;
|
||||
366
frontend/src/stores/workflowStore.ts
Normal file
366
frontend/src/stores/workflowStore.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import {
|
||||
TodayTask,
|
||||
DailyWorkflow,
|
||||
WorkflowProgress,
|
||||
UserWorkflowPreferences,
|
||||
NavigationState,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
import { taskWorkflowOrchestrator } from '../services/TaskWorkflowOrchestrator';
|
||||
|
||||
interface WorkflowState {
|
||||
// Current workflow state
|
||||
currentWorkflow: DailyWorkflow | null;
|
||||
workflowProgress: WorkflowProgress | null;
|
||||
navigationState: NavigationState | null;
|
||||
|
||||
// User preferences
|
||||
userPreferences: UserWorkflowPreferences | null;
|
||||
|
||||
// UI state
|
||||
isWorkflowModalOpen: boolean;
|
||||
isLoading: boolean;
|
||||
error: WorkflowError | null;
|
||||
|
||||
// Actions
|
||||
generateDailyWorkflow: (userId: string, date?: string) => Promise<void>;
|
||||
startWorkflow: (workflowId: string) => Promise<void>;
|
||||
pauseWorkflow: (workflowId: string) => Promise<void>;
|
||||
stopWorkflow: (workflowId: string) => Promise<void>;
|
||||
completeTask: (taskId: string, completionData?: any) => Promise<void>;
|
||||
skipTask: (taskId: string) => Promise<void>;
|
||||
moveToNextTask: () => Promise<void>;
|
||||
moveToPreviousTask: () => Promise<void>;
|
||||
|
||||
// UI actions
|
||||
openWorkflowModal: () => void;
|
||||
closeWorkflowModal: () => void;
|
||||
setError: (error: WorkflowError | null) => void;
|
||||
clearError: () => void;
|
||||
|
||||
// Preferences
|
||||
updateUserPreferences: (preferences: Partial<UserWorkflowPreferences>) => void;
|
||||
|
||||
// Utility actions
|
||||
refreshWorkflowProgress: () => void;
|
||||
getCurrentTask: () => TodayTask | null;
|
||||
getNextTask: () => TodayTask | null;
|
||||
isWorkflowComplete: () => boolean;
|
||||
getCompletionPercentage: () => number;
|
||||
}
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
currentWorkflow: null,
|
||||
workflowProgress: null,
|
||||
navigationState: null,
|
||||
userPreferences: null,
|
||||
isWorkflowModalOpen: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Generate daily workflow
|
||||
generateDailyWorkflow: async (userId: string, date?: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const workflow = await taskWorkflowOrchestrator.generateDailyWorkflow(userId, date);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(workflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(workflow.id);
|
||||
|
||||
set({
|
||||
currentWorkflow: workflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Start workflow
|
||||
startWorkflow: async (workflowId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const workflow = await taskWorkflowOrchestrator.startWorkflow(workflowId);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(workflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(workflow.id);
|
||||
|
||||
set({
|
||||
currentWorkflow: workflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Pause workflow
|
||||
pauseWorkflow: async (workflowId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// For now, we'll just update the workflow status to paused
|
||||
// In a real implementation, this would call the orchestrator
|
||||
const currentWorkflow = get().currentWorkflow;
|
||||
if (currentWorkflow && currentWorkflow.id === workflowId) {
|
||||
const pausedWorkflow = {
|
||||
...currentWorkflow,
|
||||
workflowStatus: 'paused' as const,
|
||||
pausedAt: new Date()
|
||||
};
|
||||
|
||||
set({
|
||||
currentWorkflow: pausedWorkflow,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Stop workflow
|
||||
stopWorkflow: async (workflowId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// For now, we'll just update the workflow status to stopped
|
||||
// In a real implementation, this would call the orchestrator
|
||||
const currentWorkflow = get().currentWorkflow;
|
||||
if (currentWorkflow && currentWorkflow.id === workflowId) {
|
||||
const stoppedWorkflow = {
|
||||
...currentWorkflow,
|
||||
workflowStatus: 'stopped' as const,
|
||||
completedAt: new Date()
|
||||
};
|
||||
|
||||
set({
|
||||
currentWorkflow: stoppedWorkflow,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Complete task
|
||||
completeTask: async (taskId: string, completionData?: any) => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const progress = await taskWorkflowOrchestrator.completeTask(
|
||||
currentWorkflow.id,
|
||||
taskId,
|
||||
completionData
|
||||
);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Skip task
|
||||
skipTask: async (taskId: string) => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const progress = await taskWorkflowOrchestrator.skipTask(
|
||||
currentWorkflow.id,
|
||||
taskId
|
||||
);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Move to next task
|
||||
moveToNextTask: async () => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
await taskWorkflowOrchestrator.moveToNextTask(currentWorkflow.id);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(currentWorkflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Move to previous task
|
||||
moveToPreviousTask: async () => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// This would need to be implemented in the orchestrator
|
||||
// For now, we'll just refresh the navigation state
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
set({
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
error: workflowError,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI actions
|
||||
openWorkflowModal: () => set({ isWorkflowModalOpen: true }),
|
||||
closeWorkflowModal: () => set({ isWorkflowModalOpen: false }),
|
||||
setError: (error: WorkflowError | null) => set({ error }),
|
||||
clearError: () => set({ error: null }),
|
||||
|
||||
// Update user preferences
|
||||
updateUserPreferences: (preferences: Partial<UserWorkflowPreferences>) => {
|
||||
const { userPreferences } = get();
|
||||
set({
|
||||
userPreferences: {
|
||||
...userPreferences,
|
||||
...preferences
|
||||
} as UserWorkflowPreferences
|
||||
});
|
||||
},
|
||||
|
||||
// Utility actions
|
||||
refreshWorkflowProgress: () => {
|
||||
const { currentWorkflow } = get();
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
try {
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(currentWorkflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
set({
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh workflow progress:', error);
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentTask: () => {
|
||||
const { navigationState } = get();
|
||||
return navigationState?.currentTask || null;
|
||||
},
|
||||
|
||||
getNextTask: () => {
|
||||
const { navigationState } = get();
|
||||
return navigationState?.nextTask || null;
|
||||
},
|
||||
|
||||
isWorkflowComplete: () => {
|
||||
const { workflowProgress } = get();
|
||||
return workflowProgress ? workflowProgress.completedTasks === workflowProgress.totalTasks : false;
|
||||
},
|
||||
|
||||
getCompletionPercentage: () => {
|
||||
const { workflowProgress } = get();
|
||||
return workflowProgress?.completionPercentage || 0;
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'workflow-store',
|
||||
partialize: (state) => ({
|
||||
userPreferences: state.userPreferences,
|
||||
currentWorkflow: state.currentWorkflow
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useWorkflowStore;
|
||||
168
frontend/src/types/workflow.ts
Normal file
168
frontend/src/types/workflow.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
// Core workflow and task type definitions
|
||||
import React from 'react';
|
||||
|
||||
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'skipped';
|
||||
export type TaskPriority = 'high' | 'medium' | 'low';
|
||||
export type ActionType = 'navigate' | 'modal' | 'external';
|
||||
export type WorkflowStatus = 'not_started' | 'in_progress' | 'completed' | 'paused' | 'stopped';
|
||||
|
||||
export interface TodayTask {
|
||||
id: string;
|
||||
pillarId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
estimatedTime: number; // in minutes
|
||||
dependencies?: string[]; // task IDs that must be completed first
|
||||
actionUrl?: string;
|
||||
actionType: ActionType;
|
||||
completedAt?: Date;
|
||||
startedAt?: Date;
|
||||
metadata?: Record<string, any>;
|
||||
icon?: string | React.ComponentType<any>; // icon name or component reference
|
||||
color?: string;
|
||||
enabled: boolean;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export interface DailyWorkflow {
|
||||
id: string;
|
||||
date: string; // YYYY-MM-DD format
|
||||
userId: string;
|
||||
tasks: TodayTask[];
|
||||
currentTaskIndex: number;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
workflowStatus: WorkflowStatus;
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
totalEstimatedTime: number; // in minutes
|
||||
actualTimeSpent: number; // in minutes
|
||||
}
|
||||
|
||||
export interface WorkflowProgress {
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
completionPercentage: number;
|
||||
currentTask?: TodayTask;
|
||||
nextTask?: TodayTask;
|
||||
estimatedTimeRemaining: number; // in minutes
|
||||
actualTimeSpent: number; // in minutes
|
||||
}
|
||||
|
||||
export interface TaskCompletionData {
|
||||
taskId: string;
|
||||
completedAt: Date;
|
||||
timeSpent: number; // in minutes
|
||||
userNotes?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WorkflowAnalytics {
|
||||
dailyCompletionRate: number;
|
||||
averageTaskTime: number;
|
||||
mostCompletedPillar: string;
|
||||
completionStreak: number;
|
||||
totalTasksCompleted: number;
|
||||
lastWorkflowDate?: string;
|
||||
}
|
||||
|
||||
// Pillar-specific task generation interfaces
|
||||
export interface PillarTaskConfig {
|
||||
pillarId: string;
|
||||
enabled: boolean;
|
||||
taskCount: number;
|
||||
priority: TaskPriority;
|
||||
dependencies: string[];
|
||||
customTasks?: TodayTask[];
|
||||
}
|
||||
|
||||
export interface UserWorkflowPreferences {
|
||||
userId: string;
|
||||
preferredTaskOrder: string[]; // pillar IDs in preferred order
|
||||
dailyTaskLimit: number;
|
||||
estimatedTimeLimit: number; // in minutes
|
||||
skipWeekends: boolean;
|
||||
notificationSettings: {
|
||||
taskReminders: boolean;
|
||||
completionCelebrations: boolean;
|
||||
progressUpdates: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Workflow orchestration interfaces
|
||||
export interface WorkflowOrchestratorConfig {
|
||||
autoNavigate: boolean;
|
||||
showProgress: boolean;
|
||||
enableNotifications: boolean;
|
||||
persistProgress: boolean;
|
||||
allowTaskSkipping: boolean;
|
||||
}
|
||||
|
||||
export interface TaskGenerationContext {
|
||||
userId: string;
|
||||
date: string;
|
||||
userPreferences: UserWorkflowPreferences;
|
||||
existingTasks: TodayTask[];
|
||||
platformData?: Record<string, any>; // data from connected platforms
|
||||
}
|
||||
|
||||
// Navigation and action interfaces
|
||||
export interface TaskAction {
|
||||
type: ActionType;
|
||||
url?: string;
|
||||
modalId?: string;
|
||||
externalUrl?: string;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
currentTask: TodayTask | null;
|
||||
previousTask: TodayTask | null;
|
||||
nextTask: TodayTask | null;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
}
|
||||
|
||||
// Error handling interfaces
|
||||
export interface WorkflowError {
|
||||
code: string;
|
||||
message: string;
|
||||
taskId?: string;
|
||||
timestamp: Date;
|
||||
recoverable: boolean;
|
||||
suggestedAction?: string;
|
||||
}
|
||||
|
||||
// WorkflowError class for throwing errors
|
||||
export class WorkflowError extends Error {
|
||||
code: string;
|
||||
taskId?: string;
|
||||
timestamp: Date;
|
||||
recoverable: boolean;
|
||||
suggestedAction?: string;
|
||||
|
||||
constructor(error: {
|
||||
code: string;
|
||||
message: string;
|
||||
taskId?: string;
|
||||
timestamp: Date;
|
||||
recoverable: boolean;
|
||||
suggestedAction?: string;
|
||||
}) {
|
||||
super(error.message);
|
||||
this.name = 'WorkflowError';
|
||||
this.code = error.code;
|
||||
this.taskId = error.taskId;
|
||||
this.timestamp = error.timestamp;
|
||||
this.recoverable = error.recoverable;
|
||||
this.suggestedAction = error.suggestedAction;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkflowErrorHandler {
|
||||
handleError: (error: WorkflowError) => Promise<void>;
|
||||
recoverFromError: (error: WorkflowError) => Promise<boolean>;
|
||||
logError: (error: WorkflowError) => Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user