Save local changes (GSC/Bing integrations) before merging PR #354
This commit is contained in:
@@ -35,6 +35,8 @@ export interface ImagePreset {
|
||||
style: ImageStyle;
|
||||
renderingSpeed: RenderingSpeed;
|
||||
aspectRatio: AspectRatio;
|
||||
model?: ImageModel;
|
||||
image?: string; // Path to example image
|
||||
}
|
||||
|
||||
// Model option for the model selector
|
||||
|
||||
@@ -99,6 +99,60 @@ export const PODCAST_THEME: ImageModalTheme = {
|
||||
warningAccent: '#f59e0b',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Brand Avatar Presets
|
||||
// ============================================
|
||||
|
||||
export const BRAND_AVATAR_PRESETS: ImagePreset[] = [
|
||||
{
|
||||
key: 'professionalHeadshot',
|
||||
title: 'Professional Headshot',
|
||||
subtitle: 'Clean, corporate-ready professional portrait',
|
||||
prompt: 'Professional business headshot, confident expression, soft studio lighting, neutral background, sharp focus, high resolution, corporate attire, trustworthy demeanor',
|
||||
style: 'Realistic',
|
||||
renderingSpeed: 'Quality',
|
||||
aspectRatio: '1:1',
|
||||
image: '/assets/examples/professional_headshot.png',
|
||||
},
|
||||
{
|
||||
key: 'creativeMascot',
|
||||
title: 'Creative Mascot',
|
||||
subtitle: 'Stylized 3D character for brand identity',
|
||||
prompt: '3D character mascot, friendly and approachable, vibrant brand colors, soft rendering, pixar-style, expressive features, clean background, memorable design',
|
||||
style: 'Fiction',
|
||||
renderingSpeed: 'Quality',
|
||||
aspectRatio: '1:1',
|
||||
image: '/assets/examples/creative_mascot.png',
|
||||
},
|
||||
{
|
||||
key: 'techVisionary',
|
||||
title: 'Tech Visionary',
|
||||
subtitle: 'Modern, forward-looking tech aesthetic',
|
||||
prompt: 'Modern tech entrepreneur, futuristic lighting, smart casual attire, innovative atmosphere, clean tech background, confident gaze, professional but approachable',
|
||||
style: 'Realistic',
|
||||
renderingSpeed: 'Quality',
|
||||
aspectRatio: '1:1',
|
||||
image: '/assets/examples/tech_visionary.png',
|
||||
},
|
||||
{
|
||||
key: 'artisticPortrait',
|
||||
title: 'Artistic Portrait',
|
||||
subtitle: 'Unique, hand-drawn or painted style avatar',
|
||||
prompt: 'Digital art portrait, expressive brushstrokes, unique artistic style, vibrant color palette, creative composition, abstract background elements, distinct personality',
|
||||
style: 'Fiction',
|
||||
renderingSpeed: 'Quality',
|
||||
aspectRatio: '1:1',
|
||||
image: '/assets/examples/artistic_portrait.png',
|
||||
},
|
||||
];
|
||||
|
||||
export const BRAND_AVATAR_THEME: ImageModalTheme = {
|
||||
dialogBackground: 'rgba(20, 20, 30, 0.98)',
|
||||
primaryAccent: '#7C3AED', // Violet
|
||||
secondaryAccent: '#EC4899', // Pink
|
||||
warningAccent: '#F59E0B',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// YouTube-specific Recommendations
|
||||
// ============================================
|
||||
@@ -145,3 +199,24 @@ export const PODCAST_RECOMMENDATIONS: CustomRecommendations = {
|
||||
</>,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Brand Avatar-specific Recommendations
|
||||
// ============================================
|
||||
|
||||
export const BRAND_AVATAR_RECOMMENDATIONS: CustomRecommendations = {
|
||||
style: <>
|
||||
<strong>Realistic:</strong> Best for professional personal brands and executive headshots.<br />
|
||||
<strong>Fiction:</strong> Ideal for creative agencies, gaming brands, or friendly mascots.
|
||||
</>,
|
||||
speed: <>
|
||||
<strong>Quality:</strong> Recommended for avatars as they are long-term brand assets.<br />
|
||||
<strong>Turbo:</strong> Good for exploring concepts quickly.
|
||||
</>,
|
||||
aspectRatio: <>
|
||||
<strong>1:1 (Square)</strong> is the standard for profile pictures across all social platforms (LinkedIn, Twitter, Instagram).
|
||||
</>,
|
||||
model: <>
|
||||
<strong>Ideogram V3 Turbo:</strong> Superior text rendering and photorealism (Recommended).<br />
|
||||
<strong>Qwen Image:</strong> Fast and cost-effective for iterations.
|
||||
</>,
|
||||
};
|
||||
|
||||
@@ -109,9 +109,14 @@ export const OperationButton: React.FC<OperationButtonProps> = ({
|
||||
|
||||
// Format cost as currency
|
||||
const formattedCost = useMemo(() => {
|
||||
if (!showCost || estimatedCost === 0) {
|
||||
// Show cost even if 0 if explicitly requested, but usually 0 means "free" or "included"
|
||||
// The issue reported is showing "0" when it should show real cost
|
||||
// If estimatedCost is undefined, don't show
|
||||
if (!showCost || estimatedCost === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Always format, even if 0
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
|
||||
@@ -96,7 +96,6 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
|
||||
// Method to force refresh (bypass cache)
|
||||
const forceRefresh = useCallback(async () => {
|
||||
console.log('🔄 PlatformAnalytics: Force refresh requested');
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -107,9 +106,8 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
// Reload data
|
||||
await loadData();
|
||||
|
||||
console.log('✅ PlatformAnalytics: Force refresh completed');
|
||||
} catch (err) {
|
||||
console.error('❌ PlatformAnalytics: Force refresh failed:', err);
|
||||
console.error('PlatformAnalytics: Force refresh failed:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to refresh data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
@@ -9,18 +9,17 @@ import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
Refresh,
|
||||
MoreVert,
|
||||
Dashboard
|
||||
} from '@mui/icons-material';
|
||||
import { useUser } from '@clerk/clerk-react';
|
||||
import { apiClient } from '../../api/client';
|
||||
import { useSubscription } from '../../contexts/SubscriptionContext';
|
||||
import { usePriority2Alerts } from '../../hooks/usePriority2Alerts';
|
||||
@@ -84,7 +83,8 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
|
||||
const userId = localStorage.getItem('user_id');
|
||||
const { user } = useUser();
|
||||
const userId = localStorage.getItem('user_id') || user?.id;
|
||||
|
||||
// Priority 2 Alerts - automatically appears in all tool headers
|
||||
const { alerts: priority2Alerts, dismissAlert: dismissPriority2Alert } = usePriority2Alerts({
|
||||
@@ -93,7 +93,7 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
checkInterval: 120000, // Check every 2 minutes
|
||||
});
|
||||
|
||||
const fetchUsageData = async () => {
|
||||
const fetchUsageData = useCallback(async () => {
|
||||
if (!userId) return;
|
||||
|
||||
setLoading(true);
|
||||
@@ -109,11 +109,23 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsageData();
|
||||
}, [userId]);
|
||||
|
||||
// Listen for custom event to refresh usage data
|
||||
const handleUsageRefresh = () => {
|
||||
console.log('UsageDashboard: Refreshing usage data due to event');
|
||||
fetchUsageData();
|
||||
};
|
||||
|
||||
window.addEventListener('alwrity:refresh-usage', handleUsageRefresh);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('alwrity:refresh-usage', handleUsageRefresh);
|
||||
};
|
||||
}, [fetchUsageData, userId]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchUsageData();
|
||||
@@ -159,12 +171,13 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
'serper': 'Serper',
|
||||
'metaphor': 'Metaphor',
|
||||
'firecrawl': 'Firecrawl',
|
||||
'stability': 'Stability'
|
||||
'stability': 'Stability',
|
||||
'wavespeed': 'WaveSpeed'
|
||||
};
|
||||
return names[provider] || provider;
|
||||
};
|
||||
|
||||
if (!subscription || !dashboardData) {
|
||||
if (!dashboardData) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
@@ -184,7 +197,13 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <Box />; // Return empty box instead of null
|
||||
// If no data and not loading/error, try to fetch again or show placeholder
|
||||
if (userId && !dashboardData) {
|
||||
// Optional: could auto-trigger another fetch here if needed, but useEffect handles it
|
||||
return <Box />;
|
||||
}
|
||||
|
||||
return <Box />;
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
|
||||
@@ -34,14 +34,14 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
setSystemStatus(result.data.status || 'unknown');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching system status:', err);
|
||||
// Silently fail for system status to avoid console noise
|
||||
setSystemStatus('unknown');
|
||||
}
|
||||
};
|
||||
|
||||
fetchSystemStatus();
|
||||
// Refresh every 30 seconds
|
||||
const interval = setInterval(fetchSystemStatus, 30000);
|
||||
// Refresh every 120 seconds (2 minutes) to reduce load and avoid timeouts
|
||||
const interval = setInterval(fetchSystemStatus, 120000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
|
||||
214
frontend/src/components/shared/VideoGenerationLoader.tsx
Normal file
214
frontend/src/components/shared/VideoGenerationLoader.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Stack,
|
||||
Fade,
|
||||
useTheme,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MovieFilter,
|
||||
Campaign,
|
||||
CurrencyExchange,
|
||||
RecordVoiceOver,
|
||||
AutoAwesome
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface TipSlide {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const EDUCATIONAL_SLIDES: TipSlide[] = [
|
||||
{
|
||||
title: "Powering Your Creativity",
|
||||
description: "ALwrity integrates state-of-the-art video models like HunyuanVideo, LTX-2 Pro, and WAN 2.5. Create cinematic scenes from simple text or images.",
|
||||
icon: <MovieFilter fontSize="large" />,
|
||||
tags: ["Hunyuan", "LTX-2 Pro", "WAN 2.5"]
|
||||
},
|
||||
{
|
||||
title: "Beyond Talking Heads",
|
||||
description: "Visit the Creative Scenes Studio to generate b-roll, product showcases, and dynamic social media clips. Perfect for filling gaps in your narrative.",
|
||||
icon: <AutoAwesome fontSize="large" />,
|
||||
tags: ["B-Roll", "Product Video", "Social Clips"]
|
||||
},
|
||||
{
|
||||
title: "Multi-Platform Ready",
|
||||
description: "Generate vertical videos for TikTok, Reels, and Shorts, or horizontal formats for YouTube and LinkedIn. One idea, everywhere.",
|
||||
icon: <Campaign fontSize="large" />,
|
||||
tags: ["9:16 Vertical", "16:9 Horizontal"]
|
||||
},
|
||||
{
|
||||
title: "Studio Quality, Micro Cost",
|
||||
description: "Skip the expensive equipment, actors, and studio time. Create professional marketing assets for cents, not thousands.",
|
||||
icon: <CurrencyExchange fontSize="large" />,
|
||||
tags: ["Cost Effective", "High ROI"]
|
||||
},
|
||||
{
|
||||
title: "Your Voice, Scaled",
|
||||
description: "Clone your voice once and generate unlimited audio content. Perfect for podcasts, voiceovers, and consistent brand messaging.",
|
||||
icon: <RecordVoiceOver fontSize="large" />,
|
||||
tags: ["Voice Cloning", "Podcasts"]
|
||||
}
|
||||
];
|
||||
|
||||
export const VideoGenerationLoader: React.FC = () => {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveSlide((prev) => (prev + 1) % EDUCATIONAL_SLIDES.length);
|
||||
}, 6000); // Rotate every 6 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const slide = EDUCATIONAL_SLIDES[activeSlide];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: 400,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#f8fafc',
|
||||
borderRadius: 3,
|
||||
border: '1px solid #e2e8f0',
|
||||
p: 4
|
||||
}}
|
||||
>
|
||||
{/* Background decoration */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -100,
|
||||
right: -100,
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(37,99,235,0.05) 0%, rgba(255,255,255,0) 70%)',
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -50,
|
||||
left: -50,
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(236,72,153,0.05) 0%, rgba(255,255,255,0) 70%)',
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack spacing={4} alignItems="center" sx={{ zIndex: 1, maxWidth: 600, width: '100%' }}>
|
||||
{/* Loading Indicator */}
|
||||
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
||||
<Typography variant="h6" fontWeight={700} color="primary" gutterBottom>
|
||||
Generating Your Video...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
This usually takes 2-4 minutes. While you wait, learn what else ALwrity can do.
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
bgcolor: '#eff6ff',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
borderRadius: 4,
|
||||
background: 'linear-gradient(90deg, #2563eb 0%, #7c3aed 100%)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Carousel Card */}
|
||||
<Fade in={true} key={activeSlide} timeout={800}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
bgcolor: 'white',
|
||||
borderRadius: 3,
|
||||
border: '1px solid #e2e8f0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#eff6ff',
|
||||
color: '#2563eb',
|
||||
mb: 2,
|
||||
boxShadow: '0 4px 6px -1px rgba(37, 99, 235, 0.1)'
|
||||
}}
|
||||
>
|
||||
{slide.icon}
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" fontWeight={800} color="#0f172a" gutterBottom>
|
||||
{slide.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3, lineHeight: 1.6 }}>
|
||||
{slide.description}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" justifyContent="center" gap={1}>
|
||||
{slide.tags?.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: '#f1f5f9',
|
||||
color: '#475569',
|
||||
fontWeight: 600,
|
||||
borderRadius: 1.5
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Fade>
|
||||
|
||||
{/* Progress Dots */}
|
||||
<Stack direction="row" spacing={1}>
|
||||
{EDUCATIONAL_SLIDES.map((_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: index === activeSlide ? '#2563eb' : '#cbd5e1',
|
||||
transition: 'all 0.3s ease',
|
||||
transform: index === activeSlide ? 'scale(1.2)' : 'scale(1)'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user