feat: Implement Today's Workflow and Agent Huddle enhancements

This commit is contained in:
ajaysi
2026-03-01 20:15:31 +05:30
parent 62d9c2e836
commit f8f7ddeb2a
25 changed files with 1852 additions and 272 deletions

View File

@@ -21,6 +21,7 @@ import AnalyticsInsights from './components/AnalyticsInsights';
import ToolsModal from './components/ToolsModal';
import EnhancedBillingDashboard from '../billing/EnhancedBillingDashboard';
import CompactSidebar from './components/CompactSidebar';
import TeamHuddleWidget from './components/TeamHuddleWidget';
// Shared types and utilities
import { Tool } from '../shared/types';
@@ -346,7 +347,10 @@ const MainDashboard: React.FC = () => {
</Box>
{/* Area 3: Analytics and Billing */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Team Huddle Widget - New Addition */}
<TeamHuddleWidget />
{/* Analytics Insights - Good/Bad/Ugly */}
<AnalyticsInsights />

View File

@@ -22,7 +22,9 @@ import {
CheckCircle as CheckIcon,
PlayArrow as PlayIcon,
SkipNext as SkipIcon,
NavigateNext
NavigateNext,
Psychology as AgentIcon,
Lightbulb as ReasonIcon
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useWorkflowStore } from '../../../stores/workflowStore';
@@ -351,6 +353,35 @@ const EnhancedTodayModal: React.FC<EnhancedTodayModalProps> = ({
</Typography>
</Box>
</Box>
{/* Agent Reasoning Section */}
{task.metadata?.source_agent && (
<Box sx={{
mt: 1.5,
mb: 1.5,
p: 1.5,
bgcolor: '#f8f9fa',
borderRadius: 2,
border: '1px solid #e0e0e0',
display: 'flex',
alignItems: 'flex-start',
gap: 1.5
}}>
<AgentIcon sx={{ fontSize: 16, color: pillarColor, mt: 0.3 }} />
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: '#444' }}>
Suggested by {task.metadata.source_agent.replace('Agent', '')}
</Typography>
</Box>
{task.metadata.reasoning && (
<Typography variant="caption" sx={{ color: '#666', display: 'block', lineHeight: 1.4 }}>
"{task.metadata.reasoning}"
</Typography>
)}
</Box>
</Box>
)}
{/* Task Actions */}
<Box sx={{ display: 'flex', gap: 1.25, mt: 2 }}>

View File

@@ -0,0 +1,206 @@
import React from 'react';
import {
Box,
Paper,
Typography,
Avatar,
AvatarGroup,
Chip,
List,
ListItem,
ListItemAvatar,
ListItemText,
Divider,
IconButton,
Tooltip
} from '@mui/material';
import {
Psychology as StrategyIcon,
Article as ContentIcon,
Search as SeoIcon,
Campaign as SocialIcon,
CompareArrows as CompetitorIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon
} from '@mui/icons-material';
interface AgentStatus {
id: string;
name: string;
role: string;
status: 'active' | 'thinking' | 'idle' | 'offline';
current_activity: string;
icon: React.ElementType;
color: string;
}
// Mock data - In real implementation, this would come from a backend endpoint
// /api/agents/status or similar
const AGENT_TEAM: AgentStatus[] = [
{
id: 'strategy_architect',
name: 'Strategy Architect',
role: 'Team Lead',
status: 'active',
current_activity: 'Analyzing content pillar performance',
icon: StrategyIcon,
color: '#6366f1' // Indigo
},
{
id: 'content_strategist',
name: 'Content Strategist',
role: 'Creative',
status: 'thinking',
current_activity: 'Identifying semantic gaps in "AI Tools"',
icon: ContentIcon,
color: '#10b981' // Emerald
},
{
id: 'seo_specialist',
name: 'SEO Specialist',
role: 'Technical',
status: 'idle',
current_activity: 'Monitoring SERP rankings',
icon: SeoIcon,
color: '#f59e0b' // Amber
},
{
id: 'social_manager',
name: 'Social Manager',
role: 'Engagement',
status: 'idle',
current_activity: 'Waiting for new content to schedule',
icon: SocialIcon,
color: '#ec4899' // Pink
},
{
id: 'competitor_analyst',
name: 'Competitor Analyst',
role: 'Intelligence',
status: 'active',
current_activity: 'Scanning competitor X for new posts',
icon: CompetitorIcon,
color: '#ef4444' // Red
}
];
const TeamHuddleWidget: React.FC = () => {
return (
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
height: '100%',
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)'
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6" fontWeight={700} color="text.primary">
Team Huddle
</Typography>
<Chip
label="Live"
size="small"
color="success"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/>
</Box>
<Box>
<Tooltip title="Refresh Team Status">
<IconButton size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
<List disablePadding>
{AGENT_TEAM.map((agent, index) => (
<React.Fragment key={agent.id}>
{index > 0 && <Divider variant="inset" component="li" sx={{ my: 1, ml: 7 }} />}
<ListItem
alignItems="flex-start"
disableGutters
sx={{ py: 0.5 }}
secondaryAction={
<Tooltip title={agent.status}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor:
agent.status === 'active' ? '#22c55e' :
agent.status === 'thinking' ? '#3b82f6' :
'#94a3b8',
boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none',
animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none',
'@keyframes pulse': {
'0%': { opacity: 1, transform: 'scale(1)' },
'50%': { opacity: 0.6, transform: 'scale(1.2)' },
'100%': { opacity: 1, transform: 'scale(1)' }
}
}}
/>
</Tooltip>
}
>
<ListItemAvatar>
<Avatar
sx={{
bgcolor: `${agent.color}15`,
color: agent.color,
width: 40,
height: 40
}}
>
<agent.icon fontSize="small" />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle2" fontWeight={600}>
{agent.name}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem', border: '1px solid #e2e8f0', px: 0.5, borderRadius: 1 }}>
{agent.role}
</Typography>
</Box>
}
secondary={
<Typography
variant="body2"
color="text.secondary"
sx={{
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontSize: '0.75rem',
mt: 0.25
}}
>
{agent.current_activity}
</Typography>
}
/>
</ListItem>
</React.Fragment>
))}
</List>
<Box mt={2} pt={2} borderTop="1px solid #eee" display="flex" justifyContent="center">
<Typography variant="caption" color="primary" sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}>
View Full Team Activity
</Typography>
</Box>
</Paper>
);
};
export default TeamHuddleWidget;

View File

@@ -33,7 +33,8 @@ import {
Info as InfoIcon,
CheckCircle as CheckCircleIcon,
PriorityHigh as PriorityHighIcon,
Stars as StarsIcon
Stars as StarsIcon,
Face as AgentIcon
} from '@mui/icons-material';
// TypeScript interfaces for semantic insights
@@ -45,6 +46,7 @@ export interface ContentPillar {
key_topics: string[];
competitor_coverage: number;
user_coverage: number;
source_agent?: string; // Added for agent attribution
}
export interface SemanticGap {
@@ -53,6 +55,7 @@ export interface SemanticGap {
competitor_count: number;
opportunity_score: number;
suggested_content_ideas: string[];
source_agent?: string; // Added for agent attribution
}
export interface ThemeAnalysis {
@@ -271,6 +274,15 @@ const ContentPillarsSection: React.FC<{ pillars: ContentPillar[] }> = ({ pillars
Competitor Coverage: {Math.round(pillar.competitor_coverage * 100)}%
</Typography>
</Box>
{pillar.source_agent && (
<Box mt={2} pt={1} borderTop="1px solid #eee" display="flex" alignItems="center" gap={1}>
<AgentIcon fontSize="small" color="action" sx={{ width: 16, height: 16 }} />
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Identified by {pillar.source_agent}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
@@ -336,10 +348,19 @@ const SemanticGapsSection: React.FC<{ gaps: SemanticGap[] }> = ({ gaps }) => {
</Box>
)}
<Box mt={2}>
<Box mt={2} display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="caption" color="text.secondary">
Opportunity Score: {Math.round(gap.opportunity_score * 100)}%
</Typography>
{gap.source_agent && (
<Box display="flex" alignItems="center" gap={1}>
<AgentIcon fontSize="small" color="action" sx={{ width: 16, height: 16 }} />
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Spotted by {gap.source_agent}
</Typography>
</Box>
)}
</Box>
</AccordionDetails>
</Accordion>

View File

@@ -9,7 +9,11 @@ import {
IconButton,
Menu,
MenuItem,
LinearProgress
LinearProgress,
Select,
FormControl,
InputLabel,
SelectChangeEvent
} from '@mui/material';
import {
TrendingUp,
@@ -17,7 +21,8 @@ import {
CheckCircle,
Refresh,
MoreVert,
Dashboard
Dashboard,
CalendarMonth
} from '@mui/icons-material';
import { useUser } from '@clerk/clerk-react';
import { apiClient } from '../../api/client';
@@ -34,6 +39,7 @@ interface UsageStats {
tokens: number;
cost: number;
}>;
billing_period?: string;
}
interface UsageLimits {
@@ -65,6 +71,7 @@ interface DashboardData {
usage_status: string;
unread_alerts: number;
};
trends?: { periods: string[] };
}
interface UsageDashboardProps {
@@ -82,6 +89,8 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
const [error, setError] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<string>('');
const [availablePeriods, setAvailablePeriods] = useState<string[]>([]);
const { user } = useUser();
const userId = localStorage.getItem('user_id') || user?.id;
@@ -93,42 +102,57 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
checkInterval: 120000, // Check every 2 minutes
});
const fetchUsageData = useCallback(async () => {
const fetchUsageData = useCallback(async (period?: string) => {
if (!userId) return;
setLoading(true);
setError(null);
try {
const response = await apiClient.get(`/api/subscription/dashboard/${userId}`);
setDashboardData(response.data.data);
setLastUpdated(new Date());
} catch (err) {
const url = period
? `/api/subscription/dashboard/${userId}?billing_period=${period}`
: `/api/subscription/dashboard/${userId}`;
const response = await apiClient.get<any>(url);
if (response.data && response.data.success) {
setDashboardData(response.data.data);
setLastUpdated(new Date());
// Extract available periods from trends if not set
if (!period && response.data.data.trends?.periods) {
setAvailablePeriods(response.data.data.trends.periods);
// Set current period if not selected
if (!selectedPeriod) {
const current = new Date().toISOString().slice(0, 7); // YYYY-MM
setSelectedPeriod(current);
}
}
} else {
throw new Error(response.data?.error || 'Failed to fetch usage data');
}
} catch (err: any) {
console.error('Error fetching usage data:', err);
setError('Failed to load usage data');
setError(err.message || 'Failed to load usage statistics');
} finally {
setLoading(false);
}
}, [userId]);
const handlePeriodChange = (event: SelectChangeEvent) => {
const period = event.target.value;
setSelectedPeriod(period);
fetchUsageData(period);
};
useEffect(() => {
fetchUsageData();
// Listen for custom event to refresh usage data
const handleUsageRefresh = () => {
console.log('UsageDashboard: Refreshing usage data due to event');
// Initial fetch
if (userId) {
fetchUsageData();
};
window.addEventListener('alwrity:refresh-usage', handleUsageRefresh);
return () => {
window.removeEventListener('alwrity:refresh-usage', handleUsageRefresh);
};
}, [fetchUsageData, userId]);
}
}, [userId, fetchUsageData]); // Added fetchUsageData to deps since it's memoized
const handleRefresh = () => {
fetchUsageData();
fetchUsageData(selectedPeriod);
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
@@ -141,111 +165,125 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
const handleViewFullDashboard = () => {
handleMenuClose();
window.open('/billing', '_blank');
window.location.href = '/dashboard';
};
const getUsageColor = (used: number, limit: number) => {
const percentage = (used / limit) * 100;
if (percentage >= 90) return '#f44336'; // Red
if (percentage >= 75) return '#ff9800'; // Orange
if (percentage >= 50) return '#ffeb3b'; // Yellow
return '#4caf50'; // Green
};
const getUsageStatusIcon = (status: string) => {
switch (status) {
case 'active': return <CheckCircle sx={{ fontSize: 16, color: '#4caf50' }} />;
case 'warning': return <Warning sx={{ fontSize: 16, color: '#ff9800' }} />;
case 'limit_exceeded': return <Warning sx={{ fontSize: 16, color: '#f44336' }} />;
default: return <CheckCircle sx={{ fontSize: 16, color: '#4caf50' }} />;
}
const getUsageColor = (current: number, max: number) => {
if (max === 0) return '#757575';
const percentage = (current / max) * 100;
if (percentage >= 100) return '#d32f2f'; // error
if (percentage >= 80) return '#ed6c02'; // warning
return '#2e7d32'; // success
};
const getProviderDisplayName = (provider: string) => {
const names: Record<string, string> = {
'gemini': 'Gemini',
'openai': 'OpenAI',
'anthropic': 'Claude',
'mistral': 'Mistral',
'tavily': 'Tavily',
'serper': 'Serper',
'metaphor': 'Metaphor',
// Map internal provider names to display names
const displayNames: Record<string, string> = {
'gemini': 'Google Gemini',
'openai': 'OpenAI GPT-4',
'anthropic': 'Anthropic Claude',
'mistral': 'HuggingFace (Mistral)',
'tavily': 'Tavily Search',
'serper': 'Serper Google',
'metaphor': 'Exa Search', // Metaphor is now Exa
'exa': 'Exa Search',
'firecrawl': 'Firecrawl',
'stability': 'Stability',
'stability': 'Stability AI',
'video': 'Video Gen',
'audio': 'Audio Gen',
'image_edit': 'Image Edit',
'wavespeed': 'WaveSpeed'
};
return names[provider] || provider;
return displayNames[provider] || provider.charAt(0).toUpperCase() + provider.slice(1);
};
if (!dashboardData) {
if (loading) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="caption" color="text.secondary">
Loading usage...
</Typography>
</Box>
);
}
if (error) {
return (
<Alert severity="error" sx={{ py: 0.5 }}>
<Typography variant="caption">{error}</Typography>
</Alert>
);
}
// If no data and not loading/error, try to fetch again or show placeholder
if (userId && !dashboardData) {
// Optional: could auto-trigger another fetch here if needed, but useEffect handles it
return <Box />;
}
return <Box />;
if (!dashboardData && loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
<CircularProgress size={24} />
</Box>
);
}
if (error && !dashboardData) {
return (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
<IconButton size="small" onClick={() => fetchUsageData(selectedPeriod)}>
<Refresh fontSize="small" />
</IconButton>
</Alert>
);
}
if (!dashboardData) return null;
const currentUsage = dashboardData.current_usage;
const limits = dashboardData.limits;
if (compact) {
// Compact view - show key metrics as chips
// Use current_usage for accurate cost (properly coerced from provider breakdown)
// Fallback to summary if current_usage is not available
const totalCalls = dashboardData.current_usage?.total_calls ?? dashboardData.summary.total_api_calls_this_month;
const totalCost = dashboardData.current_usage?.total_cost ?? dashboardData.summary.total_cost_this_month ?? 0;
const monthlyLimit = dashboardData.limits.limits.monthly_cost;
const usageData = dashboardData?.current_usage || {
total_calls: dashboardData?.summary?.total_api_calls_this_month || 0,
total_cost: dashboardData?.summary?.total_cost_this_month || 0,
usage_status: dashboardData?.summary?.usage_status || 'active',
provider_breakdown: {}
};
const totalCalls = usageData.total_calls;
const totalCost = usageData.total_cost;
const monthlyLimit = dashboardData?.limits?.limits?.monthly_cost || 0;
const usagePercentage = monthlyLimit > 0 ? (totalCost / monthlyLimit) * 100 : 0;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Priority 2 Alerts - Shows cost trends, OSS recommendations, spending velocity */}
<Box sx={{ width: '100%' }}>
{/* Priority 2 Alert Banner (Usage limits) */}
{priority2Alerts.length > 0 && (
<Box sx={{ mb: 0.5 }}>
<Priority2AlertBanner
alerts={priority2Alerts}
onDismiss={dismissPriority2Alert}
maxAlerts={1} // Show only 1 alert in compact view
<Box sx={{ mb: 1 }}>
<Priority2AlertBanner
alerts={[priority2Alerts[0]]}
onDismiss={() => dismissPriority2Alert(priority2Alerts[0].id)}
/>
</Box>
)}
{/* Usage Statistics */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Total API Calls */}
<Tooltip title={`${totalCalls.toLocaleString()} API calls this month`}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
{/* Month Selector for Compact View */}
{availablePeriods.length > 1 && (
<FormControl variant="standard" size="small" sx={{ minWidth: 100, mr: 1 }}>
<Select
value={selectedPeriod}
onChange={handlePeriodChange}
disableUnderline
sx={{
fontSize: '0.875rem',
fontWeight: 500,
color: 'text.secondary',
'& .MuiSelect-select': { py: 0.5 }
}}
IconComponent={() => <CalendarMonth sx={{ fontSize: 16, color: 'action.active', ml: 0.5 }} />}
>
{availablePeriods.map((period) => (
<MenuItem key={period} value={period} dense>
{period}
</MenuItem>
))}
</Select>
</FormControl>
)}
{/* Status Chip */}
<Tooltip title={`Status: ${usageData.usage_status}`}>
<Chip
icon={getUsageStatusIcon(dashboardData.summary.usage_status)}
label={`${totalCalls.toLocaleString()}`}
icon={usageData.usage_status === 'active' ? <CheckCircle sx={{ fontSize: 14 }} /> : <Warning sx={{ fontSize: 14 }} />}
label={usageData.usage_status === 'limit_reached' ? 'Limit Reached' : 'Active'}
size="small"
color={usageData.usage_status === 'limit_reached' ? 'error' : usageData.usage_status === 'warning' ? 'warning' : 'success'}
variant="outlined"
sx={{
bgcolor: 'rgba(33, 150, 243, 0.1)',
borderColor: '#2196f3',
color: '#1976d2',
fontWeight: 600,
'& .MuiChip-icon': {
color: '#2196f3'
}
}}
sx={{ fontWeight: 600 }}
/>
</Tooltip>
@@ -347,11 +385,38 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
}
// Full dashboard view (for dedicated usage page)
const usageData = dashboardData?.current_usage || {
total_calls: dashboardData?.summary?.total_api_calls_this_month || 0,
total_cost: dashboardData?.summary?.total_cost_this_month || 0,
provider_breakdown: {}
};
return (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Usage Dashboard
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
Usage Dashboard
</Typography>
{/* Month Selector for Full View */}
{availablePeriods.length > 1 && (
<FormControl variant="outlined" size="small" sx={{ minWidth: 150 }}>
<InputLabel>Billing Period</InputLabel>
<Select
value={selectedPeriod}
onChange={handlePeriodChange}
label="Billing Period"
startAdornment={<CalendarMonth sx={{ fontSize: 18, mr: 1, color: 'action.active' }} />}
>
{availablePeriods.map((period) => (
<MenuItem key={period} value={period}>
{period}
</MenuItem>
))}
</Select>
</FormControl>
)}
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 2 }}>
{/* Total Calls */}
@@ -360,7 +425,7 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
Total API Calls
</Typography>
<Typography variant="h4" color="primary">
{(dashboardData.current_usage?.total_calls ?? dashboardData.summary.total_api_calls_this_month).toLocaleString()}
{usageData.total_calls.toLocaleString()}
</Typography>
</Box>
@@ -370,10 +435,10 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
Monthly Cost
</Typography>
<Typography variant="h4" color="secondary">
${(dashboardData.current_usage?.total_cost ?? dashboardData.summary.total_cost_this_month ?? 0).toFixed(2)}
${usageData.total_cost.toFixed(2)}
</Typography>
<Typography variant="caption" color="text.secondary">
of ${dashboardData.limits.limits.monthly_cost} limit
of ${dashboardData?.limits?.limits?.monthly_cost || 0} limit
</Typography>
</Box>
@@ -382,16 +447,21 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Usage by Provider
</Typography>
{Object.entries(dashboardData.current_usage.provider_breakdown).map(([provider, stats]) => (
{Object.entries(usageData.provider_breakdown || {}).map(([provider, stats]) => (
<Box key={provider} sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2">
{getProviderDisplayName(provider)}
</Typography>
<Typography variant="body2" fontWeight={600}>
{stats.calls.toLocaleString()}
{(stats as any).calls?.toLocaleString() || 0}
</Typography>
</Box>
))}
{Object.keys(usageData.provider_breakdown || {}).length === 0 && (
<Typography variant="body2" color="text.secondary" fontStyle="italic">
No usage this period
</Typography>
)}
</Box>
</Box>
</Box>