Fix voice clone NotSupportedError and improve subscription services

This commit is contained in:
ajaysi
2026-04-22 12:27:51 +05:30
parent 641143a7d6
commit cbd68fa43f
13 changed files with 221 additions and 72 deletions

View File

@@ -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

View File

@@ -24,6 +24,7 @@ export interface SubscriptionLimits {
video_calls: number;
image_edit_calls: number;
audio_calls: number;
wavespeed_calls: number;
monthly_cost: number;
}

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(),