Files
ALwrity/frontend/src/hooks/usePriority2Alerts.ts

252 lines
8.9 KiB
TypeScript

/**
* 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<void>;
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<Priority2Alert[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [lastCheck, setLastCheck] = useState<Date | null>(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 };
};