252 lines
8.9 KiB
TypeScript
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 };
|
|
};
|