Fix voice clone NotSupportedError and improve subscription services
This commit is contained in:
@@ -58,6 +58,7 @@ interface UsageLimits {
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
wavespeed_calls: number;
|
||||
monthly_cost: number;
|
||||
};
|
||||
}
|
||||
@@ -107,10 +108,13 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
checkInterval: 120000, // Check every 2 minutes
|
||||
});
|
||||
|
||||
const fetchUsageData = useCallback(async (period?: string) => {
|
||||
const fetchUsageData = useCallback(async (period?: string, silent = false) => {
|
||||
if (!userId) return;
|
||||
|
||||
setLoading(true);
|
||||
// Don't block UI for silent background refreshes (menu open, visibility change)
|
||||
if (!silent) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
const url = period
|
||||
@@ -136,10 +140,14 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
throw new Error(response.data?.error || 'Failed to fetch usage data');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching usage data:', err);
|
||||
setError(err.message || 'Failed to load usage statistics');
|
||||
if (!silent) {
|
||||
console.error('Error fetching usage data:', err);
|
||||
setError(err.message || 'Failed to load usage statistics');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
@@ -154,13 +162,30 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
if (userId) {
|
||||
fetchUsageData();
|
||||
}
|
||||
}, [userId, fetchUsageData]); // Added fetchUsageData to deps since it's memoized
|
||||
}, [userId, fetchUsageData]);
|
||||
|
||||
// Refresh on visibility change (user returns to tab) - only if data is stale (>60s old)
|
||||
useEffect(() => {
|
||||
const STALE_THRESHOLD_MS = 60000; // 60 seconds
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && userId && lastUpdated) {
|
||||
const ageMs = Date.now() - lastUpdated.getTime();
|
||||
if (ageMs > STALE_THRESHOLD_MS) {
|
||||
fetchUsageData(selectedPeriod, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, [userId, fetchUsageData, selectedPeriod, lastUpdated]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchUsageData(selectedPeriod);
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
// Show cached data immediately, don't wait for fetch
|
||||
// Data will refresh when user clicks the manual refresh button
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
@@ -266,6 +291,10 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
const researchCalls = (providerBreakdown.exa?.calls || 0) + (providerBreakdown.tavily?.calls || 0) + (providerBreakdown.serper?.calls || 0) + (providerBreakdown.firecrawl?.calls || 0);
|
||||
const researchCallLimit = (providerLimits.exa_calls || 0) + (providerLimits.tavily_calls || 0) + (providerLimits.serper_calls || 0) + (providerLimits.firecrawl_calls || 0);
|
||||
|
||||
// WaveSpeed calls (all WaveSpeed API calls)
|
||||
const wavespeedCalls = providerBreakdown.wavespeed?.calls || 0;
|
||||
const wavespeedCallLimit = providerLimits.wavespeed_calls || 0;
|
||||
|
||||
const formatLimit = (used: number, limit: number) => {
|
||||
if (limit === 0) return `${used} / ∞`;
|
||||
return `${used} / ${limit}`;
|
||||
@@ -470,6 +499,21 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{wavespeedCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>WaveSpeed</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={wavespeedCallLimit > 0 ? Math.min((wavespeedCalls / wavespeedCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(wavespeedCalls, wavespeedCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(wavespeedCalls, wavespeedCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(wavespeedCalls, wavespeedCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface SubscriptionLimits {
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
wavespeed_calls: number;
|
||||
monthly_cost: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ export const useSubscriptionGuard = (options: SubscriptionGuardOptions = {}) =>
|
||||
return subscription.limits.ai_text_generation_calls || 0;
|
||||
case 'exa_calls':
|
||||
return subscription.limits.exa_calls || 0;
|
||||
case 'wavespeed_calls':
|
||||
return subscription.limits.wavespeed_calls || 0;
|
||||
case 'monthly_cost':
|
||||
return subscription.limits.monthly_cost;
|
||||
default:
|
||||
|
||||
@@ -161,6 +161,7 @@ const defaultLimits = {
|
||||
video_calls: 0,
|
||||
image_edit_calls: 0,
|
||||
audio_calls: 0,
|
||||
wavespeed_calls: 0,
|
||||
gemini_tokens: 0,
|
||||
openai_tokens: 0,
|
||||
anthropic_tokens: 0,
|
||||
@@ -211,6 +212,7 @@ function coerceUsageStats(raw: any): UsageStats {
|
||||
video_calls: raw?.limits?.limits?.video_calls ?? 0,
|
||||
image_edit_calls: raw?.limits?.limits?.image_edit_calls ?? 0,
|
||||
audio_calls: raw?.limits?.limits?.audio_calls ?? 0,
|
||||
wavespeed_calls: raw?.limits?.limits?.wavespeed_calls ?? 0,
|
||||
gemini_tokens: raw?.limits?.limits?.gemini_tokens ?? 0,
|
||||
openai_tokens: raw?.limits?.limits?.openai_tokens ?? 0,
|
||||
anthropic_tokens: raw?.limits?.limits?.anthropic_tokens ?? 0,
|
||||
|
||||
@@ -786,6 +786,14 @@ export const podcastApi = {
|
||||
seed?: number;
|
||||
maskImageUrl?: string;
|
||||
}): Promise<{ taskId: string; status: string; message: string }> {
|
||||
// Preflight check for video generation
|
||||
await ensurePreflight({
|
||||
provider: 'video',
|
||||
model: 'kling-v2.5-turbo-5s',
|
||||
operation_type: 'video_generation',
|
||||
actual_provider_name: 'wavespeed',
|
||||
});
|
||||
|
||||
const response = await aiApiClient.post("/api/podcast/render/video", {
|
||||
project_id: params.projectId,
|
||||
scene_id: params.sceneId,
|
||||
@@ -884,6 +892,14 @@ export const podcastApi = {
|
||||
cost: number;
|
||||
image_prompt?: string;
|
||||
}> {
|
||||
// Preflight check for image generation
|
||||
await ensurePreflight({
|
||||
provider: 'stability',
|
||||
model: 'stability-ai',
|
||||
operation_type: 'image_generation',
|
||||
actual_provider_name: 'wavespeed',
|
||||
});
|
||||
|
||||
const response = await aiApiClient.post("/api/podcast/image", {
|
||||
scene_id: params.sceneId,
|
||||
scene_title: params.sceneTitle,
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface SubscriptionLimits {
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
wavespeed_calls: number;
|
||||
gemini_tokens: number;
|
||||
openai_tokens: number;
|
||||
anthropic_tokens: number;
|
||||
@@ -224,6 +225,7 @@ export const SubscriptionLimitsSchema = z.object({
|
||||
video_calls: z.number().optional().default(0),
|
||||
image_edit_calls: z.number().optional().default(0),
|
||||
audio_calls: z.number().optional().default(0),
|
||||
wavespeed_calls: z.number().optional().default(0),
|
||||
gemini_tokens: z.number(),
|
||||
openai_tokens: z.number(),
|
||||
anthropic_tokens: z.number(),
|
||||
|
||||
Reference in New Issue
Block a user