fix: PrimaryButton ref warning + research modal close race condition
This commit is contained in:
46
.planning/PROJECT.md
Normal file
46
.planning/PROJECT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# ALwrity Project
|
||||
|
||||
## What This Is
|
||||
ALwrity is an AI-powered content creation platform that helps users generate various types of content including podcasts, videos, blogs, and social media content. The platform features a React frontend and a FastAPI backend with onboarding workflows, API key management, and content generation capabilities.
|
||||
|
||||
## Core Value
|
||||
To provide an all-in-one AI content creation suite that simplifies the content production process for creators, marketers, and businesses.
|
||||
|
||||
## Current Focus
|
||||
Based on recent git commits, the team has been working on:
|
||||
- Podcast production features (voice cloning, avatar generation, B-roll integration)
|
||||
- Onboarding flow improvements
|
||||
- Backend stability and debugging
|
||||
- Frontend UI/UX enhancements
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
- User authentication (Clerk)
|
||||
- API key management for AI providers
|
||||
- Basic podcast generation workflow
|
||||
- File storage and media handling
|
||||
|
||||
### Active
|
||||
- Podcast script generation and editing
|
||||
- Voice cloning and avatar creation
|
||||
- B-roll scene rendering and integration
|
||||
- Onboarding flow completion tracking
|
||||
- API endpoint stability and debugging
|
||||
|
||||
### Out of Scope
|
||||
- Mobile applications (currently web-only)
|
||||
- Enterprise team collaboration features
|
||||
- Advanced analytics dashboard
|
||||
|
||||
## Key Decisions
|
||||
- Using FastAPI for backend performance
|
||||
- React with Material-UI for frontend consistency
|
||||
- Modular API design for extensibility
|
||||
- Database-first approach for persistence
|
||||
|
||||
## Constraints
|
||||
- Must maintain backward compatibility with existing API
|
||||
- Deployment targets include both development and production environments
|
||||
- Must support multiple AI providers (OpenAI, HuggingFace, etc.)
|
||||
- Budget-conscious resource usage for AI API calls
|
||||
40
.planning/STATE.md
Normal file
40
.planning/STATE.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
**Core Value**: ALwrity is an AI-powered content creation platform that helps users generate various types of content including podcasts, videos, blogs, and social media content.
|
||||
|
||||
**Current Focus**: Based on recent development activity, the team is implementing Phase 2 of the WaveSpeed AI integration roadmap - Hyper-Personalization features for the Persona system, including voice training and avatar creation.
|
||||
|
||||
## Current Position
|
||||
**Phase**: 2 of 3 - Hyper-Personalization
|
||||
**Plan**: 3 of 5 - Persona Avatar Creation & Integration
|
||||
**Status**: In Progress - Working on avatar service implementation and frontend UI for avatar creation
|
||||
|
||||
## Progress
|
||||
Progress: [███████░░] 70%
|
||||
|
||||
## Recent Decisions
|
||||
1. **Avatar Service Architecture**: Decided to create a shared avatar service in backend/services/wavespeed/avatar/ for reuse across LinkedIn and Persona modules
|
||||
2. **UI Framework**: Continuing with Material-UI (MUI) for consistent avatar creation interface
|
||||
3. **Storage Strategy**: Using cloud storage for avatar assets with metadata tracking in PostgreSQL
|
||||
4. **Generation Queue**: Implementing asynchronous processing for avatar generation to prevent API timeouts
|
||||
|
||||
## Pending Todos
|
||||
- [ ] Complete avatar generation API endpoints
|
||||
- [ ] Implement avatar library management UI
|
||||
- [ ] Add avatar preview functionality
|
||||
- [ ] Create avatar upload/download capabilities
|
||||
- [ ] Integrate avatar selection into Persona dashboard
|
||||
- [ ] Add usage tracking and cost estimation for avatar generation
|
||||
- [ ] Write comprehensive tests for avatar service
|
||||
- [ ] Update documentation for avatar feature
|
||||
|
||||
## Blockers/Concerns
|
||||
- **WaveSpeed API Rate Limits**: Need to implement proper queuing and retry mechanisms
|
||||
- **Storage Costs**: Avatar storage could become expensive at scale - need to implement cleanup policies
|
||||
- **Generation Time**: Avatar generation can take 30-60 seconds - need to improve user experience during wait
|
||||
- **Quality Consistency**: Ensuring generated avatars maintain consistent quality across different inputs
|
||||
|
||||
Last session: 2026-04-21 07:02:08
|
||||
Stopped at: Session resumed, proceeding to discuss Phase 2 context
|
||||
Resume file: [updated if applicable]
|
||||
@@ -87,17 +87,34 @@ export const QuerySelection: React.FC<QuerySelectionProps> = ({
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const prevIsResearchingRef = useRef(isResearching);
|
||||
const modalCloseTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Close modal only when research actually completes (transitions from true to false)
|
||||
// Prevent closing while research is in progress
|
||||
useEffect(() => {
|
||||
// Clear any pending close timeout when research starts
|
||||
if (researchStarted && isResearching) {
|
||||
if (modalCloseTimeoutRef.current) {
|
||||
clearTimeout(modalCloseTimeoutRef.current);
|
||||
modalCloseTimeoutRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const wasResearching = prevIsResearchingRef.current;
|
||||
const nowNotResearching = !isResearching;
|
||||
|
||||
if (showResearchModal && researchStarted && wasResearching && nowNotResearching) {
|
||||
setTimeout(() => setShowResearchModal(false), 1000);
|
||||
modalCloseTimeoutRef.current = setTimeout(() => setShowResearchModal(false), 1000);
|
||||
}
|
||||
|
||||
prevIsResearchingRef.current = isResearching;
|
||||
|
||||
return () => {
|
||||
if (modalCloseTimeoutRef.current) {
|
||||
clearTimeout(modalCloseTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [isResearching, showResearchModal, researchStarted]);
|
||||
|
||||
// Progress message cycling
|
||||
|
||||
@@ -14,7 +14,7 @@ interface PrimaryButtonProps {
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
||||
export const PrimaryButton = React.forwardRef<HTMLButtonElement, PrimaryButtonProps>(({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
@@ -25,7 +25,7 @@ export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
||||
ariaLabel,
|
||||
sx,
|
||||
size = "medium",
|
||||
}) => {
|
||||
}, ref) => {
|
||||
const sizeStyles = {
|
||||
small: { px: 1.5, py: 0.5, fontSize: "0.75rem" },
|
||||
medium: { px: 3, py: 1, fontSize: "0.875rem" },
|
||||
@@ -34,6 +34,7 @@ export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="contained"
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
@@ -62,10 +63,12 @@ export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip title={tooltip} arrow>
|
||||
<span>{button}</span>
|
||||
{button}
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
PrimaryButton.displayName = "PrimaryButton";
|
||||
|
||||
|
||||
@@ -68,16 +68,16 @@ export const UsageLimitRings: React.FC<UsageLimitRingsProps> = ({
|
||||
{
|
||||
label: 'AI Calls',
|
||||
used: currentUsage.total_calls,
|
||||
limit: limits.limits.gemini_calls || limits.limits.openai_calls || 50,
|
||||
limit: limits.limits.ai_text_generation_calls || limits.limits.gemini_calls || limits.limits.openai_calls || 50,
|
||||
color: '#3b82f6',
|
||||
unlimited: false,
|
||||
unlimited: limits.limits.ai_text_generation_calls === 0 && limits.limits.gemini_calls === 0 && limits.limits.openai_calls === 0,
|
||||
},
|
||||
{
|
||||
label: 'Images',
|
||||
used: imageCalls,
|
||||
limit: limits.limits.stability_calls || 50,
|
||||
color: '#a855f7',
|
||||
unlimited: false,
|
||||
unlimited: limits.limits.stability_calls === 0,
|
||||
},
|
||||
{
|
||||
label: 'Videos',
|
||||
@@ -85,6 +85,13 @@ export const UsageLimitRings: React.FC<UsageLimitRingsProps> = ({
|
||||
limit: limits.limits.video_calls,
|
||||
color: '#ec4899',
|
||||
unlimited: limits.limits.video_calls === 0,
|
||||
},
|
||||
{
|
||||
label: 'Audio',
|
||||
used: currentUsage.provider_breakdown?.audio?.calls ?? 0,
|
||||
limit: limits.limits.audio_calls,
|
||||
color: '#22c55e',
|
||||
unlimited: limits.limits.audio_calls === 0,
|
||||
}
|
||||
].filter(item => item.unlimited || item.limit > 0);
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ interface UsageStats {
|
||||
|
||||
interface UsageLimits {
|
||||
limits: {
|
||||
ai_text_generation_calls: number;
|
||||
gemini_calls: number;
|
||||
openai_calls: number;
|
||||
anthropic_calls: number;
|
||||
@@ -51,8 +52,12 @@ interface UsageLimits {
|
||||
tavily_calls: number;
|
||||
serper_calls: number;
|
||||
metaphor_calls: number;
|
||||
exa_calls: number;
|
||||
firecrawl_calls: number;
|
||||
stability_calls: number;
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
monthly_cost: number;
|
||||
};
|
||||
}
|
||||
@@ -169,11 +174,11 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
};
|
||||
|
||||
const getUsageColor = (current: number, max: number) => {
|
||||
if (max === 0) return '#757575';
|
||||
if (max === 0) return '#9ca3af';
|
||||
const percentage = (current / max) * 100;
|
||||
if (percentage >= 100) return '#d32f2f'; // error
|
||||
if (percentage >= 80) return '#ed6c02'; // warning
|
||||
return '#2e7d32'; // success
|
||||
if (percentage >= 100) return '#dc2626';
|
||||
if (percentage >= 80) return '#ea580c';
|
||||
return '#16a34a';
|
||||
};
|
||||
|
||||
const getProviderDisplayName = (provider: string) => {
|
||||
@@ -237,6 +242,35 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
const monthlyLimit = dashboardData?.limits?.limits?.monthly_cost || 0;
|
||||
const usagePercentage = monthlyLimit > 0 ? (totalCost / monthlyLimit) * 100 : 0;
|
||||
|
||||
// Build per-category usage summaries from provider_breakdown and limits
|
||||
const providerBreakdown = usageData.provider_breakdown || {};
|
||||
const providerLimits = dashboardData?.limits?.limits || {};
|
||||
|
||||
// Aggregate AI text calls (gemini + openai + anthropic + mistral)
|
||||
const aiCalls = (providerBreakdown.gemini?.calls || 0) + (providerBreakdown.openai?.calls || 0) + (providerBreakdown.anthropic?.calls || 0) + (providerBreakdown.mistral?.calls || 0) + (providerBreakdown.huggingface?.calls || 0) + (providerBreakdown.wavespeed?.calls || 0);
|
||||
const aiCallLimit = providerLimits.ai_text_generation_calls || providerLimits.gemini_calls || 0;
|
||||
|
||||
// Image calls (stability + wavespeed image)
|
||||
const imageCalls = (providerBreakdown.stability?.calls || 0) + (providerBreakdown.image_edit?.calls || 0);
|
||||
const imageCallLimit = providerLimits.stability_calls || 0;
|
||||
|
||||
// Audio calls
|
||||
const audioCalls = providerBreakdown.audio?.calls || 0;
|
||||
const audioCallLimit = providerLimits.audio_calls || 0;
|
||||
|
||||
// Video calls
|
||||
const videoCalls = providerBreakdown.video?.calls || 0;
|
||||
const videoCallLimit = providerLimits.video_calls || 0;
|
||||
|
||||
// Research calls (exa + tavily + serper + firecrawl)
|
||||
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);
|
||||
|
||||
const formatLimit = (used: number, limit: number) => {
|
||||
if (limit === 0) return `${used} / ∞`;
|
||||
return `${used} / ${limit}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Priority 2 Alert Banner (Usage limits) */}
|
||||
@@ -261,10 +295,10 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
sx={{
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
color: '#374151',
|
||||
'& .MuiSelect-select': { py: 0.5 }
|
||||
}}
|
||||
IconComponent={() => <CalendarMonth sx={{ fontSize: 16, color: 'action.active', ml: 0.5 }} />}
|
||||
IconComponent={() => <CalendarMonth sx={{ fontSize: 16, color: '#6b7280', ml: 0.5 }} />}
|
||||
>
|
||||
{availablePeriods.map((period) => (
|
||||
<MenuItem key={period} value={period} dense>
|
||||
@@ -295,8 +329,8 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
bgcolor: `${getUsageColor(totalCost, monthlyLimit)}20`,
|
||||
borderColor: getUsageColor(totalCost, monthlyLimit),
|
||||
bgcolor: `${getUsageColor(totalCost, monthlyLimit)}10`,
|
||||
borderColor: `${getUsageColor(totalCost, monthlyLimit)}60`,
|
||||
color: getUsageColor(totalCost, monthlyLimit),
|
||||
fontWeight: 600,
|
||||
'& .MuiChip-icon': {
|
||||
@@ -315,14 +349,14 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
width: 40,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
bgcolor: 'rgba(0,0,0,0.1)',
|
||||
bgcolor: '#e5e7eb',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
bgcolor: getUsageColor(totalCost, monthlyLimit),
|
||||
borderRadius: 3
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 600, color: '#374151' }}>
|
||||
{usagePercentage.toFixed(0)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -335,7 +369,8 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
disabled={loading}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.04)' }
|
||||
color: '#6b7280',
|
||||
'&:hover': { bgcolor: '#f3f4f6' }
|
||||
}}
|
||||
>
|
||||
<Refresh sx={{ fontSize: 16 }} />
|
||||
@@ -349,12 +384,93 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.04)' }
|
||||
color: '#6b7280',
|
||||
'&:hover': { bgcolor: '#f3f4f6' }
|
||||
}}
|
||||
>
|
||||
<MoreVert sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Per-Provider Usage Breakdown */}
|
||||
<Box sx={{ mt: 1.5, display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
{aiCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>AI Calls</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={aiCallLimit > 0 ? Math.min((aiCalls / aiCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(aiCalls, aiCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(aiCalls, aiCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(aiCalls, aiCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{imageCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Images</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={imageCallLimit > 0 ? Math.min((imageCalls / imageCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(imageCalls, imageCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(imageCalls, imageCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(imageCalls, imageCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{audioCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Audio</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={audioCallLimit > 0 ? Math.min((audioCalls / audioCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(audioCalls, audioCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(audioCalls, audioCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(audioCalls, audioCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{videoCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Video</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={videoCallLimit > 0 ? Math.min((videoCalls / videoCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(videoCalls, videoCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(videoCalls, videoCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(videoCalls, videoCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{researchCallLimit > 0 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Research</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={researchCallLimit > 0 ? Math.min((researchCalls / researchCallLimit) * 100, 100) : 0}
|
||||
sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(researchCalls, researchCallLimit), borderRadius: 2 } }}
|
||||
/>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(researchCalls, researchCallLimit), minWidth: 55, textAlign: 'right' }}>
|
||||
{formatLimit(researchCalls, researchCallLimit)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -362,24 +478,31 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: '#ffffff',
|
||||
border: '1px solid rgba(0,0,0,0.08)',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleViewFullDashboard}>
|
||||
<MenuItem onClick={handleViewFullDashboard} sx={{ color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
|
||||
<Dashboard sx={{ mr: 1, fontSize: 18 }} />
|
||||
View Full Dashboard
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleRefresh}>
|
||||
<MenuItem onClick={handleRefresh} sx={{ color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
|
||||
<Refresh sx={{ mr: 1, fontSize: 18 }} />
|
||||
Refresh Data
|
||||
</MenuItem>
|
||||
{lastUpdated && (
|
||||
<Box sx={{ px: 2, py: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<Typography variant="caption" sx={{ color: '#9ca3af' }}>
|
||||
Last updated: {lastUpdated.toLocaleTimeString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,54 +172,60 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 320,
|
||||
maxWidth: 400,
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto'
|
||||
minWidth: 340,
|
||||
maxWidth: 420,
|
||||
maxHeight: '85vh',
|
||||
overflow: 'auto',
|
||||
bgcolor: '#ffffff',
|
||||
border: '1px solid rgba(0,0,0,0.08)',
|
||||
borderRadius: 3,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 2, py: 1, borderBottom: '1px solid rgba(0,0,0,0.1)' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
{/* User Info Header */}
|
||||
<Box sx={{ px: 2.5, py: 2, bgcolor: '#f8f9fb', borderBottom: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: '#1a1a2e', fontSize: '0.9rem' }}>
|
||||
{user?.fullName || user?.username || 'User'}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<Typography variant="caption" sx={{ color: '#6b7280', fontSize: '0.75rem' }}>
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Subscription Info in Menu */}
|
||||
<Box sx={{ px: 2, py: 1.5, bgcolor: 'rgba(0,0,0,0.02)' }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
||||
{/* Subscription Info */}
|
||||
<Box sx={{ px: 2.5, py: 1.5, bgcolor: '#f8f9fb' }}>
|
||||
<Typography variant="caption" sx={{ display: 'block', mb: 0.5, fontWeight: 600, color: '#6b7280', fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
Current Plan
|
||||
</Typography>
|
||||
<Chip
|
||||
label={getPlanLabel()}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: `${getPlanColor()}20`,
|
||||
border: `1px solid ${getPlanColor()}`,
|
||||
bgcolor: `${getPlanColor()}15`,
|
||||
border: `1.5px solid ${getPlanColor()}40`,
|
||||
color: getPlanColor(),
|
||||
fontWeight: 700,
|
||||
fontSize: '0.75rem',
|
||||
height: 26,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
|
||||
{/* System Status Indicator */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
px: 2.5,
|
||||
py: 1.5,
|
||||
bgcolor: 'rgba(0,0,0,0.02)',
|
||||
bgcolor: '#f8f9fb',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1, fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ display: 'block', mb: 1, fontWeight: 600, color: '#6b7280', fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
System Health
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', '& > *': { transform: 'scale(0.85)' } }}>
|
||||
@@ -227,33 +233,33 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
|
||||
{/* Usage Dashboard */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
px: 2.5,
|
||||
py: 1.5,
|
||||
bgcolor: 'rgba(0,0,0,0.02)',
|
||||
bgcolor: '#ffffff',
|
||||
maxWidth: '100%',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1, fontWeight: 600 }}>
|
||||
<Typography variant="caption" sx={{ display: 'block', mb: 1, fontWeight: 600, color: '#6b7280', fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
Usage Statistics
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
<UsageDashboard compact={true} />
|
||||
</Box>
|
||||
<UsageDashboard compact={true} />
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
|
||||
<MenuItem onClick={() => { handleClose(); window.location.href = '/pricing'; }}>
|
||||
<MenuItem onClick={() => { handleClose(); window.location.href = '/pricing'; }} sx={{ mx: 1, borderRadius: 1, color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
|
||||
Manage Subscription
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleSignOut}>Sign out</MenuItem>
|
||||
<MenuItem onClick={handleSignOut} sx={{ mx: 1, borderRadius: 1, color: '#6b7280', '&:hover': { bgcolor: '#fef2f2', color: '#ef4444' } }}>
|
||||
Sign out
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { saveNavigationState, getCurrentPhaseForTool } from '../utils/navigation
|
||||
import { showSubscriptionExpiredToast, showUsageLimitToast, showSubscriptionToast } from '../utils/toastNotifications';
|
||||
|
||||
export interface SubscriptionLimits {
|
||||
ai_text_generation_calls: number;
|
||||
gemini_calls: number;
|
||||
openai_calls: number;
|
||||
anthropic_calls: number;
|
||||
@@ -17,8 +18,12 @@ export interface SubscriptionLimits {
|
||||
tavily_calls: number;
|
||||
serper_calls: number;
|
||||
metaphor_calls: number;
|
||||
exa_calls: number;
|
||||
firecrawl_calls: number;
|
||||
stability_calls: number;
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
monthly_cost: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,16 @@ export const useSubscriptionGuard = (options: SubscriptionGuardOptions = {}) =>
|
||||
return subscription.limits.firecrawl_calls;
|
||||
case 'stability_calls':
|
||||
return subscription.limits.stability_calls;
|
||||
case 'video_calls':
|
||||
return subscription.limits.video_calls || 0;
|
||||
case 'image_edit_calls':
|
||||
return subscription.limits.image_edit_calls || 0;
|
||||
case 'audio_calls':
|
||||
return subscription.limits.audio_calls || 0;
|
||||
case 'ai_text_generation_calls':
|
||||
return subscription.limits.ai_text_generation_calls || 0;
|
||||
case 'exa_calls':
|
||||
return subscription.limits.exa_calls || 0;
|
||||
case 'monthly_cost':
|
||||
return subscription.limits.monthly_cost;
|
||||
default:
|
||||
|
||||
@@ -147,6 +147,7 @@ const defaultLimits = {
|
||||
plan_name: 'Unknown Plan',
|
||||
tier: 'free' as const,
|
||||
limits: {
|
||||
ai_text_generation_calls: 0,
|
||||
gemini_calls: 0,
|
||||
openai_calls: 0,
|
||||
anthropic_calls: 0,
|
||||
@@ -154,8 +155,12 @@ const defaultLimits = {
|
||||
tavily_calls: 0,
|
||||
serper_calls: 0,
|
||||
metaphor_calls: 0,
|
||||
exa_calls: 0,
|
||||
firecrawl_calls: 0,
|
||||
stability_calls: 0,
|
||||
video_calls: 0,
|
||||
image_edit_calls: 0,
|
||||
audio_calls: 0,
|
||||
gemini_tokens: 0,
|
||||
openai_tokens: 0,
|
||||
anthropic_tokens: 0,
|
||||
@@ -192,6 +197,7 @@ function coerceUsageStats(raw: any): UsageStats {
|
||||
plan_name: raw?.limits?.plan_name ?? 'free',
|
||||
tier: raw?.limits?.tier ?? 'free',
|
||||
limits: {
|
||||
ai_text_generation_calls: raw?.limits?.limits?.ai_text_generation_calls ?? 0,
|
||||
gemini_calls: raw?.limits?.limits?.gemini_calls ?? 0,
|
||||
openai_calls: raw?.limits?.limits?.openai_calls ?? 0,
|
||||
anthropic_calls: raw?.limits?.limits?.anthropic_calls ?? 0,
|
||||
@@ -199,10 +205,12 @@ function coerceUsageStats(raw: any): UsageStats {
|
||||
tavily_calls: raw?.limits?.limits?.tavily_calls ?? 0,
|
||||
serper_calls: raw?.limits?.limits?.serper_calls ?? 0,
|
||||
metaphor_calls: raw?.limits?.limits?.metaphor_calls ?? 0,
|
||||
exa_calls: raw?.limits?.limits?.exa_calls ?? 0,
|
||||
firecrawl_calls: raw?.limits?.limits?.firecrawl_calls ?? 0,
|
||||
stability_calls: raw?.limits?.limits?.stability_calls ?? 0,
|
||||
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,
|
||||
gemini_tokens: raw?.limits?.limits?.gemini_tokens ?? 0,
|
||||
openai_tokens: raw?.limits?.limits?.openai_tokens ?? 0,
|
||||
anthropic_tokens: raw?.limits?.limits?.anthropic_tokens ?? 0,
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface SubscriptionLimits {
|
||||
plan_name: string;
|
||||
tier: 'free' | 'basic' | 'pro' | 'enterprise';
|
||||
limits: {
|
||||
ai_text_generation_calls: number;
|
||||
gemini_calls: number;
|
||||
openai_calls: number;
|
||||
anthropic_calls: number;
|
||||
@@ -56,10 +57,12 @@ export interface SubscriptionLimits {
|
||||
tavily_calls: number;
|
||||
serper_calls: number;
|
||||
metaphor_calls: number;
|
||||
exa_calls: number;
|
||||
firecrawl_calls: number;
|
||||
stability_calls: number;
|
||||
video_calls: number;
|
||||
image_edit_calls: number;
|
||||
audio_calls: number;
|
||||
gemini_tokens: number;
|
||||
openai_tokens: number;
|
||||
anthropic_tokens: number;
|
||||
@@ -207,6 +210,7 @@ export const SubscriptionLimitsSchema = z.object({
|
||||
plan_name: z.string(),
|
||||
tier: z.enum(['free', 'basic', 'pro', 'enterprise']),
|
||||
limits: z.object({
|
||||
ai_text_generation_calls: z.number().optional().default(0),
|
||||
gemini_calls: z.number(),
|
||||
openai_calls: z.number(),
|
||||
anthropic_calls: z.number(),
|
||||
@@ -214,10 +218,12 @@ export const SubscriptionLimitsSchema = z.object({
|
||||
tavily_calls: z.number(),
|
||||
serper_calls: z.number(),
|
||||
metaphor_calls: z.number(),
|
||||
exa_calls: z.number().optional().default(0),
|
||||
firecrawl_calls: z.number(),
|
||||
stability_calls: z.number(),
|
||||
video_calls: z.number().optional().default(0),
|
||||
image_edit_calls: z.number().optional().default(0),
|
||||
audio_calls: z.number().optional().default(0),
|
||||
gemini_tokens: z.number(),
|
||||
openai_tokens: z.number(),
|
||||
anthropic_tokens: z.number(),
|
||||
|
||||
46
temp_state.md
Normal file
46
temp_state.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Session Continuity
|
||||
|
||||
Last session:
|
||||
Stopped at: Session resumed, proceeding to discuss Phase 2 context
|
||||
Resume file: [updated if applicable]
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
**Core Value**: ALwrity is an AI-powered content creation platform that helps users generate various types of content including podcasts, videos, blogs, and social media content.
|
||||
|
||||
**Current Focus**: Based on recent development activity, the team is implementing Phase 2 of the WaveSpeed AI integration roadmap - Hyper-Personalization features for the Persona system, including voice training and avatar creation.
|
||||
|
||||
## Current Position
|
||||
**Phase**: 2 of 3 - Hyper-Personalization
|
||||
**Plan**: 3 of 5 - Persona Avatar Creation & Integration
|
||||
**Status**: In Progress - Working on avatar service implementation and frontend UI for avatar creation
|
||||
|
||||
## Progress
|
||||
Progress: [███████░░] 70%
|
||||
|
||||
## Recent Decisions
|
||||
1. **Avatar Service Architecture**: Decided to create a shared avatar service in backend/services/wavespeed/avatar/ for reuse across LinkedIn and Persona modules
|
||||
2. **UI Framework**: Continuing with Material-UI (MUI) for consistent avatar creation interface
|
||||
3. **Storage Strategy**: Using cloud storage for avatar assets with metadata tracking in PostgreSQL
|
||||
4. **Generation Queue**: Implementing asynchronous processing for avatar generation to prevent API timeouts
|
||||
|
||||
## Pending Todos
|
||||
- [ ] Complete avatar generation API endpoints
|
||||
- [ ] Implement avatar library management UI
|
||||
- [ ] Add avatar preview functionality
|
||||
- [ ] Create avatar upload/download capabilities
|
||||
- [ ] Integrate avatar selection into Persona dashboard
|
||||
- [ ] Add usage tracking and cost estimation for avatar generation
|
||||
- [ ] Write comprehensive tests for avatar service
|
||||
- [ ] Update documentation for avatar feature
|
||||
|
||||
## Blockers/Concerns
|
||||
- **WaveSpeed API Rate Limits**: Need to implement proper queuing and retry mechanisms
|
||||
- **Storage Costs**: Avatar storage could become expensive at scale - need to implement cleanup policies
|
||||
- **Generation Time**: Avatar generation can take 30-60 seconds - need to improve user experience during wait
|
||||
- **Quality Consistency**: Ensuring generated avatars maintain consistent quality across different inputs
|
||||
|
||||
## Session Continuity
|
||||
Last session: 2026-04-21 06:57:00
|
||||
Stopped at: Session resumed, proceeding to planning Phase 2 avatar creation work
|
||||
Resume file: .planning/phases/02-persona-hyper-personalization/03-avatar-creation/CONTINUE-HERE.md
|
||||
Reference in New Issue
Block a user