feat: Implement Today's Workflow and Agent Huddle enhancements
This commit is contained in:
@@ -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 />
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user