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';
|
} from '@mui/material';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import AskAlwrityIcon from '../../assets/images/AskAlwrity-min.ico';
|
||||||
|
|
||||||
// Shared components
|
// Shared components
|
||||||
import DashboardHeader from '../shared/DashboardHeader';
|
import DashboardHeader from '../shared/DashboardHeader';
|
||||||
@@ -20,13 +21,15 @@ import CategoryHeader from '../shared/CategoryHeader';
|
|||||||
import LoadingSkeleton from '../shared/LoadingSkeleton';
|
import LoadingSkeleton from '../shared/LoadingSkeleton';
|
||||||
import ErrorDisplay from '../shared/ErrorDisplay';
|
import ErrorDisplay from '../shared/ErrorDisplay';
|
||||||
import EmptyState from '../shared/EmptyState';
|
import EmptyState from '../shared/EmptyState';
|
||||||
|
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
||||||
|
|
||||||
// Shared types and utilities
|
// Shared types and utilities
|
||||||
import { Tool, Category } from '../shared/types';
|
import { Tool } from '../shared/types';
|
||||||
import { getFilteredCategories, getToolsForCategory } from '../shared/utils';
|
import { getFilteredCategories, getToolsForCategory } from '../shared/utils';
|
||||||
|
|
||||||
// Zustand store
|
// Zustand stores
|
||||||
import { useDashboardStore } from '../../stores/dashboardStore';
|
import { useDashboardStore } from '../../stores/dashboardStore';
|
||||||
|
import { useWorkflowStore } from '../../stores/workflowStore';
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
import { toolCategories } from '../../data/toolCategories';
|
import { toolCategories } from '../../data/toolCategories';
|
||||||
@@ -34,7 +37,6 @@ import { toolCategories } from '../../data/toolCategories';
|
|||||||
// Main dashboard component
|
// Main dashboard component
|
||||||
const MainDashboard: React.FC = () => {
|
const MainDashboard: React.FC = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Zustand store hooks
|
// Zustand store hooks
|
||||||
@@ -50,13 +52,114 @@ const MainDashboard: React.FC = () => {
|
|||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
setSelectedCategory,
|
setSelectedCategory,
|
||||||
setSelectedSubCategory,
|
setSelectedSubCategory,
|
||||||
setError,
|
|
||||||
setLoading,
|
|
||||||
showSnackbar,
|
showSnackbar,
|
||||||
hideSnackbar,
|
hideSnackbar,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
} = useDashboardStore();
|
} = 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) => {
|
const handleToolClick = (tool: Tool) => {
|
||||||
console.log('Navigating to tool:', tool.path);
|
console.log('Navigating to tool:', tool.path);
|
||||||
if (tool.path) {
|
if (tool.path) {
|
||||||
@@ -120,12 +223,27 @@ const MainDashboard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{/* Dashboard Header */}
|
{/* Dashboard Header */}
|
||||||
<DashboardHeader
|
<DashboardHeader
|
||||||
title="🚀 Alwrity Content Hub"
|
title="Alwrity Content Hub"
|
||||||
subtitle="Your AI-powered content creation suite"
|
subtitle=""
|
||||||
statusChips={[]}
|
statusChips={[]}
|
||||||
rightContent={<SystemStatusIndicator />}
|
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 */}
|
{/* Search and Filter */}
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -149,12 +267,14 @@ const MainDashboard: React.FC = () => {
|
|||||||
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
|
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
|
||||||
>
|
>
|
||||||
<Box sx={{ mb: 5 }}>
|
<Box sx={{ mb: 5 }}>
|
||||||
{/* Category Header */}
|
{/* Only show Category Header when no specific category is selected (showing all tools) */}
|
||||||
|
{selectedCategory === null && (
|
||||||
<CategoryHeader
|
<CategoryHeader
|
||||||
categoryName={categoryName}
|
categoryName={categoryName}
|
||||||
category={category}
|
category={category}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
|
{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 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 { ShimmerHeader } from './styled';
|
||||||
import { DashboardHeaderProps } from './types';
|
import { DashboardHeaderProps } from './types';
|
||||||
|
|
||||||
@@ -7,17 +8,33 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
statusChips = [],
|
statusChips = [],
|
||||||
rightContent
|
rightContent,
|
||||||
|
customIcon,
|
||||||
|
workflowControls
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ShimmerHeader sx={{ mb: 5 }}>
|
<ShimmerHeader sx={{ mb: 2 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
<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>
|
<Box>
|
||||||
<Typography variant="h2" component="h1" sx={{
|
<Typography variant="h2" component="h1" sx={{
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||||
mb: 1,
|
mb: subtitle ? 1 : 0,
|
||||||
fontSize: { xs: '2rem', md: '3rem' },
|
fontSize: { xs: '2rem', md: '3rem' },
|
||||||
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
|
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
|
||||||
backgroundClip: 'text',
|
backgroundClip: 'text',
|
||||||
@@ -26,6 +43,7 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{subtitle && (
|
||||||
<Typography variant="h5" sx={{
|
<Typography variant="h5" sx={{
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
color: 'rgba(255, 255, 255, 0.9)',
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
@@ -33,6 +51,183 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Typography>
|
</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>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
{statusChips.length > 0 && (
|
{statusChips.length > 0 && (
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
toolCategories,
|
toolCategories,
|
||||||
theme
|
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 (
|
return (
|
||||||
<SearchContainer>
|
<SearchContainer>
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
|
||||||
@@ -74,13 +83,14 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Enhanced Category Filter */}
|
{/* Enhanced Category Filter with Tool Count Badges */}
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
<CategoryChip
|
<CategoryChip
|
||||||
label="All Tools"
|
label="All Tools"
|
||||||
onClick={() => onCategoryChange(null)}
|
onClick={() => onCategoryChange(null)}
|
||||||
active={selectedCategory === null}
|
active={selectedCategory === null}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
|
||||||
/>
|
/>
|
||||||
{Object.keys(toolCategories).map((category) => (
|
{Object.keys(toolCategories).map((category) => (
|
||||||
<CategoryChip
|
<CategoryChip
|
||||||
@@ -89,6 +99,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
onClick={() => onCategoryChange(category)}
|
onClick={() => onCategoryChange(category)}
|
||||||
active={selectedCategory === category}
|
active={selectedCategory === category}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
toolCount={getToolCount(toolCategories[category])}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -96,15 +96,24 @@ export const SearchContainer = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const CategoryChip = styled(Chip, {
|
export const CategoryChip = styled(Chip, {
|
||||||
shouldForwardProp: (prop) => prop !== 'active',
|
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'toolCount',
|
||||||
})<{ active?: boolean }>(({ theme, active }) => ({
|
})<{ active?: boolean; toolCount?: number }>(({ theme, active, toolCount }) => ({
|
||||||
background: active ? 'rgba(255, 255, 255, 0.25)' : 'rgba(255, 255, 255, 0.1)',
|
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',
|
color: 'white',
|
||||||
fontWeight: 600,
|
fontWeight: active ? 700 : 600,
|
||||||
fontSize: '0.9rem',
|
fontSize: '0.9rem',
|
||||||
padding: theme.spacing(1, 2),
|
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)',
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
position: 'relative',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'rgba(255, 255, 255, 0.25)',
|
background: 'rgba(255, 255, 255, 0.25)',
|
||||||
transform: 'translateY(-2px)',
|
transform: 'translateY(-2px)',
|
||||||
@@ -112,7 +121,29 @@ export const CategoryChip = styled(Chip, {
|
|||||||
},
|
},
|
||||||
'& .MuiChip-label': {
|
'& .MuiChip-label': {
|
||||||
padding: theme.spacing(0.5, 1),
|
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 }) => ({
|
export const EnhancedGlassCard = styled(GlassCard)(({ theme }) => ({
|
||||||
|
|||||||
@@ -77,13 +77,24 @@ export interface SearchFilterProps {
|
|||||||
|
|
||||||
export interface DashboardHeaderProps {
|
export interface DashboardHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle?: string;
|
||||||
statusChips?: Array<{
|
statusChips?: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
}>;
|
}>;
|
||||||
rightContent?: React.ReactNode;
|
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 {
|
export interface LoadingSkeletonProps {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import {
|
import {
|
||||||
Article as ArticleIcon,
|
Article as ArticleIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
TrendingUp as TrendingUpIcon,
|
|
||||||
Campaign as CampaignIcon,
|
Campaign as CampaignIcon,
|
||||||
Analytics as AnalyticsIcon,
|
Analytics as AnalyticsIcon,
|
||||||
Psychology as PsychologyIcon,
|
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