/** * Priority 2 Alert System Hook * * Integrates Priority 2 features from cost transparency review as alerts: * - Dynamic Pricing Display alerts (pricing changes, OSS model recommendations) * - Cost Estimation Before Operations alerts (high-cost operation warnings) * - Historical Cost Trends alerts (spending velocity, projection warnings) */ import { useState, useEffect, useCallback } from 'react'; import { billingService } from '../services/billingService'; import { DashboardData } from '../types/billing'; import { showToastNotification } from '../utils/toastNotifications'; export interface Priority2Alert { id: string; type: 'pricing_change' | 'cost_estimation' | 'cost_trend' | 'oss_recommendation'; severity: 'info' | 'warning' | 'error'; title: string; message: string; action?: { label: string; onClick: () => void; }; dismissible?: boolean; } interface UsePriority2AlertsOptions { userId?: string; enabled?: boolean; checkInterval?: number; // milliseconds } interface UsePriority2AlertsReturn { alerts: Priority2Alert[]; isLoading: boolean; refreshAlerts: () => Promise; dismissAlert: (alertId: string) => void; } const ALERT_CHECK_INTERVAL = 60000; // 1 minute default export const usePriority2Alerts = ( options: UsePriority2AlertsOptions = {} ): UsePriority2AlertsReturn => { const { userId, enabled = true, checkInterval = ALERT_CHECK_INTERVAL } = options; const [alerts, setAlerts] = useState([]); const [isLoading, setIsLoading] = useState(false); const [dashboardData, setDashboardData] = useState(null); const [lastCheck, setLastCheck] = useState(null); const generateAlerts = useCallback((data: DashboardData): Priority2Alert[] => { const generatedAlerts: Priority2Alert[] = []; const currentUsage = data.current_usage; const limits = data.limits; const projections = data.projections; if (!currentUsage || !limits) return generatedAlerts; // 1. Cost Trend Alerts (Priority 2: Historical Cost Trends) const costLimit = limits.limits?.monthly_cost || 0; const currentCost = currentUsage.total_cost || 0; const projectedCost = projections?.projected_monthly_cost || 0; const costUsagePercentage = costLimit > 0 ? (currentCost / costLimit) * 100 : 0; const projectedPercentage = costLimit > 0 ? (projectedCost / costLimit) * 100 : 0; // High spending velocity alert if (projectedPercentage > 120 && costUsagePercentage < 80) { const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); const currentDay = new Date().getDate(); const avgDailyCost = currentCost / currentDay; const daysUntilExhaustion = costLimit > avgDailyCost ? Math.ceil((costLimit - currentCost) / avgDailyCost) : 0; generatedAlerts.push({ id: 'cost-velocity-high', type: 'cost_trend', severity: 'warning', title: 'High Spending Velocity Detected', message: `Your current spending rate projects to ${projectedCost.toFixed(2)} this month (${projectedPercentage.toFixed(0)}% of limit). At this rate, you'll exhaust your budget in ~${daysUntilExhaustion} days.`, action: { label: 'View Cost Trends', onClick: () => { // Navigate to billing dashboard window.location.href = '/billing'; } }, dismissible: true }); } // Cost projection warning if (projectedPercentage >= 95 && costUsagePercentage < 95) { generatedAlerts.push({ id: 'cost-projection-critical', type: 'cost_trend', severity: 'error', title: 'Critical: Budget Exhaustion Projected', message: `Based on current spending, you're projected to exceed your $${costLimit.toFixed(2)} monthly budget. Current: $${currentCost.toFixed(2)} (${costUsagePercentage.toFixed(0)}%), Projected: $${projectedCost.toFixed(2)} (${projectedPercentage.toFixed(0)}%)`, action: { label: 'Upgrade Plan', onClick: () => { window.location.href = '/subscription'; } }, dismissible: false }); } // 2. OSS Model Recommendation Alerts (Priority 2: Dynamic Pricing Display) // Check if user is using expensive models when cheaper OSS alternatives exist const providerBreakdown = currentUsage.provider_breakdown || {}; // ProviderBreakdown type may not include all providers, so use type assertion for dynamic access const stabilityCost = (providerBreakdown as any).stability?.cost || 0; const stabilityCalls = (providerBreakdown as any).stability?.calls || 0; // If using Stability AI for images, recommend OSS alternative if (stabilityCalls > 10 && stabilityCost > 0.5) { const ossSavings = (stabilityCost * 0.25).toFixed(2); // 25% savings with OSS generatedAlerts.push({ id: 'oss-image-recommendation', type: 'oss_recommendation', severity: 'info', title: '💡 Cost Savings Opportunity', message: `You've spent $${stabilityCost.toFixed(2)} on image generation. Switch to Qwen Image OSS model to save ~$${ossSavings} (25% cheaper at $0.03/image vs $0.04/image).`, action: { label: 'Learn More', onClick: () => { // Could open a modal or navigate to pricing page showToastNotification('OSS models are automatically used as defaults in Basic tier', 'info'); } }, dismissible: true }); } // 3. Cost Estimation Alerts (Priority 2: Cost Estimation Before Operations) // These are generated contextually when operations are about to be performed // This hook provides the infrastructure, but alerts are triggered by components return generatedAlerts; }, []); const refreshAlerts = useCallback(async () => { if (!enabled || !userId) return; try { setIsLoading(true); const data = await billingService.getDashboardData(userId); setDashboardData(data); const newAlerts = generateAlerts(data); setAlerts(newAlerts); setLastCheck(new Date()); // Show toast for critical alerts const criticalAlerts = newAlerts.filter(a => a.severity === 'error'); if (criticalAlerts.length > 0) { criticalAlerts.forEach(alert => { showToastNotification(alert.message, 'error', { duration: 8000 }); }); } } catch (error) { console.error('[Priority2Alerts] Error refreshing alerts:', error); } finally { setIsLoading(false); } }, [enabled, userId, generateAlerts]); // Initial load useEffect(() => { if (enabled && userId) { refreshAlerts(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, userId]); // Only run on mount or when enabled/userId changes // Periodic refresh useEffect(() => { if (!enabled || !userId) return; const interval = setInterval(() => { refreshAlerts(); }, checkInterval); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, userId, checkInterval]); const dismissAlert = useCallback((alertId: string) => { setAlerts(prev => prev.filter(alert => { if (alert.id === alertId && alert.dismissible) { // Store dismissed alert ID in localStorage to prevent re-showing const dismissed = JSON.parse(localStorage.getItem('dismissedPriority2Alerts') || '[]'); dismissed.push(alertId); localStorage.setItem('dismissedPriority2Alerts', JSON.stringify(dismissed)); return false; } return true; })); }, []); // Filter out dismissed alerts useEffect(() => { const dismissed = JSON.parse(localStorage.getItem('dismissedPriority2Alerts') || '[]'); setAlerts(prev => prev.filter(alert => !dismissed.includes(alert.id))); }, []); return { alerts, isLoading, refreshAlerts, dismissAlert }; }; /** * Hook for generating cost estimation alerts before operations * Used by components to show warnings before expensive operations */ export const useCostEstimationAlert = () => { const showEstimationAlert = useCallback(( estimatedCost: number, operationType: string, onProceed: () => void, onCancel: () => void ) => { const costLimit = 45; // Basic tier limit - could be fetched from subscription const costPercentage = (estimatedCost / costLimit) * 100; if (estimatedCost > 1.0 || costPercentage > 5) { // High-cost operation warning const severity = costPercentage > 10 ? 'error' : 'warning'; const message = `This ${operationType} will cost approximately $${estimatedCost.toFixed(4)}. ` + `This represents ${costPercentage.toFixed(1)}% of your monthly budget.`; showToastNotification(message, severity, { duration: 10000 }); // Note: Toast doesn't support actions - user can proceed via the operation button } }, []); return { showEstimationAlert }; };