ALwrity + Wordpress + Wix + GSC integration

This commit is contained in:
ajaysi
2025-10-08 10:13:14 +05:30
parent 14dfb2e5c0
commit 3bab3450dc
147 changed files with 19815 additions and 17053 deletions

View File

@@ -13,6 +13,34 @@ import OptimizedImage from './OptimizedImage';
import { SignInButton } from '@clerk/clerk-react';
import { RocketLaunch } from '@mui/icons-material';
import { motion } from 'framer-motion';
import { ScrambleText } from '../ScrambleText';
// Scrambling text component for multiple phrases
const ScramblingText: React.FC<{ phrases: string[]; interval?: number; duration?: number; delay?: number }> = ({
phrases,
interval = 3500,
duration = 500,
delay = 300
}) => {
const [currentIndex, setCurrentIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % phrases.length);
}, interval);
return () => clearInterval(timer);
}, [phrases.length, interval]);
return (
<ScrambleText
text={phrases[currentIndex]}
duration={duration}
delay={delay}
restartInterval={interval}
as="span"
/>
);
};
const EnterpriseCTA: React.FC = () => {
const theme = useTheme();
@@ -111,7 +139,12 @@ const EnterpriseCTA: React.FC = () => {
transition: 'all 0.3s ease'
}}
>
Start Creating Now
<ScramblingText
phrases={['Start Creating Now', 'Begin Content Creation', 'Launch Your Content', 'Start Creating Today']}
duration={500}
delay={300}
interval={3500}
/>
</Button>
</SignInButton>

View File

@@ -20,37 +20,42 @@ import {
CloudDone,
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import { ScrambleText } from '../ScrambleText';
// Rotating text component
const RotatingText: React.FC<{ words: string[]; interval?: number }> = ({
words,
interval = 2000
// Scrambling text component with multiple phrases
const ScramblingText: React.FC<{ phrases: string[]; interval?: number; duration?: number; delay?: number }> = ({
phrases,
interval = 4000,
duration = 800,
delay = 200
}) => {
const [currentIndex, setCurrentIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % words.length);
setCurrentIndex((prev) => (prev + 1) % phrases.length);
}, interval);
return () => clearInterval(timer);
}, [words.length, interval]);
}, [phrases.length, interval]);
return (
<Box
component="span"
sx={{
<ScrambleText
text={phrases[currentIndex]}
duration={duration}
delay={delay}
restartInterval={interval}
as="span"
className="scramble-text"
style={{
color: '#fff',
fontWeight: 900,
// Strong text shadow for readability
textShadow: `
0 2px 10px rgba(0, 0, 0, 0.9),
0 4px 20px rgba(0, 0, 0, 0.7),
0 0 40px rgba(102, 126, 234, 0.4)
`,
}}
>
{words[currentIndex]}
</Box>
/>
);
};
@@ -195,8 +200,8 @@ const HeroSection: React.FC = () => {
}}
>
Enterprise AI for{' '}
<RotatingText
words={['Revenue Growth', 'Brand Automation', 'Content Strategy', 'Market Intelligence']}
<ScramblingText
phrases={['Content Planning', 'MultiModal Generation', 'Cross Platform Publishing', 'All-Analytics One-platform', 'Content Engagement', 'Content Remarketing']}
/>
</Typography>
@@ -299,7 +304,12 @@ const HeroSection: React.FC = () => {
},
}}
>
ALwrity For Free - BYOK
<ScramblingText
phrases={['ALwrity For Free - BYOK', 'Start Free Today', 'Try ALwrity Free', 'Get Started Free']}
duration={600}
delay={500}
interval={4000}
/>
</Button>
</SignInButton>

View File

@@ -24,6 +24,34 @@ import {
Speed
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import { ScrambleText } from '../ScrambleText';
// Scrambling text component for multiple phrases
const ScramblingText: React.FC<{ phrases: string[]; interval?: number; duration?: number; delay?: number }> = ({
phrases,
interval = 4000,
duration = 600,
delay = 0
}) => {
const [currentIndex, setCurrentIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % phrases.length);
}, interval);
return () => clearInterval(timer);
}, [phrases.length, interval]);
return (
<ScrambleText
text={phrases[currentIndex]}
duration={duration}
delay={delay}
restartInterval={interval}
as="span"
/>
);
};
const IntroducingAlwrity: React.FC = () => {
const theme = useTheme();
@@ -134,7 +162,12 @@ const IntroducingAlwrity: React.FC = () => {
<Stack spacing={6} alignItems="center" textAlign="center">
<motion.div variants={fadeInUp}>
<Typography variant="h3" fontWeight={700} sx={{ color: 'white' }}>
Introducing ALwrity
<ScramblingText
phrases={['Introducing ALwrity', 'Welcome to ALwrity', 'Meet ALwrity', 'Discover ALwrity']}
duration={600}
delay={1000}
interval={5000}
/>
</Typography>
</motion.div>
<motion.div variants={fadeInUp}>
@@ -166,7 +199,12 @@ const IntroducingAlwrity: React.FC = () => {
transition: 'all 0.3s ease'
}}
>
Start Your AI Journey
<ScramblingText
phrases={['Start Your AI Journey', 'Begin AI Transformation', 'Launch AI Success', 'Start AI Revolution']}
duration={500}
delay={300}
interval={3500}
/>
</Button>
</SignInButton>
</Box>

View File

@@ -16,12 +16,7 @@ import {
CircularProgress
} from '@mui/material';
import { keyframes } from '@mui/system';
import { SignInButton } from '@clerk/clerk-react';
import {
AutoAwesome,
Speed,
TrendingUp,
Security,
Analytics,
Psychology,
AccessTime,
@@ -37,6 +32,36 @@ import {
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import HeroSection from './HeroSection';
import { ScrambleText } from '../ScrambleText';
// Scrambling text component for multiple phrases
const ScramblingText: React.FC<{ phrases: string[]; interval?: number; duration?: number; delay?: number; style?: React.CSSProperties }> = ({
phrases,
interval = 3000,
duration = 400,
delay = 200,
style = {}
}) => {
const [currentIndex, setCurrentIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % phrases.length);
}, interval);
return () => clearInterval(timer);
}, [phrases.length, interval]);
return (
<ScrambleText
text={phrases[currentIndex]}
duration={duration}
delay={delay}
restartInterval={interval}
as="span"
style={style}
/>
);
};
// Lazy load components for better performance
const FeatureShowcase = lazy(() => import('./FeatureShowcase'));
@@ -51,29 +76,6 @@ const Landing: React.FC = () => {
usePerformanceMonitor('Landing');
// Optimized Framer Motion variants for better performance
const fadeInUp = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: "easeOut" as const,
// Use transform3d for hardware acceleration
transform: "translate3d(0,0,0)"
}
},
};
const stagger = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.08, // Reduced stagger time
delayChildren: 0.1
}
},
};
// Cinematic lifecycle section animations
const backgroundFade = {
@@ -206,49 +208,9 @@ const Landing: React.FC = () => {
];
const painPoints = [
{
icon: <AccessTime />,
title: 'Time Constraints',
description: 'Limited time for content creation and strategy development. Solopreneurs wear many hats and struggle to maintain consistent content output.'
},
{
icon: <TrendingDown />,
title: 'Lack of Expertise',
description: 'Not trained as content strategists, SEO experts, or data analysts. Missing the knowledge to create effective marketing campaigns.'
},
{
icon: <MonetizationOn />,
title: 'Resource Limitations',
description: 'Cannot afford full marketing teams or expensive enterprise tools. Need cost-effective solutions that deliver professional results.'
},
{
icon: <Analytics />,
title: 'Poor ROI Tracking',
description: 'Only 21% of marketers successfully track content ROI. Lack of data-driven insights to optimize marketing spend and strategy.'
},
{
icon: <Group />,
title: 'Manual Processes',
description: 'Overwhelmed by repetitive content creation tasks. Need automation to scale efforts without sacrificing quality.'
},
{
icon: <Psychology />,
title: 'Inconsistent Voice',
description: 'Struggle to maintain brand voice across platforms. Need personalized AI that understands your unique style and messaging.'
}
];
// Glassmorphism styles
const glassPanelSx = {
background: `linear-gradient(135deg, ${alpha(theme.palette.common.white, 0.06)} 0%, ${alpha(theme.palette.common.white, 0.02)} 100%)`,
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4,
boxShadow: '0 10px 30px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06)'
} as const;
const glassCardSx = {
background: `linear-gradient(135deg, ${alpha(theme.palette.common.white, 0.05)} 0%, ${alpha(theme.palette.common.white, 0.015)} 100%)`,
@@ -279,11 +241,6 @@ const Landing: React.FC = () => {
}
`;
// Slide in animation for lifecycle image
const slideIn = keyframes`
0% { opacity: 0; transform: scale(0.9) translateY(20px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
`;
// Loading component for Suspense
const LoadingSpinner = () => (
@@ -425,8 +382,15 @@ const Landing: React.FC = () => {
</Box>
{/* chips */}
<Grid container spacing={{ xs: 1, md: 2 }} justifyContent="space-between" alignItems="center">
{['Plan','Generate','Publish','Analyze','Engage','Remarket'].map((label, idx) => (
<Grid item key={label} xs={2} sx={{ display: 'flex', justifyContent: idx === 0 ? 'flex-start' : idx === 5 ? 'flex-end' : 'center' }}>
{[
{ label: 'Plan', variations: ['Plan', 'Strategy', 'Research', 'Blueprint'] },
{ label: 'Generate', variations: ['Generate', 'Create', 'Produce', 'Craft'] },
{ label: 'Publish', variations: ['Publish', 'Launch', 'Deploy', 'Release'] },
{ label: 'Analyze', variations: ['Analyze', 'Measure', 'Track', 'Monitor'] },
{ label: 'Engage', variations: ['Engage', 'Interact', 'Connect', 'Respond'] },
{ label: 'Remarket', variations: ['Remarket', 'Repurpose', 'Recycle', 'Amplify'] }
].map((item, idx) => (
<Grid item key={item.label} xs={2} sx={{ display: 'flex', justifyContent: idx === 0 ? 'flex-start' : idx === 5 ? 'flex-end' : 'center' }}>
<Chip
label={
<Stack direction="row" spacing={0.5} alignItems="center">
@@ -440,16 +404,17 @@ const Landing: React.FC = () => {
>
{idx+1}
</Typography>
<Typography
variant="caption"
sx={{
fontWeight: 700,
fontSize: { xs: '0.7rem', md: '0.8rem' },
<ScramblingText
phrases={item.variations}
duration={400}
delay={200}
interval={3000}
style={{
fontWeight: 700,
fontSize: '0.7rem',
color: 'white'
}}
>
{label}
</Typography>
/>
</Stack>
}
size="medium"

View File

@@ -17,6 +17,34 @@ import {
ArrowForward
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import { ScrambleText } from '../ScrambleText';
// Scrambling text component for multiple phrases
const ScramblingText: React.FC<{ phrases: string[]; interval?: number; duration?: number; delay?: number }> = ({
phrases,
interval = 4000,
duration = 600,
delay = 0
}) => {
const [currentIndex, setCurrentIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % phrases.length);
}, interval);
return () => clearInterval(timer);
}, [phrases.length, interval]);
return (
<ScrambleText
text={phrases[currentIndex]}
duration={duration}
delay={delay}
restartInterval={interval}
as="span"
/>
);
};
const SolopreneurDilemma: React.FC = () => {
const theme = useTheme();
@@ -25,16 +53,19 @@ const SolopreneurDilemma: React.FC = () => {
{
icon: <Psychology />,
title: "Content Overwhelm",
titleVariations: ["Content Overwhelm", "Content Chaos", "Content Confusion", "Content Crisis"],
description: "Managing 8+ social platforms with different audiences, tones, and posting schedules"
},
{
icon: <TrendingUp />,
title: "Inconsistent Brand Voice",
titleVariations: ["Inconsistent Brand Voice", "Voice Confusion", "Brand Inconsistency", "Tone Problems"],
description: "Struggling to maintain your unique voice across all platforms while scaling content"
},
{
icon: <Speed />,
title: "Time Drain",
titleVariations: ["Time Drain", "Time Sink", "Time Waste", "Productivity Loss"],
description: "Spending 4-6 hours daily on content creation, research, and platform management"
}
];
@@ -238,7 +269,12 @@ const SolopreneurDilemma: React.FC = () => {
textShadow: '0 1px 2px rgba(0, 0, 0, 0.7)'
}}
>
{point.title}
<ScramblingText
phrases={point.titleVariations || [point.title]}
duration={500}
delay={500}
interval={4000}
/>
</Typography>
<Typography
variant="body1"
@@ -375,7 +411,12 @@ const SolopreneurDilemma: React.FC = () => {
transition: 'all 0.3s ease',
}}
>
End the Struggle Today
<ScramblingText
phrases={['End the Struggle Today', 'Stop the Chaos', 'Solve Content Problems', 'Transform Your Workflow']}
duration={500}
delay={300}
interval={3500}
/>
</Button>
</motion.div>
</Stack>

View File

@@ -7,47 +7,21 @@ import {
Alert,
Button,
Grid,
Card,
CardContent,
CardActions,
Chip,
Avatar,
LinearProgress,
Dialog,
DialogTitle,
DialogContent
} from '@mui/material';
import {
Business as BusinessIcon,
Assessment as AssessmentIcon,
OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon,
Share as ShareIcon,
Facebook as FacebookIcon,
Instagram as InstagramIcon,
LinkedIn as LinkedInIcon,
YouTube as YouTubeIcon,
Twitter as TwitterIcon
Refresh as RefreshIcon
} from '@mui/icons-material';
import { aiApiClient } from '../../api/client'; // Use aiApiClient for long-running operations
import { useOnboardingStyles } from './common/useOnboardingStyles';
import { SocialMediaPresenceSection, CompetitorsGrid, SitemapAnalysisResults } from './WebsiteStep/components';
import type { Competitor } from './WebsiteStep/components';
import { ComingSoonSection } from './CompetitorAnalysisStep/ComingSoonSection';
interface Competitor {
url: string;
domain: string;
title: string;
summary: string;
relevance_score: number;
highlights?: string[];
competitive_insights: {
business_model: string;
target_audience: string;
};
content_insights: {
content_focus: string;
content_quality: string;
};
}
interface ResearchSummary {
total_competitors: number;
@@ -61,13 +35,16 @@ interface CompetitorAnalysisStepProps {
// sessionId removed - backend uses authenticated user from Clerk token
userUrl: string;
industryContext?: string;
// Expose data collection function for global Continue button
onDataReady?: (getData: () => any) => void;
}
const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
onContinue,
onBack,
userUrl,
industryContext
industryContext,
onDataReady
}) => {
const classes = useOnboardingStyles();
const [isAnalyzing, setIsAnalyzing] = useState(false);
@@ -83,6 +60,8 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
const [selectedCompetitorHighlights, setSelectedCompetitorHighlights] = useState<string[]>([]);
const [selectedCompetitorTitle, setSelectedCompetitorTitle] = useState<string>('');
const [usingCachedData, setUsingCachedData] = useState(false);
const [sitemapAnalysis, setSitemapAnalysis] = useState<any>(null);
const [isAnalyzingSitemap, setIsAnalyzingSitemap] = useState(false);
// Check for cached competitor analysis data
const loadCachedAnalysis = useCallback(() => {
@@ -112,6 +91,7 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
setSocialMediaAccounts(parsedData.social_media_accounts || {});
setSocialMediaCitations(parsedData.social_media_citations || []);
setResearchSummary(parsedData.research_summary || null);
setSitemapAnalysis(parsedData.sitemap_analysis || null);
setUsingCachedData(true);
return true; // Successfully loaded from cache
@@ -127,6 +107,22 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
}
}, [userUrl]);
// Update cache with sitemap analysis
const updateCacheWithSitemapAnalysis = useCallback((sitemapResult: any) => {
try {
const cachedData = localStorage.getItem('competitor_analysis_data');
if (cachedData) {
const parsedData = JSON.parse(cachedData);
parsedData.sitemap_analysis = sitemapResult;
localStorage.setItem('competitor_analysis_data', JSON.stringify(parsedData));
console.log('CompetitorAnalysisStep: Updated cache with sitemap analysis');
}
} catch (err) {
console.warn('Failed to update cache with sitemap analysis:', err);
}
}, []);
const startCompetitorDiscovery = useCallback(async (force = false) => {
// Check cache first unless forced
if (!force && loadCachedAnalysis()) {
@@ -194,7 +190,8 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
competitors: result.competitors || [],
social_media_accounts: result.social_media_accounts || {},
social_media_citations: result.social_media_citations || [],
research_summary: result.research_summary || null
research_summary: result.research_summary || null,
sitemap_analysis: null // Will be updated when sitemap analysis completes
};
setCompetitors(analysisData.competitors);
@@ -225,6 +222,46 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
}
}, [userUrl, industryContext, loadCachedAnalysis]); // sessionId removed from dependencies
// Sitemap Analysis Function
const startSitemapAnalysis = useCallback(async () => {
if (isAnalyzingSitemap) return;
setIsAnalyzingSitemap(true);
try {
const finalUserUrl = userUrl || localStorage.getItem('website_url') || '';
const competitorDomains = competitors.map(c => c.domain).filter(Boolean);
console.log('Starting sitemap analysis for:', finalUserUrl);
const response = await aiApiClient.post('/api/onboarding/step3/analyze-sitemap', {
user_url: finalUserUrl,
competitors: competitorDomains,
industry_context: industryContext,
analyze_content_trends: true,
analyze_publishing_patterns: true
});
const result = response.data;
if (result.success) {
console.log('Sitemap analysis completed successfully');
setSitemapAnalysis(result);
// Update cache with sitemap analysis
updateCacheWithSitemapAnalysis(result);
} else {
console.error('Sitemap analysis failed:', result.error);
setError(result.error || 'Sitemap analysis failed');
}
} catch (err) {
console.error('Sitemap analysis error:', err);
setError(err instanceof Error ? err.message : 'Sitemap analysis failed');
} finally {
setIsAnalyzingSitemap(false);
}
}, [userUrl, competitors, industryContext, isAnalyzingSitemap]);
// Initialize: Check cache first, then run analysis if needed
useEffect(() => {
const initialize = async () => {
@@ -241,17 +278,85 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Run only once on mount
const handleContinue = () => {
const researchData = {
// Auto-trigger sitemap analysis when competitors are loaded (only if not cached)
useEffect(() => {
if (competitors.length > 0 && !sitemapAnalysis && !isAnalyzingSitemap) {
// Check if sitemap analysis is already cached
const cachedData = localStorage.getItem('competitor_analysis_data');
if (cachedData) {
try {
const parsedData = JSON.parse(cachedData);
if (parsedData.sitemap_analysis) {
console.log('CompetitorAnalysisStep: Sitemap analysis already cached, skipping auto-trigger');
setSitemapAnalysis(parsedData.sitemap_analysis);
return;
}
} catch (err) {
console.warn('Error checking cached sitemap analysis:', err);
}
}
console.log('Competitors loaded, starting sitemap analysis...');
startSitemapAnalysis();
}
}, [competitors, sitemapAnalysis, isAnalyzingSitemap, startSitemapAnalysis]);
// Data collection function for global Continue button
const getResearchData = useCallback(() => {
return {
competitors,
researchSummary,
sitemapAnalysis,
userUrl,
industryContext,
analysisTimestamp: new Date().toISOString()
};
onContinue(researchData);
}, [competitors, researchSummary, sitemapAnalysis, userUrl, industryContext]);
const handleContinue = async () => {
// Save research preferences to backend before continuing
try {
const researchData = getResearchData();
// Extract research preferences for saving (use defaults if not available)
const researchPreferences = {
research_depth: 'Comprehensive',
content_types: ['blog_posts', 'social_media'],
auto_research: true,
factual_content: true
};
// Save research preferences to backend
await aiApiClient.post('/api/ai-research/configure-preferences', {
research_depth: researchPreferences.research_depth,
content_types: researchPreferences.content_types,
auto_research: researchPreferences.auto_research,
factual_content: researchPreferences.factual_content
});
console.log('Research preferences saved to backend');
} catch (error) {
console.error('Error saving research preferences:', error);
// Continue anyway - don't block user progress for save errors
}
// Continue with wizard navigation
onContinue(getResearchData());
};
// Expose data collection function to parent (only when onDataReady changes)
useEffect(() => {
if (onDataReady) {
console.log('CompetitorAnalysisStep: Exposing data collection function to parent');
// Always provide a data collection function, even if data is empty
const safeGetData = () => {
console.log('CompetitorAnalysisStep: getResearchData called');
return getResearchData();
};
onDataReady(safeGetData);
}
}, [onDataReady, getResearchData]); // Include getResearchData in dependencies
const handleShowHighlights = (competitor: Competitor) => {
setSelectedCompetitorHighlights(competitor.highlights || []);
setSelectedCompetitorTitle(competitor.title);
@@ -361,182 +466,53 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
)}
{/* Social Media Accounts Section */}
{Object.keys(socialMediaAccounts).length > 0 && (
<SocialMediaPresenceSection socialMediaAccounts={socialMediaAccounts} />
{/* Competitors Grid Section */}
<CompetitorsGrid
competitors={competitors}
onShowHighlights={handleShowHighlights}
/>
{/* Sitemap Analysis Section */}
{(sitemapAnalysis || isAnalyzingSitemap) && (
<>
<Typography
variant="h6"
gutterBottom
fontWeight={600}
mb={3}
sx={{ color: '#1a202c !important' }} // Force dark text
>
<ShareIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#667eea !important' }} />
Social Media Presence
</Typography>
<Grid container spacing={2} mb={4}>
{Object.entries(socialMediaAccounts).map(([platform, url]) => {
if (!url) return null;
const platformIcons: { [key: string]: React.ReactNode } = {
facebook: <FacebookIcon />,
instagram: <InstagramIcon />,
linkedin: <LinkedInIcon />,
youtube: <YouTubeIcon />,
twitter: <TwitterIcon />,
tiktok: <ShareIcon /> // Fallback icon for TikTok
};
return (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} key={platform}>
<Card sx={{
height: '100%',
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
boxShadow: '0 4px 12px rgba(3, 169, 244, 0.15)',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(3, 169, 244, 0.25)'
}
}}>
<CardContent>
<Box display="flex" alignItems="center" gap={2}>
<Avatar sx={{ width: 40, height: 40, bgcolor: 'primary.main' }}>
{platformIcons[platform] || <ShareIcon />}
</Avatar>
<Box flex={1}>
<Typography variant="h6" fontWeight={600} textTransform="capitalize">
{platform}
</Typography>
<Button
variant="text"
size="small"
href={url as string}
target="_blank"
rel="noopener noreferrer"
sx={{ p: 0, minWidth: 'auto', textTransform: 'none' }}
>
View Profile
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
);
})}
</Grid>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography
variant="h5"
fontWeight={600}
sx={{ color: '#1a202c !important' }}
>
Website Structure Analysis
</Typography>
{!isAnalyzingSitemap && (
<Button
variant="outlined"
size="small"
onClick={startSitemapAnalysis}
sx={{
borderColor: '#667eea',
color: '#667eea',
'&:hover': {
borderColor: '#5a6fd8',
backgroundColor: 'rgba(102, 126, 234, 0.04)'
}
}}
>
Re-analyze Structure
</Button>
)}
</Box>
<SitemapAnalysisResults
analysisData={sitemapAnalysis?.analysis_data || {}}
userUrl={userUrl || localStorage.getItem('website_url') || ''}
sitemapUrl={sitemapAnalysis?.sitemap_url || `${(userUrl || localStorage.getItem('website_url') || '').replace(/\/$/, '')}/sitemap.xml`}
isLoading={isAnalyzingSitemap}
discoveryMethod={sitemapAnalysis?.discovery_method}
/>
</>
)}
<Typography
variant="h6"
gutterBottom
fontWeight={600}
mb={3}
sx={{ color: '#1a202c !important' }} // Force dark text
>
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#667eea !important' }} />
Discovered Competitors ({competitors.length})
</Typography>
<Grid container spacing={3}>
{competitors.map((competitor, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} key={index}>
<Card sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
boxShadow: '0 4px 12px rgba(3, 169, 244, 0.15)',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(3, 169, 244, 0.25)'
}
}}>
<CardContent sx={{ flexGrow: 1 }}>
<Box display="flex" alignItems="flex-start" gap={2} mb={2}>
<Avatar sx={{ width: 40, height: 40 }}>
<BusinessIcon />
</Avatar>
<Box flex={1}>
<Typography
variant="h6"
fontWeight={600}
gutterBottom
sx={{ color: '#1a202c !important' }} // Force dark text for readability
>
{competitor.title}
</Typography>
<Typography
variant="body2"
gutterBottom
sx={{ color: '#4a5568 !important' }} // Force dark text for readability
>
{competitor.domain}
</Typography>
<Chip
label={`${Math.round(competitor.relevance_score * 100)}% Match`}
color="primary"
size="small"
/>
</Box>
</Box>
<Typography
variant="body2"
mb={2}
sx={{ color: '#2d3748 !important' }} // Force dark text for readability
>
{competitor.summary.length > 150
? `${competitor.summary.substring(0, 150)}...`
: competitor.summary
}
</Typography>
</CardContent>
<CardActions sx={{ p: 2, pt: 0 }}>
<Button
size="small"
startIcon={<OpenInNewIcon />}
onClick={() => window.open(competitor.url, '_blank')}
>
Visit Website
</Button>
{competitor.highlights && competitor.highlights.length > 0 && (
<Button
size="small"
variant="outlined"
onClick={() => handleShowHighlights(competitor)}
>
Highlights
</Button>
)}
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Box display="flex" justifyContent="center" mt={4}>
<Button
variant="contained"
size="large"
onClick={handleContinue}
sx={{
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
borderRadius: 2
}}
>
Continue to Next Step
</Button>
</Box>
</Box>
)}
@@ -627,6 +603,9 @@ const CompetitorAnalysisStep: React.FC<CompetitorAnalysisStepProps> = ({
)}
</DialogContent>
</Dialog>
{/* Coming Soon Section */}
<ComingSoonSection />
</Box>
);
};

View File

@@ -0,0 +1,374 @@
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
Grid,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemIcon,
ListItemText,
Alert,
LinearProgress
} from '@mui/material';
import {
Search as SearchIcon,
Analytics as AnalyticsIcon,
TrendingUp as TrendingIcon,
Speed as SpeedIcon,
Security as SecurityIcon,
CheckCircle as CheckIcon,
Schedule as ScheduleIcon,
Rocket as RocketIcon,
DataUsage as DataIcon,
Compare as CompareIcon,
Insights as InsightsIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
export const ComingSoonSection: React.FC = () => {
const [openModal, setOpenModal] = useState(false);
const [selectedFeature, setSelectedFeature] = useState<string | null>(null);
const features = [
{
id: 'deep-competitor-analysis',
title: 'Deep Competitor Analysis',
description: 'Comprehensive analysis of competitor websites and content strategies',
icon: <SearchIcon />,
status: 'Coming Soon',
color: '#3b82f6',
details: [
'Analyze 15-25 relevant competitors automatically discovered',
'Crawl competitor homepages for content strategy analysis',
'Extract competitive advantages and market positioning',
'Identify content gaps and opportunities',
'Generate strategic recommendations based on competitive intelligence'
]
},
{
id: 'sitemap-benchmarking',
title: 'Competitive Sitemap Benchmarking',
description: 'Compare your site structure against competitors',
icon: <AnalyticsIcon />,
status: 'In Development',
color: '#10b981',
details: [
'Analyze competitor sitemaps for structure insights',
'Benchmark content volume against market leaders',
'Compare publishing frequency and patterns',
'Identify missing content categories',
'Get SEO structure optimization recommendations'
]
},
{
id: 'ai-competitive-insights',
title: 'AI-Powered Competitive Insights',
description: 'Advanced AI analysis of competitive landscape',
icon: <InsightsIcon />,
status: 'Planned',
color: '#8b5cf6',
details: [
'AI-generated competitive intelligence reports',
'Market positioning analysis with business impact',
'Content strategy recommendations based on competitor data',
'Competitive advantage identification',
'Strategic roadmap for competitive differentiation'
]
}
];
const handleFeatureClick = (featureId: string) => {
setSelectedFeature(featureId);
setOpenModal(true);
};
const selectedFeatureData = features.find(f => f.id === selectedFeature);
return (
<>
<Box sx={{ mt: 4, mb: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: '#1e293b', mb: 1.5 }}>
🔍 Coming Soon
</Typography>
<Typography variant="body1" sx={{ color: '#64748b', mb: 4, fontSize: '1.1rem' }}>
Advanced competitor analysis features to give you the competitive edge
</Typography>
<Grid container spacing={2}>
{features.map((feature) => (
<Grid item xs={12} md={4} key={feature.id}>
<Card
sx={{
height: '100%',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: '2px solid #e2e8f0',
backgroundColor: '#ffffff',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.15)',
borderColor: feature.color,
'& .feature-icon': {
transform: 'scale(1.1)',
backgroundColor: `${feature.color}20`
}
}
}}
onClick={() => handleFeatureClick(feature.id)}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Box
className="feature-icon"
sx={{
p: 2,
borderRadius: 3,
backgroundColor: `${feature.color}15`,
color: feature.color,
mr: 2,
transition: 'all 0.3s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{feature.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700, color: '#1e293b', mb: 1 }}>
{feature.title}
</Typography>
<Chip
label={feature.status}
size="small"
sx={{
backgroundColor: `${feature.color}20`,
color: feature.color,
fontWeight: 600,
fontSize: '0.75rem',
height: 24,
'& .MuiChip-label': {
px: 1.5
}
}}
/>
</Box>
</Box>
<Typography variant="body1" sx={{ color: '#64748b', mb: 3, lineHeight: 1.6 }}>
{feature.description}
</Typography>
<Button
variant="outlined"
size="medium"
sx={{
borderColor: feature.color,
color: feature.color,
fontWeight: 600,
px: 3,
py: 1,
borderRadius: 2,
textTransform: 'none',
'&:hover': {
backgroundColor: `${feature.color}15`,
borderColor: feature.color,
transform: 'translateY(-1px)'
}
}}
>
Learn More
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
<Alert
severity="info"
sx={{
mt: 4,
backgroundColor: '#f0f9ff',
border: '2px solid #0ea5e9',
borderRadius: 3,
'& .MuiAlert-icon': {
color: '#0ea5e9',
fontSize: '1.5rem'
}
}}
>
<Typography variant="body1" sx={{ color: '#0c4a6e', fontWeight: 500 }}>
<strong>What's Next:</strong> These advanced competitor analysis features will be available in upcoming releases.
Your current competitor research provides valuable insights to get started!
</Typography>
</Alert>
</Box>
{/* Feature Details Modal */}
<Dialog
open={openModal}
onClose={() => setOpenModal(false)}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
backgroundColor: '#ffffff',
borderRadius: 3,
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
}
}}
>
<DialogTitle sx={{ pb: 2, backgroundColor: '#f8fafc', borderBottom: '1px solid #e2e8f0' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
{selectedFeatureData && (
<>
<Box
sx={{
p: 2,
borderRadius: 3,
backgroundColor: `${selectedFeatureData.color}15`,
color: selectedFeatureData.color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{selectedFeatureData.icon}
</Box>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1e293b', mb: 1 }}>
{selectedFeatureData.title}
</Typography>
<Chip
label={selectedFeatureData.status}
size="medium"
sx={{
backgroundColor: `${selectedFeatureData.color}15`,
color: selectedFeatureData.color,
fontWeight: 600,
fontSize: '0.875rem'
}}
/>
</Box>
</>
)}
</Box>
</DialogTitle>
<DialogContent sx={{ backgroundColor: '#ffffff', p: 3 }}>
{selectedFeatureData && (
<>
<Typography variant="body1" sx={{ color: '#64748b', mb: 4, fontSize: '1.2rem', lineHeight: 1.7, fontWeight: 500 }}>
{selectedFeatureData.description}
</Typography>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: '#1e293b', fontSize: '1.3rem' }}>
Key Features:
</Typography>
<List sx={{ pl: 0 }}>
{selectedFeatureData.details.map((detail, index) => (
<ListItem key={index} sx={{ pl: 0, py: 1 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<CheckIcon sx={{ color: selectedFeatureData.color, fontSize: 20 }} />
</ListItemIcon>
<ListItemText
primary={detail}
primaryTypographyProps={{
variant: 'body1',
color: '#374151',
fontWeight: 600,
fontSize: '1.05rem',
lineHeight: 1.6
}}
/>
</ListItem>
))}
</List>
{selectedFeatureData.id === 'deep-competitor-analysis' && (
<Box sx={{ mt: 3, p: 3, backgroundColor: '#f8fafc', borderRadius: 3, border: '1px solid #e2e8f0' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 2, color: '#1e293b', fontSize: '1.1rem' }}>
How It Works:
</Typography>
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
Our AI automatically discovers 15-25 relevant competitors using advanced search algorithms.
Then we crawl each competitor's homepage to analyze their content strategy, identify their
competitive advantages, and find content gaps that present opportunities for your business.
</Typography>
</Box>
)}
{selectedFeatureData.id === 'sitemap-benchmarking' && (
<Box sx={{ mt: 3, p: 3, backgroundColor: '#f0f9ff', borderRadius: 3, border: '1px solid #e2e8f0' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 2, color: '#1e293b', fontSize: '1.1rem' }}>
Competitive Intelligence:
</Typography>
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
We analyze competitor sitemaps to understand their content structure, publishing patterns,
and SEO optimization. This gives you data-driven insights into how your site compares to
market leaders and what improvements will have the biggest competitive impact.
</Typography>
</Box>
)}
{selectedFeatureData.id === 'ai-competitive-insights' && (
<Box sx={{ mt: 3, p: 3, backgroundColor: '#f0f9ff', borderRadius: 3, border: '1px solid #e2e8f0' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 2, color: '#1e293b', fontSize: '1.1rem' }}>
Strategic Value:
</Typography>
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
Our AI analyzes the competitive landscape to provide strategic recommendations with
business impact estimates. You'll get specific content priorities, competitive positioning
advice, and a roadmap for differentiating your brand in the market.
</Typography>
</Box>
)}
</>
)}
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1, backgroundColor: '#f8fafc', borderTop: '1px solid #e2e8f0' }}>
<Button
onClick={() => setOpenModal(false)}
variant="outlined"
sx={{
borderColor: '#d1d5db',
color: '#6b7280',
'&:hover': {
borderColor: '#9ca3af',
backgroundColor: '#f9fafb'
}
}}
>
Close
</Button>
<Button
onClick={() => setOpenModal(false)}
variant="contained"
sx={{
backgroundColor: selectedFeatureData?.color || '#3b82f6',
'&:hover': {
backgroundColor: selectedFeatureData?.color || '#3b82f6',
opacity: 0.9
}
}}
>
Notify Me When Ready
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default ComingSoonSection;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,394 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box
} from '@mui/material';
import {
Psychology as PsychologyIcon,
AutoAwesome as AutoAwesomeIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { usePersonaPolling } from '../../hooks/usePersonaPolling';
import { apiClient } from '../../api/client';
import {
type GenerationStep
} from './PersonaStep/PersonaGenerationProgress';
import { usePersonaInitialization } from './PersonaStep/personaInitialization';
import { usePersonaGeneration } from './PersonaStep/personaGeneration';
import { PersonaPreviewSection } from './PersonaStep/PersonaPreviewSection';
import { PersonaLoadingState } from './PersonaStep/PersonaLoadingState';
import { ComingSoonSection } from './PersonaStep/ComingSoonSection';
interface PersonaStepProps {
onContinue: (personaData: PersonaData) => void;
updateHeaderContent: (content: StepHeaderContent) => void;
onboardingData?: {
websiteAnalysis?: any;
competitorResearch?: any;
sitemapAnalysis?: any;
businessData?: any;
};
stepData?: {
corePersona?: any;
platformPersonas?: Record<string, any>;
qualityMetrics?: any;
selectedPlatforms?: string[];
};
}
interface StepHeaderContent {
title: string;
description: string;
}
interface PersonaData {
corePersona: any;
platformPersonas: Record<string, any>;
qualityMetrics: any;
selectedPlatforms: string[];
}
// GenerationStep and ProgressMessage types imported from PersonaGenerationProgress
interface QualityMetrics {
overall_score: number;
style_consistency: number;
brand_alignment: number;
platform_optimization: number;
engagement_potential: number;
recommendations: string[];
}
const PersonaStep: React.FC<PersonaStepProps> = ({
onContinue,
updateHeaderContent,
onboardingData = {},
stepData
}) => {
// Generation state
const [generationStep, setGenerationStep] = useState<string>('analyzing');
const [isGenerating, setIsGenerating] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
// Persona data
const [corePersona, setCorePersona] = useState<any>(null);
const [platformPersonas, setPlatformPersonas] = useState<Record<string, any>>({});
const [qualityMetrics, setQualityMetrics] = useState<QualityMetrics | null>(null);
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>(['linkedin', 'blog']);
// UI state
const [showPreview, setShowPreview] = useState(false);
const [expandedAccordion, setExpandedAccordion] = useState<string | false>('core');
const [hasCheckedCache, setHasCheckedCache] = useState(false);
// Available platforms are now defined in PersonaPreviewSection
// Generation steps
const generationSteps: GenerationStep[] = [
{
id: 'analyzing',
name: 'Analyzing Your Data',
description: 'Processing website analysis, competitor research, and content insights',
icon: <AssessmentIcon />,
completed: generationStep !== 'analyzing',
progress: generationStep === 'analyzing' ? 100 : 100
},
{
id: 'generating',
name: 'Generating Core Persona',
description: 'Creating your unique writing style and brand voice',
icon: <PsychologyIcon />,
completed: ['adapting', 'assessing', 'preview'].includes(generationStep),
progress: ['adapting', 'assessing', 'preview'].includes(generationStep) ? 100 : 0
},
{
id: 'adapting',
name: 'Creating Platform Adaptations',
description: 'Optimizing your persona for different content platforms',
icon: <AutoAwesomeIcon />,
completed: ['assessing', 'preview'].includes(generationStep),
progress: ['assessing', 'preview'].includes(generationStep) ? 100 : 0
},
{
id: 'assessing',
name: 'Quality Assessment',
description: 'Evaluating persona accuracy and optimization potential',
icon: <AssessmentIcon />,
completed: generationStep === 'preview',
progress: generationStep === 'preview' ? 100 : 0
}
];
// Load cached persona data
const loadCachedPersonaData = useCallback(() => {
try {
const cachedData = localStorage.getItem('persona_generation_data');
if (cachedData) {
const parsedData = JSON.parse(cachedData);
// Check if cache is still valid (24 hours)
const cacheTime = new Date(parsedData.timestamp);
const now = new Date();
const hoursDiff = (now.getTime() - cacheTime.getTime()) / (1000 * 60 * 60);
if (hoursDiff < 24) {
console.log('Loading cached persona data...');
setCorePersona(parsedData.core_persona);
setPlatformPersonas(parsedData.platform_personas);
setQualityMetrics(parsedData.quality_metrics);
setShowPreview(true);
setGenerationStep('preview');
setProgress(100);
// Show cache notification
setSuccess('Loaded cached persona data. Click "Generate New" for fresh analysis.');
return true;
} else {
// Remove expired cache
localStorage.removeItem('persona_generation_data');
}
}
} catch (err) {
console.warn('Failed to load cached persona data:', err);
}
return false;
}, []);
// Load cached persona data from server (24h TTL on backend)
const loadServerCachedPersonaData = useCallback(async () => {
try {
const resp = await apiClient.get('/api/onboarding/step4/persona-latest');
if (resp.data && resp.data.success && resp.data.persona) {
const p = resp.data.persona;
setCorePersona(p.core_persona);
setPlatformPersonas(p.platform_personas || {});
setQualityMetrics(p.quality_metrics || null);
if (Array.isArray(p.selected_platforms)) {
setSelectedPlatforms(p.selected_platforms);
}
setShowPreview(true);
setGenerationStep('preview');
setProgress(100);
// Mirror to local cache for faster subsequent loads
try {
localStorage.setItem('persona_generation_data', JSON.stringify({
...p,
timestamp: p.timestamp || new Date().toISOString(),
}));
} catch {}
setSuccess('Loaded cached persona from server. Click "Generate New" for fresh analysis.');
return true;
}
} catch (e: any) {
// 404 means no cache; 401 means auth issue (will be handled by delay/retry)
if (e?.response?.status === 404) {
console.log('No cached persona found on server');
} else if (e?.response?.status === 401) {
console.log('Authentication not ready, will retry');
throw e; // Re-throw to trigger retry in parent function
} else {
console.warn('Error loading server cached persona:', e);
}
}
return false;
}, []);
// Save persona data to cache
const savePersonaDataToCache = useCallback((personaData: any) => {
try {
const cacheData = {
...personaData,
timestamp: new Date().toISOString(),
selected_platforms: selectedPlatforms
};
localStorage.setItem('persona_generation_data', JSON.stringify(cacheData));
console.log('Persona data cached successfully');
} catch (err) {
console.warn('Failed to cache persona data:', err);
}
}, [selectedPlatforms]);
// Use the polling hook for persona generation first
const {
progressMessages,
error: pollingError,
startPolling
} = usePersonaPolling({
onProgress: (message, progress) => {
console.log('Persona generation progress:', message, progress);
setProgress(progress);
setGenerationStep(getStepFromMessage(message));
},
onComplete: (personaResult) => {
console.log('Persona generation completed:', personaResult);
if (personaResult && personaResult.success) {
setCorePersona(personaResult.core_persona);
setPlatformPersonas(personaResult.platform_personas);
setQualityMetrics(personaResult.quality_metrics);
setShowPreview(true);
setGenerationStep('preview');
setProgress(100);
// Save to cache
savePersonaDataToCache(personaResult);
}
setIsGenerating(false);
},
onError: (error) => {
console.error('Persona generation failed:', error);
setError(error);
setIsGenerating(false);
}
});
// Use extracted hooks for initialization and generation logic
const {
generatePersonas,
getStepFromMessage
} = usePersonaGeneration({
onboardingData,
selectedPlatforms,
setCorePersona,
setPlatformPersonas,
setQualityMetrics,
setShowPreview,
setGenerationStep,
setProgress,
setIsGenerating,
setError,
savePersonaDataToCache,
startPolling
});
const {
initialize
} = usePersonaInitialization({
stepData,
updateHeaderContent,
setCorePersona,
setPlatformPersonas,
setQualityMetrics,
setSelectedPlatforms,
setShowPreview,
setGenerationStep,
setProgress,
setHasCheckedCache,
setSuccess,
loadCachedPersonaData,
loadServerCachedPersonaData,
generatePersonas
});
// Prevent double initialization in React Strict Mode
const initRef = useRef(false);
useEffect(() => {
// Skip if already initialized
if (initRef.current) {
console.log('PersonaStep: Skipping duplicate initialization (initRef guard)');
return;
}
initRef.current = true;
initialize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Run only once on mount
// Cache loading and saving functions are now handled by usePersonaInitialization hook
const handleRegenerate = () => {
setShowPreview(false);
setCorePersona(null);
setPlatformPersonas({});
setQualityMetrics(null);
generatePersonas();
};
// Handle continue with persona data
const handleContinue = useCallback(() => {
if (corePersona && platformPersonas && qualityMetrics) {
const personaData = {
corePersona,
platformPersonas,
qualityMetrics,
selectedPlatforms,
stepType: 'personalization',
completedAt: new Date().toISOString()
};
console.log('PersonaStep: Calling onContinue with persona data:', personaData);
onContinue(personaData);
} else {
console.warn('PersonaStep: Missing persona data, cannot continue');
}
}, [corePersona, platformPersonas, qualityMetrics, selectedPlatforms, onContinue]);
// Auto-call onContinue when persona data is ready
useEffect(() => {
console.log('PersonaStep: Checking persona data readiness:', {
corePersona: !!corePersona,
platformPersonas: !!platformPersonas,
qualityMetrics: !!qualityMetrics,
success,
isGenerating
});
if (corePersona && platformPersonas && qualityMetrics && success) {
console.log('PersonaStep: Persona data is ready, auto-calling onContinue');
handleContinue();
}
}, [corePersona, platformPersonas, qualityMetrics, success, handleContinue]);
// (auto-generation handled in initial effect via server/local cache fallback)
return (
<Box sx={{
width: '100%',
maxWidth: '100%',
mx: 'auto',
p: { xs: 1, sm: 2, md: 3 },
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
minHeight: '100vh',
overflow: 'hidden'
}}>
{/* Loading State and Error Handling */}
<PersonaLoadingState
showPreview={showPreview}
isGenerating={isGenerating}
corePersona={corePersona}
progress={progress}
generationStep={generationStep}
generationSteps={generationSteps}
progressMessages={progressMessages}
error={error}
pollingError={pollingError}
success={success}
handleRegenerate={handleRegenerate}
generatePersonas={generatePersonas}
setShowPreview={setShowPreview}
setSuccess={setSuccess}
/>
{/* Persona Preview Section */}
<PersonaPreviewSection
showPreview={showPreview}
corePersona={corePersona}
platformPersonas={platformPersonas}
qualityMetrics={qualityMetrics}
selectedPlatforms={selectedPlatforms}
expandedAccordion={expandedAccordion}
setExpandedAccordion={setExpandedAccordion}
setCorePersona={setCorePersona}
setPlatformPersonas={setPlatformPersonas}
handleRegenerate={handleRegenerate}
/>
{/* Coming Soon Section */}
<ComingSoonSection />
</Box>
);
};
export default PersonaStep;

View File

@@ -0,0 +1,350 @@
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
Grid,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
Alert,
LinearProgress
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
ContentPaste as ContentIcon,
Psychology as PsychologyIcon,
TrendingUp as TrendingIcon,
Security as SecurityIcon,
Speed as SpeedIcon,
CheckCircle as CheckIcon,
Schedule as ScheduleIcon,
Rocket as RocketIcon,
DataUsage as DataIcon,
Tune as TuneIcon,
SmartToy as SmartToyIcon
} from '@mui/icons-material';
interface ComingSoonSectionProps {
contentCalendar?: any[];
}
export const ComingSoonSection: React.FC<ComingSoonSectionProps> = ({
contentCalendar = []
}) => {
const [openModal, setOpenModal] = useState(false);
const [selectedFeature, setSelectedFeature] = useState<string | null>(null);
const features = [
{
id: 'test-persona',
title: 'Test Your Persona',
description: 'Generate content with different personas to see the difference',
icon: <PsychologyIcon />,
status: 'Coming Soon',
color: '#3b82f6',
details: [
'Compare content generated with and without your persona',
'Test Core, Blog, and LinkedIn personas side-by-side',
'Choose from your content calendar topics',
'Provide feedback to improve your persona',
'AI model settings automatically optimized per persona'
]
},
{
id: 'deep-crawl',
title: 'Deep Website Analysis',
description: 'Crawl 10+ pages for comprehensive persona generation',
icon: <DataIcon />,
status: 'In Development',
color: '#10b981',
details: [
'Analyze multiple blog posts and pages',
'Extract comprehensive writing patterns',
'Understand content themes and topics',
'Generate more accurate personas',
'Better brand voice detection'
]
},
{
id: 'fine-tuning',
title: 'Personal AI Fine-Tuning',
description: 'Train a custom AI model specifically for your brand',
icon: <SmartToyIcon />,
status: 'Planned',
color: '#8b5cf6',
details: [
'Fine-tune Google Gemma model with your data',
'Create your personal AI marketing team',
'Learn from your website, social media, and analytics',
'Generate content that sounds authentically like you',
'Private model - your data stays secure'
]
}
];
const handleFeatureClick = (featureId: string) => {
setSelectedFeature(featureId);
setOpenModal(true);
};
const selectedFeatureData = features.find(f => f.id === selectedFeature);
return (
<>
<Box sx={{ mt: 4, mb: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: '#1e293b', mb: 1.5 }}>
🚀 Coming Soon
</Typography>
<Typography variant="body1" sx={{ color: '#64748b', mb: 4, fontSize: '1.1rem' }}>
Exciting features in development to make your AI writing even more powerful
</Typography>
<Grid container spacing={2}>
{features.map((feature) => (
<Grid item xs={12} md={4} key={feature.id}>
<Card
sx={{
height: '100%',
cursor: 'pointer',
transition: 'all 0.3s ease',
border: '2px solid #e2e8f0',
backgroundColor: '#ffffff',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.15)',
borderColor: feature.color,
'& .feature-icon': {
transform: 'scale(1.1)',
backgroundColor: `${feature.color}20`
}
}
}}
onClick={() => handleFeatureClick(feature.id)}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Box
className="feature-icon"
sx={{
p: 2,
borderRadius: 3,
backgroundColor: `${feature.color}15`,
color: feature.color,
mr: 2,
transition: 'all 0.3s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{feature.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700, color: '#1e293b', mb: 1 }}>
{feature.title}
</Typography>
<Chip
label={feature.status}
size="small"
sx={{
backgroundColor: `${feature.color}20`,
color: feature.color,
fontWeight: 600,
fontSize: '0.75rem',
height: 24,
'& .MuiChip-label': {
px: 1.5
}
}}
/>
</Box>
</Box>
<Typography variant="body1" sx={{ color: '#64748b', mb: 3, lineHeight: 1.6 }}>
{feature.description}
</Typography>
<Button
variant="outlined"
size="medium"
sx={{
borderColor: feature.color,
color: feature.color,
fontWeight: 600,
px: 3,
py: 1,
borderRadius: 2,
textTransform: 'none',
'&:hover': {
backgroundColor: `${feature.color}15`,
borderColor: feature.color,
transform: 'translateY(-1px)'
}
}}
>
Learn More
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
<Alert
severity="info"
sx={{
mt: 4,
backgroundColor: '#f0f9ff',
border: '2px solid #0ea5e9',
borderRadius: 3,
'& .MuiAlert-icon': {
color: '#0ea5e9',
fontSize: '1.5rem'
}
}}
>
<Typography variant="body1" sx={{ color: '#0c4a6e', fontWeight: 500 }}>
<strong>What's Next:</strong> These features will be available in upcoming releases.
Your current persona is already powerful and ready to use!
</Typography>
</Alert>
</Box>
{/* Feature Details Modal */}
<Dialog
open={openModal}
onClose={() => setOpenModal(false)}
maxWidth="md"
fullWidth
>
<DialogTitle sx={{ pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
{selectedFeatureData && (
<>
<Box
sx={{
p: 2,
borderRadius: 3,
backgroundColor: `${selectedFeatureData.color}20`,
color: selectedFeatureData.color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{selectedFeatureData.icon}
</Box>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1e293b', mb: 1 }}>
{selectedFeatureData.title}
</Typography>
<Chip
label={selectedFeatureData.status}
size="medium"
sx={{
backgroundColor: `${selectedFeatureData.color}20`,
color: selectedFeatureData.color,
fontWeight: 600,
fontSize: '0.875rem'
}}
/>
</Box>
</>
)}
</Box>
</DialogTitle>
<DialogContent>
{selectedFeatureData && (
<>
<Typography variant="body1" sx={{ color: '#64748b', mb: 4, fontSize: '1.1rem', lineHeight: 1.6 }}>
{selectedFeatureData.description}
</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3, color: '#1e293b' }}>
Key Features:
</Typography>
<List sx={{ pl: 0 }}>
{selectedFeatureData.details.map((detail, index) => (
<ListItem key={index} sx={{ pl: 0, py: 1 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<CheckIcon sx={{ color: selectedFeatureData.color, fontSize: 20 }} />
</ListItemIcon>
<ListItemText
primary={detail}
primaryTypographyProps={{
variant: 'body1',
color: '#374151',
fontWeight: 500
}}
/>
</ListItem>
))}
</List>
{selectedFeatureData.id === 'test-persona' && (
<Box sx={{ mt: 3, p: 2, backgroundColor: '#f8fafc', borderRadius: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: '#1e293b' }}>
How It Works:
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Select a topic from your content calendar, then generate content using different personas
to see how your AI adapts its writing style. Compare the results and provide feedback
to continuously improve your persona.
</Typography>
</Box>
)}
{selectedFeatureData.id === 'fine-tuning' && (
<Box sx={{ mt: 3, p: 2, backgroundColor: '#f0f9ff', borderRadius: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: '#1e293b' }}>
Privacy & Security:
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Your data is used exclusively to train your private AI model. It's never shared
or used for any other purpose. You own your AI, and it works only for you.
</Typography>
</Box>
)}
</>
)}
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1 }}>
<Button
onClick={() => setOpenModal(false)}
variant="outlined"
>
Close
</Button>
<Button
onClick={() => setOpenModal(false)}
variant="contained"
sx={{
backgroundColor: selectedFeatureData?.color || '#3b82f6',
'&:hover': {
backgroundColor: selectedFeatureData?.color || '#3b82f6',
opacity: 0.9
}
}}
>
Notify Me When Ready
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default ComingSoonSection;

View File

@@ -0,0 +1,264 @@
import React from 'react';
import {
Box,
Card,
CardContent,
Typography,
LinearProgress,
CircularProgress,
Grid
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
AutoAwesome as AutoAwesomeIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { Fade } from '@mui/material';
export interface GenerationStep {
id: string;
name: string;
description: string;
icon: React.ReactNode;
completed: boolean;
progress: number;
}
export interface ProgressMessage {
timestamp: string;
message: string;
progress?: number;
}
export interface PersonaGenerationProgressProps {
isGenerating: boolean;
progress: number;
currentStep: string;
generationSteps: GenerationStep[];
progressMessages: ProgressMessage[];
}
export const PersonaGenerationProgress: React.FC<PersonaGenerationProgressProps> = ({
isGenerating,
progress,
currentStep,
generationSteps,
progressMessages
}) => {
const activeStep = generationSteps.find(step => step.id === currentStep);
return (
<>
{/* Generation Progress Card */}
{isGenerating && (
<Fade in={true}>
<Card
sx={{
mb: 4,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.05)',
borderRadius: 3,
overflow: 'hidden'
}}
>
<CardContent sx={{ p: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Box
sx={{
p: 1.5,
borderRadius: 2,
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<AutoAwesomeIcon sx={{ fontSize: 20 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 0.5 }}>
{activeStep?.name}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
{activeStep?.description}
</Typography>
</Box>
</Box>
<Box sx={{ mb: 3 }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: '#e2e8f0',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%)'
}
}}
/>
<Typography variant="body2" sx={{ mt: 1, fontWeight: 500, color: '#475569' }}>
{progress}% Complete
</Typography>
</Box>
{/* Real-time progress messages */}
{progressMessages.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 500, color: '#334155', mb: 2 }}>
Recent Updates:
</Typography>
<Box sx={{ maxHeight: 120, overflow: 'auto', pl: 1 }}>
{progressMessages.slice(-3).map((msg, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1.5 }}>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
flexShrink: 0
}}
/>
<Typography variant="body2" sx={{ color: '#475569', fontSize: '0.875rem' }}>
{msg.message}
</Typography>
</Box>
))}
</Box>
</Box>
)}
</CardContent>
</Card>
</Fade>
)}
{/* Generation Steps Grid */}
<AnimatePresence>
{isGenerating && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<Grid container spacing={3} sx={{ mb: 4 }}>
{generationSteps.map((step, index) => (
<Grid item xs={12} sm={6} md={3} key={step.id}>
<Card
sx={{
height: '100%',
background: step.completed
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: step.id === currentStep
? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'
: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
color: step.completed || step.id === currentStep ? 'white' : '#1e293b',
transition: 'all 0.3s ease',
border: '1px solid',
borderColor: step.completed || step.id === currentStep ? 'transparent' : '#e2e8f0',
boxShadow: step.completed || step.id === currentStep
? '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'
: '0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.05)',
borderRadius: 3,
cursor: 'default',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: step.completed || step.id === currentStep
? '0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.05)'
: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}
}}
>
<CardContent sx={{ textAlign: 'center', p: 3 }}>
<Box sx={{ mb: 2 }}>
{step.completed ? (
<Box
sx={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mx: 'auto',
backdropFilter: 'blur(10px)'
}}
>
<CheckCircleIcon sx={{ fontSize: 24, color: 'white' }} />
</Box>
) : step.id === currentStep ? (
<Box
sx={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mx: 'auto',
backdropFilter: 'blur(10px)',
position: 'relative'
}}
>
<CircularProgress
size={24}
sx={{
color: 'white',
'& .MuiCircularProgress-circle': {
strokeLinecap: 'round',
}
}}
/>
</Box>
) : (
<Box
sx={{
width: 48,
height: 48,
borderRadius: '50%',
background: 'linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mx: 'auto'
}}
>
<Box sx={{ color: '#64748b' }}>
{step.icon}
</Box>
</Box>
)}
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
{step.name}
</Typography>
<Typography variant="caption" sx={{
opacity: step.completed || step.id === currentStep ? 0.9 : 0.7,
lineHeight: 1.4,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{step.description}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default PersonaGenerationProgress;

View File

@@ -0,0 +1,143 @@
import React from 'react';
import {
Box,
Button,
Typography,
Alert,
Card,
CardContent,
LinearProgress,
Fade
} from '@mui/material';
import { Psychology as PsychologyIcon } from '@mui/icons-material';
import { PersonaGenerationProgress } from './PersonaGenerationProgress';
import { type GenerationStep } from './PersonaGenerationProgress';
interface PersonaLoadingStateProps {
showPreview: boolean;
isGenerating: boolean;
corePersona: any;
progress: number;
generationStep: string;
generationSteps: GenerationStep[];
progressMessages: any[];
error: string | null;
pollingError: string | null;
success: string | null;
handleRegenerate: () => void;
generatePersonas: () => void;
setShowPreview: (show: boolean) => void;
setSuccess: (message: string | null) => void;
}
export const PersonaLoadingState: React.FC<PersonaLoadingStateProps> = ({
showPreview,
isGenerating,
corePersona,
progress,
generationStep,
generationSteps,
progressMessages,
error,
pollingError,
success,
handleRegenerate,
generatePersonas,
setShowPreview,
setSuccess
}) => {
return (
<>
{/* Safeguard: show loading instead of blank while initial checks run */}
{!showPreview && !isGenerating && !corePersona && (
<Fade in={true}>
<Card sx={{
mb: 4,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05)',
borderRadius: 3
}}>
<CardContent sx={{ p: 4, textAlign: 'center' }}>
<Box sx={{ mb: 3 }}>
<Box
sx={{
width: 64,
height: 64,
borderRadius: '50%',
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mx: 'auto',
mb: 2
}}
>
<PsychologyIcon sx={{ fontSize: 32, color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 1 }}>
Preparing Persona Workspace
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Checking cache and initializing generation...
</Typography>
</Box>
<LinearProgress
sx={{
height: 6,
borderRadius: 3,
backgroundColor: '#e2e8f0',
'& .MuiLinearProgress-bar': {
borderRadius: 3,
background: 'linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%)'
}
}}
/>
</CardContent>
</Card>
</Fade>
)}
{/* Generation Progress */}
<PersonaGenerationProgress
isGenerating={isGenerating}
progress={progress}
currentStep={generationStep}
generationSteps={generationSteps}
progressMessages={progressMessages}
/>
{/* Error Display */}
{(error || pollingError) && (
<Alert severity="error" sx={{ mb: 3 }}>
{error || pollingError}
<Button
size="small"
onClick={handleRegenerate}
sx={{ ml: 2 }}
>
Try Again
</Button>
</Alert>
)}
{/* Generate New Button (when cached data is loaded) */}
{showPreview && success && success.includes('cached') && (
<Alert severity="info" sx={{ mb: 3 }}>
{success}
<Button
size="small"
onClick={() => {
setShowPreview(false);
setSuccess(null);
generatePersonas();
}}
sx={{ ml: 2 }}
>
Generate New
</Button>
</Alert>
)}
</>
);
};

View File

@@ -0,0 +1,369 @@
import React from 'react';
import {
Box,
Button,
Typography,
Alert,
Chip,
Divider,
Accordion,
AccordionSummary,
AccordionDetails,
Fade
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
Refresh as RefreshIcon,
Psychology as PsychologyIcon,
AutoAwesome as AutoAwesomeIcon,
Assessment as AssessmentIcon,
LinkedIn as LinkedInIcon,
Facebook as FacebookIcon,
Twitter as TwitterIcon,
Article as ArticleIcon,
Instagram as InstagramIcon
} from '@mui/icons-material';
import { CorePersonaDisplay } from './sections/CorePersonaDisplay';
import { PlatformPersonaDisplay } from './sections/PlatformPersonaDisplay';
import { QualityMetricsDisplay } from './QualityMetricsDisplay';
interface PersonaPreviewSectionProps {
showPreview: boolean;
corePersona: any;
platformPersonas: Record<string, any>;
qualityMetrics: any;
selectedPlatforms: string[];
expandedAccordion: string | false;
setExpandedAccordion: (accordion: string | false) => void;
setCorePersona: (persona: any) => void;
setPlatformPersonas: (personas: Record<string, any>) => void;
handleRegenerate: () => void;
}
const availablePlatforms = [
{ id: 'linkedin', name: 'LinkedIn', icon: <LinkedInIcon />, color: '#0077B5' },
{ id: 'facebook', name: 'Facebook', icon: <FacebookIcon />, color: '#1877F2' },
{ id: 'twitter', name: 'Twitter', icon: <TwitterIcon />, color: '#1DA1F2' },
{ id: 'blog', name: 'Blog', icon: <ArticleIcon />, color: '#FF6B35' },
{ id: 'instagram', name: 'Instagram', icon: <InstagramIcon />, color: '#E4405F' }
];
export const PersonaPreviewSection: React.FC<PersonaPreviewSectionProps> = ({
showPreview,
corePersona,
platformPersonas,
qualityMetrics,
selectedPlatforms,
expandedAccordion,
setExpandedAccordion,
setCorePersona,
setPlatformPersonas,
handleRegenerate
}) => {
if (!showPreview || !corePersona) {
return null;
}
return (
<Fade in={true}>
<Box>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 4,
p: 3,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05)'
}}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1e293b', mb: 0.5 }}>
Your AI Writing Persona
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Comprehensive analysis of your unique writing style and brand voice
</Typography>
</Box>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRegenerate}
size="small"
sx={{
borderColor: '#e2e8f0',
color: '#475569',
'&:hover': {
borderColor: '#3b82f6',
backgroundColor: '#f8fafc'
}
}}
>
Regenerate
</Button>
</Box>
{/* Core Persona */}
<Accordion
expanded={expandedAccordion === 'core'}
onChange={() => setExpandedAccordion(expandedAccordion === 'core' ? false : 'core')}
sx={{
mb: 3,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05)',
'&:before': {
display: 'none'
},
'&.Mui-expanded': {
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
}
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#64748b' }} />}
sx={{
px: 4,
py: 3,
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, width: '100%' }}>
<Box
sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<PsychologyIcon sx={{ fontSize: 24 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 0.5 }}>
Core Writing Style
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Your unique voice and writing characteristics
</Typography>
</Box>
{qualityMetrics && (
<Chip
label={`${qualityMetrics.overall_score}% Quality`}
sx={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
fontWeight: 600,
'& .MuiChip-label': {
px: 2
}
}}
size="small"
/>
)}
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 4, pb: 4 }}>
<CorePersonaDisplay
persona={corePersona}
onChange={(updatedPersona) => {
setCorePersona(updatedPersona);
// TODO: Add debounced auto-save
}}
/>
</AccordionDetails>
</Accordion>
{/* Platform Adaptations */}
<Accordion
expanded={expandedAccordion === 'platforms'}
onChange={() => setExpandedAccordion(expandedAccordion === 'platforms' ? false : 'platforms')}
sx={{
mb: 3,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05)',
'&:before': {
display: 'none'
},
'&.Mui-expanded': {
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
}
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#64748b' }} />}
sx={{
px: 4,
py: 3,
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, width: '100%' }}>
<Box
sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<AutoAwesomeIcon sx={{ fontSize: 24 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 0.5 }}>
Platform Adaptations
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Optimized for different content platforms
</Typography>
</Box>
<Chip
label={`${selectedPlatforms.length} Platforms`}
sx={{
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
fontWeight: 600,
'& .MuiChip-label': {
px: 2
}
}}
size="small"
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 4, pb: 4 }}>
<Box>
{selectedPlatforms.map((platformId, index) => {
const platformInfo = availablePlatforms.find(p => p.id === platformId);
return (
<Box key={platformId} sx={{ mb: index < selectedPlatforms.length - 1 ? 4 : 0 }}>
<Divider sx={{ mb: 3 }}>
<Chip
icon={platformInfo?.icon}
label={platformInfo?.name || platformId}
sx={{
background: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
color: 'white',
fontWeight: 600
}}
/>
</Divider>
<PlatformPersonaDisplay
platformPersona={platformPersonas[platformId] || {}}
platformName={platformId}
onChange={(updatedPersona) => {
setPlatformPersonas({
...platformPersonas,
[platformId]: updatedPersona
});
// TODO: Add debounced auto-save
}}
/>
</Box>
);
})}
{selectedPlatforms.length === 0 && (
<Alert severity="info" sx={{
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid #0ea5e9',
color: '#0c4a6e'
}}>
No platforms selected. Please select at least one platform to see optimized personas.
</Alert>
)}
</Box>
</AccordionDetails>
</Accordion>
{/* Quality Metrics */}
{qualityMetrics && (
<Accordion
expanded={expandedAccordion === 'quality'}
onChange={() => setExpandedAccordion(expandedAccordion === 'quality' ? false : 'quality')}
sx={{
mb: 4,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.05)',
'&:before': {
display: 'none'
},
'&.Mui-expanded': {
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
}
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#64748b' }} />}
sx={{
px: 4,
py: 3,
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, width: '100%' }}>
<Box
sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<AssessmentIcon sx={{ fontSize: 24 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 0.5 }}>
Quality Assessment
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Performance metrics and recommendations
</Typography>
</Box>
<Chip
label={`${qualityMetrics.overall_score}% Quality`}
sx={{
background: qualityMetrics.overall_score >= 85
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: qualityMetrics.overall_score >= 70
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
color: 'white',
fontWeight: 600,
'& .MuiChip-label': {
px: 2
}
}}
size="small"
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 4, pb: 4 }}>
<QualityMetricsDisplay metrics={qualityMetrics} />
</AccordionDetails>
</Accordion>
)}
</Box>
</Fade>
);
};

View File

@@ -0,0 +1,165 @@
import React from 'react';
import {
Box,
Grid,
Typography,
Stack
} from '@mui/material';
interface QualityMetrics {
overall_score: number;
style_consistency?: number;
brand_alignment?: number;
platform_optimization?: number;
engagement_potential?: number;
core_completeness?: number;
platform_consistency?: number;
linguistic_quality?: number;
recommendations: string[];
}
interface QualityMetricsDisplayProps {
metrics: QualityMetrics;
}
export const QualityMetricsDisplay: React.FC<QualityMetricsDisplayProps> = ({ metrics }) => {
// Determine which metric set is being used (old vs new)
const isNewMetrics = metrics.core_completeness !== undefined;
const metricItems = isNewMetrics ? [
{ label: 'Overall Quality', value: metrics.overall_score },
{ label: 'Core Completeness', value: metrics.core_completeness || 0 },
{ label: 'Platform Consistency', value: metrics.platform_consistency || 0 },
{ label: 'Platform Optimization', value: metrics.platform_optimization || 0 },
{ label: 'Linguistic Quality', value: metrics.linguistic_quality || 0 }
] : [
{ label: 'Overall Quality', value: metrics.overall_score },
{ label: 'Style Consistency', value: metrics.style_consistency || 0 },
{ label: 'Brand Alignment', value: metrics.brand_alignment || 0 },
{ label: 'Platform Optimization', value: metrics.platform_optimization || 0 },
{ label: 'Engagement Potential', value: metrics.engagement_potential || 0 }
];
return (
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
height: '100%'
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Performance Scores
</Typography>
<Stack spacing={3}>
{metricItems.map((metric, index) => (
<Box key={index}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1.5 }}>
<Typography variant="body2" sx={{ fontWeight: 500, color: '#334155' }}>
{metric.label}
</Typography>
<Typography variant="body2" sx={{
fontWeight: 700,
color: metric.value >= 85 ? '#059669' : metric.value >= 70 ? '#d97706' : '#dc2626'
}}>
{metric.value}%
</Typography>
</Box>
<Box sx={{
width: '100%',
height: 10,
backgroundColor: '#e2e8f0',
borderRadius: 5,
overflow: 'hidden',
position: 'relative'
}}>
<Box
sx={{
width: `${metric.value}%`,
height: '100%',
background: metric.value >= 85
? 'linear-gradient(90deg, #10b981 0%, #059669 100%)'
: metric.value >= 70
? 'linear-gradient(90deg, #f59e0b 0%, #d97706 100%)'
: 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)',
borderRadius: 5,
transition: 'width 1s ease-in-out'
}}
/>
</Box>
</Box>
))}
</Stack>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
height: '100%'
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Recommendations
</Typography>
<Stack spacing={2}>
{metrics.recommendations && metrics.recommendations.length > 0 ? (
metrics.recommendations.map((recommendation, index) => (
<Box key={index} sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
p: 2,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0',
borderRadius: 2,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.05)'
}}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
mt: 0.5,
flexShrink: 0
}} />
<Typography variant="body2" sx={{ color: '#334155', lineHeight: 1.6 }}>
{recommendation}
</Typography>
</Box>
))
) : (
<Box sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
p: 2,
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid #10b981',
borderRadius: 2
}}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
mt: 0.5,
flexShrink: 0
}} />
<Typography variant="body2" sx={{ color: '#065f46', lineHeight: 1.6 }}>
Your personas demonstrate excellent quality across all assessment criteria!
</Typography>
</Box>
)}
</Stack>
</Box>
</Grid>
</Grid>
);
};
export default QualityMetricsDisplay;

View File

@@ -0,0 +1,250 @@
import React, { useState } from 'react';
import {
Box,
Chip,
TextField,
IconButton,
Typography,
Tooltip,
Paper,
Stack
} from '@mui/material';
import {
Add as AddIcon,
Delete as DeleteIcon,
Info as InfoIcon
} from '@mui/icons-material';
interface EditableChipArrayProps {
label: string;
values: string[];
onChange: (newValues: string[]) => void;
placeholder?: string;
maxItems?: number;
color?: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning';
helperText?: string;
allowDuplicates?: boolean;
tooltipInfo?: {
title: string;
description: string;
howWeCalculated: string;
whyItMatters: string;
example?: string;
};
}
/**
* Editable array of chips (tags) component
* Allows adding, removing, and managing string arrays
*/
export const EditableChipArray: React.FC<EditableChipArrayProps> = ({
label,
values = [],
onChange,
placeholder = 'Type and press Enter to add...',
maxItems,
color = 'primary',
helperText,
allowDuplicates = false,
tooltipInfo
}) => {
const [inputValue, setInputValue] = useState('');
const [error, setError] = useState('');
const renderTooltipContent = () => {
if (!tooltipInfo) return '';
return (
<Box sx={{ maxWidth: 400, p: 1 }}>
<Typography variant="subtitle2" fontWeight="bold" gutterBottom>
{tooltipInfo.title}
</Typography>
<Typography variant="body2" paragraph>
{tooltipInfo.description}
</Typography>
<Typography variant="caption" display="block" sx={{ fontWeight: 600, mt: 1 }}>
🔍 How we calculated this:
</Typography>
<Typography variant="caption" display="block" paragraph>
{tooltipInfo.howWeCalculated}
</Typography>
<Typography variant="caption" display="block" sx={{ fontWeight: 600 }}>
💡 Why it matters:
</Typography>
<Typography variant="caption" display="block" paragraph>
{tooltipInfo.whyItMatters}
</Typography>
{tooltipInfo.example && (
<>
<Typography variant="caption" display="block" sx={{ fontWeight: 600 }}>
📝 Example:
</Typography>
<Typography variant="caption" display="block" sx={{ fontStyle: 'italic' }}>
{tooltipInfo.example}
</Typography>
</>
)}
</Box>
);
};
const handleAdd = () => {
const trimmedValue = inputValue.trim();
if (!trimmedValue) {
setError('Value cannot be empty');
return;
}
if (!allowDuplicates && values.includes(trimmedValue)) {
setError('This value already exists');
return;
}
if (maxItems && values.length >= maxItems) {
setError(`Maximum ${maxItems} items allowed`);
return;
}
onChange([...values, trimmedValue]);
setInputValue('');
setError('');
};
const handleRemove = (indexToRemove: number) => {
onChange(values.filter((_, index) => index !== indexToRemove));
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAdd();
} else if (e.key === 'Escape') {
setInputValue('');
setError('');
}
};
const canAdd = !maxItems || values.length < maxItems;
return (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 1 }}>
<Typography variant="caption" color="text.secondary">
{label}
</Typography>
{tooltipInfo && (
<Tooltip title={renderTooltipContent()} arrow placement="right" enterDelay={200}>
<InfoIcon sx={{ fontSize: 14, color: 'info.main', cursor: 'help' }} />
</Tooltip>
)}
{maxItems && (
<Typography
component="span"
variant="caption"
color="text.disabled"
sx={{ ml: 'auto' }}
>
({values.length}/{maxItems})
</Typography>
)}
</Box>
{/* Input field for adding new items */}
{canAdd && (
<Box sx={{ display: 'flex', gap: 1, mb: 1.5 }}>
<TextField
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setError('');
}}
onKeyDown={handleKeyDown}
placeholder={placeholder}
size="small"
fullWidth
error={!!error}
helperText={error || helperText}
sx={{
'& .MuiOutlinedInput-root': {
backgroundColor: 'background.paper'
}
}}
/>
<Tooltip title="Add item (Enter)">
<span>
<IconButton
onClick={handleAdd}
color="primary"
disabled={!inputValue.trim()}
size="small"
sx={{
height: 40,
width: 40
}}
>
<AddIcon />
</IconButton>
</span>
</Tooltip>
</Box>
)}
{/* Display chips */}
{values.length > 0 ? (
<Paper
variant="outlined"
sx={{
p: 1.5,
backgroundColor: 'background.default',
minHeight: 60
}}
>
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
{values.map((value, index) => (
<Chip
key={`${value}-${index}`}
label={value}
color={color}
size="small"
onDelete={() => handleRemove(index)}
deleteIcon={
<Tooltip title="Remove">
<DeleteIcon />
</Tooltip>
}
sx={{
mb: 0.5,
'& .MuiChip-deleteIcon': {
fontSize: '16px'
}
}}
/>
))}
</Stack>
</Paper>
) : (
<Paper
variant="outlined"
sx={{
p: 2,
backgroundColor: 'background.default',
textAlign: 'center'
}}
>
<Typography variant="body2" color="text.disabled">
No items added yet. {canAdd ? 'Add some above!' : ''}
</Typography>
</Paper>
)}
{!canAdd && (
<Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}>
Maximum items reached. Remove some to add more.
</Typography>
)}
</Box>
);
};
export default EditableChipArray;

View File

@@ -0,0 +1,272 @@
import React, { useState, useEffect } from 'react';
import {
Box,
TextField,
Typography,
IconButton,
Tooltip,
Fade
} from '@mui/material';
import {
Edit as EditIcon,
Check as CheckIcon,
Close as CloseIcon,
Info as InfoIcon
} from '@mui/icons-material';
interface EditableTextFieldProps {
label: string;
value: string;
onChange: (newValue: string) => void;
multiline?: boolean;
helperText?: string;
fullWidth?: boolean;
placeholder?: string;
required?: boolean;
maxLength?: number;
type?: 'text' | 'number';
tooltipInfo?: {
title: string;
description: string;
howWeCalculated: string;
whyItMatters: string;
example?: string;
};
}
/**
* Editable text field component with inline editing
* Shows text display by default, switches to edit mode on click
*/
export const EditableTextField: React.FC<EditableTextFieldProps> = ({
label,
value,
onChange,
multiline = false,
helperText,
fullWidth = true,
placeholder = 'Click to edit...',
required = false,
maxLength,
type = 'text',
tooltipInfo
}) => {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const [isHovered, setIsHovered] = useState(false);
// Update local value when prop changes
useEffect(() => {
setLocalValue(value);
}, [value]);
const handleSave = () => {
if (required && !localValue.trim()) {
return; // Don't save if required field is empty
}
onChange(localValue);
setIsEditing(false);
};
const handleCancel = () => {
setLocalValue(value); // Reset to original value
setIsEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !multiline) {
e.preventDefault();
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
};
const renderTooltipContent = () => {
if (!tooltipInfo) return '';
return (
<Box sx={{ maxWidth: 400, p: 1 }}>
<Typography variant="subtitle2" fontWeight="bold" gutterBottom>
{tooltipInfo.title}
</Typography>
<Typography variant="body2" paragraph>
{tooltipInfo.description}
</Typography>
<Typography variant="caption" display="block" sx={{ fontWeight: 600, mt: 1 }}>
🔍 How we calculated this:
</Typography>
<Typography variant="caption" display="block" paragraph>
{tooltipInfo.howWeCalculated}
</Typography>
<Typography variant="caption" display="block" sx={{ fontWeight: 600 }}>
💡 Why it matters:
</Typography>
<Typography variant="caption" display="block" paragraph>
{tooltipInfo.whyItMatters}
</Typography>
{tooltipInfo.example && (
<>
<Typography variant="caption" display="block" sx={{ fontWeight: 600 }}>
📝 Example:
</Typography>
<Typography variant="caption" display="block" sx={{ fontStyle: 'italic' }}>
{tooltipInfo.example}
</Typography>
</>
)}
</Box>
);
};
if (isEditing) {
return (
<Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: '#1e293b', fontSize: '0.875rem' }}>
{label} {required && <span style={{ color: '#ef4444' }}>*</span>}
</Typography>
{tooltipInfo && (
<Tooltip title={renderTooltipContent()} arrow placement="right" enterDelay={200}>
<InfoIcon sx={{ fontSize: 14, color: 'info.main', cursor: 'help' }} />
</Tooltip>
)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<TextField
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
onKeyDown={handleKeyDown}
multiline={multiline}
rows={multiline ? 3 : 1}
fullWidth={fullWidth}
placeholder={placeholder}
autoFocus
size="small"
helperText={helperText}
error={required && !localValue.trim()}
inputProps={{
maxLength: maxLength
}}
type={type}
sx={{
'& .MuiOutlinedInput-root': {
backgroundColor: '#ffffff',
fontSize: '0.875rem',
'&:hover': {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#3b82f6',
},
},
'&.Mui-focused': {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#3b82f6',
borderWidth: 2,
},
},
},
}}
/>
<Box sx={{ display: 'flex', gap: 0.5, pt: 0.5 }}>
<Tooltip title="Save (Enter)">
<IconButton
size="small"
color="primary"
onClick={handleSave}
disabled={required && !localValue.trim()}
>
<CheckIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Cancel (Esc)">
<IconButton size="small" color="default" onClick={handleCancel}>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
);
}
return (
<Box
sx={{
mb: 2,
position: 'relative',
width: '100%',
maxWidth: '100%'
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: '#1e293b', fontSize: '0.875rem' }}>
{label}
</Typography>
{tooltipInfo && (
<Tooltip title={renderTooltipContent()} arrow placement="right" enterDelay={200}>
<InfoIcon sx={{ fontSize: 14, color: 'info.main', cursor: 'help' }} />
</Tooltip>
)}
</Box>
<Box
onClick={() => setIsEditing(true)}
sx={{
p: 1.5,
borderRadius: 1,
border: '1px solid #e2e8f0',
backgroundColor: value ? '#f8fafc' : '#ffffff',
cursor: 'pointer',
transition: 'all 0.2s',
minHeight: multiline ? '60px' : '36px',
display: 'flex',
alignItems: multiline ? 'flex-start' : 'center',
position: 'relative',
width: '100%',
maxWidth: '100%',
'&:hover': {
borderColor: '#3b82f6',
backgroundColor: '#f1f5f9'
}
}}
>
<Typography
variant="body2"
sx={{
flex: 1,
color: value ? '#1e293b' : '#94a3b8',
whiteSpace: multiline ? 'pre-wrap' : 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: '0.875rem',
lineHeight: multiline ? 1.4 : 1
}}
>
{value || placeholder}
</Typography>
<Fade in={isHovered}>
<IconButton
size="small"
sx={{
position: 'absolute',
right: 4,
top: '50%',
transform: 'translateY(-50%)',
opacity: 0.7
}}
>
<EditIcon fontSize="small" />
</IconButton>
</Fade>
</Box>
{helperText && (
<Typography variant="caption" sx={{ color: '#64748b', mt: 0.5, display: 'block', fontSize: '0.75rem' }}>
{helperText}
</Typography>
)}
</Box>
);
};
export default EditableTextField;

View File

@@ -0,0 +1,135 @@
import React from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
Box,
Avatar,
Chip
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon
} from '@mui/icons-material';
interface SectionAccordionProps {
title: string;
icon?: React.ReactNode;
children: React.ReactNode;
defaultExpanded?: boolean;
badge?: string | number;
subtitle?: string;
color?: string;
expanded?: boolean;
onChange?: (event: React.SyntheticEvent, isExpanded: boolean) => void;
}
/**
* Reusable accordion component for organizing persona sections
* Provides consistent styling and behavior across all sections
*/
export const SectionAccordion: React.FC<SectionAccordionProps> = ({
title,
icon,
children,
defaultExpanded = false,
badge,
subtitle,
color = 'primary.main',
expanded,
onChange
}) => {
return (
<Accordion
defaultExpanded={defaultExpanded}
expanded={expanded}
onChange={onChange}
sx={{
mb: 1.5,
borderRadius: 2,
background: '#ffffff',
border: '1px solid #e2e8f0',
width: '100%',
maxWidth: '100%',
'&:before': {
display: 'none' // Remove default MUI divider
},
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
'&.Mui-expanded': {
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
px: 2,
py: 1.5,
'&.Mui-expanded': {
minHeight: 56
},
'& .MuiAccordionSummary-content': {
my: 0,
'&.Mui-expanded': {
my: 0
}
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
{/* Icon */}
{icon && (
<Avatar
sx={{
bgcolor: color,
width: 32,
height: 32
}}
>
{icon}
</Avatar>
)}
{/* Title and subtitle */}
<Box sx={{ flex: 1 }}>
<Typography variant="h6" fontWeight="600" sx={{ fontSize: '1rem', color: '#1e293b' }}>
{title}
</Typography>
{subtitle && (
<Typography variant="body2" sx={{ color: '#64748b', fontSize: '0.875rem' }}>
{subtitle}
</Typography>
)}
</Box>
{/* Badge */}
{badge !== undefined && (
<Chip
label={badge}
size="small"
color="primary"
sx={{
fontWeight: 600,
minWidth: 60
}}
/>
)}
</Box>
</AccordionSummary>
<AccordionDetails
sx={{
pt: 1,
pb: 2,
px: 2,
backgroundColor: '#ffffff'
}}
>
{children}
</AccordionDetails>
</Accordion>
);
};
export default SectionAccordion;

View File

@@ -0,0 +1,9 @@
/**
* Persona Step Components Index
* Export all reusable components for persona display
*/
export { EditableTextField } from './EditableTextField';
export { EditableChipArray } from './EditableChipArray';
export { SectionAccordion } from './SectionAccordion';

View File

@@ -0,0 +1,163 @@
import { useCallback } from 'react';
import { apiClient } from '../../../api/client';
import {
generateWritingPersonas,
assessPersonaQuality,
prepareOnboardingData,
validatePersonaRequest,
PersonaGenerationRequest
} from '../../../api/personaApi';
interface PersonaGenerationProps {
onboardingData: any;
selectedPlatforms: string[];
setCorePersona: (persona: any) => void;
setPlatformPersonas: (personas: Record<string, any>) => void;
setQualityMetrics: (metrics: any) => void;
setShowPreview: (show: boolean) => void;
setGenerationStep: (step: string) => void;
setProgress: (progress: number) => void;
setIsGenerating: (generating: boolean) => void;
setError: (error: string | null) => void;
savePersonaDataToCache: (data: any) => void;
startPolling: (taskId: string) => void;
}
export const usePersonaGeneration = ({
onboardingData,
selectedPlatforms,
setCorePersona,
setPlatformPersonas,
setQualityMetrics,
setShowPreview,
setGenerationStep,
setProgress,
setIsGenerating,
setError,
savePersonaDataToCache,
startPolling
}: PersonaGenerationProps) => {
const generatePersonas = useCallback(async () => {
setIsGenerating(true);
setError(null);
setProgress(0);
setShowPreview(false);
// Clear session cache flag since we're generating fresh
sessionStorage.removeItem('persona_server_cache_checked');
try {
// Start async persona generation
const request: PersonaGenerationRequest = {
onboarding_data: prepareOnboardingData(onboardingData),
selected_platforms: selectedPlatforms,
user_preferences: null
};
console.log('Starting async persona generation...');
const response = await apiClient.post('/api/onboarding/step4/generate-personas-async', request);
if (response.data.task_id) {
console.log('Persona generation task response:', response.data);
// Check if the task is already completed (cache hit)
if (response.data.status === 'completed') {
console.log('Task already completed (cache hit), fetching result immediately');
// Fetch the completed task result
const taskResponse = await apiClient.get(`/api/onboarding/step4/persona-task/${response.data.task_id}`);
if (taskResponse.data && taskResponse.data.result) {
const result = taskResponse.data.result;
setCorePersona(result.core_persona);
setPlatformPersonas(result.platform_personas);
setQualityMetrics(result.quality_metrics);
setShowPreview(true);
setGenerationStep('preview');
setProgress(100);
savePersonaDataToCache(result);
setIsGenerating(false);
return;
}
}
// Start polling for the task
console.log('Starting polling for task:', response.data.task_id);
startPolling(response.data.task_id);
} else {
throw new Error('Failed to start persona generation task');
}
} catch (err) {
console.error('Failed to start persona generation:', err);
setError(err instanceof Error ? err.message : 'Failed to start persona generation');
setIsGenerating(false);
}
}, [onboardingData, selectedPlatforms, startPolling, setIsGenerating, setError, setProgress, setShowPreview, setCorePersona, setPlatformPersonas, setQualityMetrics, setGenerationStep, savePersonaDataToCache]);
const generateCorePersona = async (data: any) => {
const request: PersonaGenerationRequest = {
onboarding_data: prepareOnboardingData(data),
selected_platforms: selectedPlatforms,
user_preferences: null
};
// Validate request
const validationErrors = validatePersonaRequest(request);
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join(', ')}`);
}
const response = await generateWritingPersonas(request);
if (!response.success) {
throw new Error(response.error || 'Failed to generate core persona');
}
return response.core_persona;
};
const generatePlatformPersonas = async (corePersona: any, platforms: string[]) => {
const request: PersonaGenerationRequest = {
onboarding_data: prepareOnboardingData(onboardingData),
selected_platforms: platforms,
user_preferences: null
};
const response = await generateWritingPersonas(request);
if (!response.success) {
throw new Error(response.error || 'Failed to generate platform personas');
}
return response.platform_personas || {};
};
const assessPersonaQualityInternal = async (corePersona: any, platformPersonas: any) => {
const response = await assessPersonaQuality({
core_persona: corePersona,
platform_personas: platformPersonas,
user_feedback: null
});
if (!response.success) {
throw new Error(response.error || 'Failed to assess persona quality');
}
return response.quality_metrics;
};
const getStepFromMessage = (message: string): string => {
if (message.includes('Initializing')) return 'analyzing';
if (message.includes('core persona')) return 'generating';
if (message.includes('platform')) return 'adapting';
if (message.includes('quality')) return 'assessing';
if (message.includes('completed')) return 'preview';
return 'generating';
};
return {
generatePersonas,
generateCorePersona,
generatePlatformPersonas,
assessPersonaQualityInternal,
getStepFromMessage
};
};

View File

@@ -0,0 +1,125 @@
import { useCallback } from 'react';
interface PersonaInitializationProps {
stepData?: {
corePersona?: any;
platformPersonas?: Record<string, any>;
qualityMetrics?: any;
selectedPlatforms?: string[];
};
updateHeaderContent: (content: { title: string; description: string }) => void;
setCorePersona: (persona: any) => void;
setPlatformPersonas: (personas: Record<string, any>) => void;
setQualityMetrics: (metrics: any) => void;
setSelectedPlatforms: (platforms: string[]) => void;
setShowPreview: (show: boolean) => void;
setGenerationStep: (step: string) => void;
setProgress: (progress: number) => void;
setHasCheckedCache: (checked: boolean) => void;
setSuccess: (message: string | null) => void;
loadCachedPersonaData: () => boolean;
loadServerCachedPersonaData: () => Promise<boolean>;
generatePersonas: () => Promise<void>;
}
export const usePersonaInitialization = ({
stepData,
updateHeaderContent,
setCorePersona,
setPlatformPersonas,
setQualityMetrics,
setSelectedPlatforms,
setShowPreview,
setGenerationStep,
setProgress,
setHasCheckedCache,
setSuccess,
loadCachedPersonaData,
loadServerCachedPersonaData,
generatePersonas
}: PersonaInitializationProps) => {
const initialize = useCallback(async () => {
console.log('PersonaStep: Initialization started');
// Update header immediately
updateHeaderContent({
title: 'AI Writing Persona Generation',
description: 'ALwrity is analyzing your content and creating a sophisticated AI writing persona that captures your unique style, brand voice, and content preferences across all platforms.'
});
// Check if we already have persona data from stepData (when navigating back)
if (stepData?.corePersona) {
console.log('PersonaStep: Loading persona data from stepData (navigation back)');
setCorePersona(stepData.corePersona);
setPlatformPersonas(stepData.platformPersonas || {});
setQualityMetrics(stepData.qualityMetrics || null);
if (stepData.selectedPlatforms) {
setSelectedPlatforms(stepData.selectedPlatforms);
}
setShowPreview(true);
setGenerationStep('preview');
setProgress(100);
setHasCheckedCache(true);
return;
}
// Check session flag to avoid redundant server cache checks
const serverCacheChecked = sessionStorage.getItem('persona_server_cache_checked');
// Try to load from server cache first (skip if already checked this session and was 404)
let foundCache = false;
if (!serverCacheChecked || serverCacheChecked !== '404') {
try {
console.log('PersonaStep: Checking server cache');
foundCache = await loadServerCachedPersonaData();
if (foundCache) {
console.log('PersonaStep: Server cache found, using it');
sessionStorage.setItem('persona_server_cache_checked', 'found');
setHasCheckedCache(true);
return;
} else {
// Mark that we checked and got 404
sessionStorage.setItem('persona_server_cache_checked', '404');
}
} catch (error: any) {
console.warn('PersonaStep: Error loading server cache, trying local cache:', error);
sessionStorage.setItem('persona_server_cache_checked', '404');
}
} else {
console.log('PersonaStep: Skipping server cache check (already checked this session, was 404)');
}
// Try local cache
console.log('PersonaStep: Checking local cache');
foundCache = loadCachedPersonaData();
if (foundCache) {
console.log('PersonaStep: Local cache found, using it');
setHasCheckedCache(true);
return;
}
// No cache found, start generation
console.log('PersonaStep: No cache found, starting generation');
await generatePersonas();
setHasCheckedCache(true);
}, [
stepData,
updateHeaderContent,
setCorePersona,
setPlatformPersonas,
setQualityMetrics,
setSelectedPlatforms,
setShowPreview,
setGenerationStep,
setProgress,
setHasCheckedCache,
loadCachedPersonaData,
loadServerCachedPersonaData,
generatePersonas
]);
return {
initialize
};
};

View File

@@ -0,0 +1,506 @@
import React from 'react';
import { Box, Grid, Typography } from '@mui/material';
import {
Psychology as PsychologyIcon,
RecordVoiceOver as VoiceIcon,
Tune as TuneIcon,
FormatPaint as FormatIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { SectionAccordion } from '../components/SectionAccordion';
import { EditableTextField } from '../components/EditableTextField';
import { EditableChipArray } from '../components/EditableChipArray';
import { corePersonaTooltips } from '../utils/personaTooltips';
interface CorePersonaDisplayProps {
persona: any;
onChange: (updatedPersona: any) => void;
}
/**
* Comprehensive display for Core Persona data
* Shows all backend-generated fields in organized, editable sections
*/
export const CorePersonaDisplay: React.FC<CorePersonaDisplayProps> = ({
persona,
onChange
}) => {
// Helper function to update nested fields
const updateField = (path: string[], value: any) => {
const updatedPersona = { ...persona };
let current = updatedPersona;
for (let i = 0; i < path.length - 1; i++) {
if (!current[path[i]]) {
current[path[i]] = {};
}
current = current[path[i]];
}
current[path[path.length - 1]] = value;
onChange(updatedPersona);
};
// Safe getter for nested properties
const getNestedValue = (obj: any, path: string[], defaultValue: any = '') => {
return path.reduce((current, key) => current?.[key], obj) ?? defaultValue;
};
return (
<Box>
{/* 1. Identity & Brand Voice Section */}
<SectionAccordion
title="Identity & Brand Voice"
subtitle="Core personality and brand characteristics"
icon={<PsychologyIcon />}
defaultExpanded={true}
color="primary.main"
>
<Box sx={{
p: 2,
background: '#ffffff',
border: '1px solid #e2e8f0',
borderRadius: 2,
mb: 2,
width: '100%',
overflow: 'visible'
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 2 }}>
Core Identity
</Typography>
<Grid container spacing={2} sx={{ width: '100%' }}>
<Grid item xs={12} sm={6} sx={{ width: '100%' }}>
<EditableTextField
label="Persona Name"
value={getNestedValue(persona, ['identity', 'persona_name'])}
onChange={(val) => updateField(['identity', 'persona_name'], val)}
placeholder="e.g., The Thought Leader"
helperText="A descriptive name for this writing persona"
tooltipInfo={corePersonaTooltips.personaName}
/>
</Grid>
<Grid item xs={12} sm={6} sx={{ width: '100%' }}>
<EditableTextField
label="Archetype"
value={getNestedValue(persona, ['identity', 'archetype'])}
onChange={(val) => updateField(['identity', 'archetype'], val)}
placeholder="e.g., Expert Educator, Innovator, Storyteller"
helperText="The primary archetype this persona embodies"
tooltipInfo={corePersonaTooltips.archetype}
/>
</Grid>
<Grid item xs={12} sx={{ width: '100%' }}>
<EditableTextField
label="Core Belief"
value={getNestedValue(persona, ['identity', 'core_belief'])}
onChange={(val) => updateField(['identity', 'core_belief'], val)}
multiline
placeholder="What is the fundamental belief driving this persona?"
helperText="The underlying philosophy or conviction"
tooltipInfo={corePersonaTooltips.coreBelief}
/>
</Grid>
<Grid item xs={12}>
<EditableTextField
label="Brand Voice Description"
value={getNestedValue(persona, ['identity', 'brand_voice_description'])}
onChange={(val) => updateField(['identity', 'brand_voice_description'], val)}
multiline
placeholder="Describe the overall brand voice..."
helperText="A comprehensive description of the brand voice and tone"
tooltipInfo={corePersonaTooltips.brandVoice}
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 2. Linguistic Fingerprint Section */}
<SectionAccordion
title="Linguistic Fingerprint"
subtitle="Detailed writing style characteristics"
icon={<VoiceIcon />}
color="secondary.main"
>
{/* Sentence Metrics */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Sentence Metrics
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Average Sentence Length (words)"
value={getNestedValue(persona, ['linguistic_fingerprint', 'sentence_metrics', 'average_sentence_length_words'], '')}
onChange={(val) => updateField(['linguistic_fingerprint', 'sentence_metrics', 'average_sentence_length_words'], Number(val))}
type="number"
placeholder="e.g., 18"
helperText="Typical sentence length in words"
tooltipInfo={corePersonaTooltips.avgSentenceLength}
/>
<EditableTextField
label="Preferred Sentence Type"
value={getNestedValue(persona, ['linguistic_fingerprint', 'sentence_metrics', 'preferred_sentence_type'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'sentence_metrics', 'preferred_sentence_type'], val)}
placeholder="e.g., Compound, Complex, Simple"
helperText="Most commonly used sentence structure"
tooltipInfo={corePersonaTooltips.sentenceType}
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Active to Passive Ratio"
value={getNestedValue(persona, ['linguistic_fingerprint', 'sentence_metrics', 'active_to_passive_ratio'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'sentence_metrics', 'active_to_passive_ratio'], val)}
placeholder="e.g., 80:20, Mostly active"
helperText="Balance of active vs passive voice"
tooltipInfo={corePersonaTooltips.activePassiveRatio}
/>
<EditableTextField
label="Complexity Level"
value={getNestedValue(persona, ['linguistic_fingerprint', 'sentence_metrics', 'complexity_level'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'sentence_metrics', 'complexity_level'], val)}
placeholder="e.g., Moderate, Complex, Simple"
helperText="Overall sentence complexity"
tooltipInfo={corePersonaTooltips.complexityLevel}
/>
</Grid>
</Grid>
</Box>
{/* Lexical Features */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Lexical Features
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Go-To Words"
values={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'go_to_words'], [])}
onChange={(vals) => updateField(['linguistic_fingerprint', 'lexical_features', 'go_to_words'], vals)}
placeholder="Add frequently used words..."
color="primary"
helperText="Words frequently used in this writing style"
tooltipInfo={corePersonaTooltips.goToWords}
/>
<EditableChipArray
label="Go-To Phrases"
values={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'go_to_phrases'], [])}
onChange={(vals) => updateField(['linguistic_fingerprint', 'lexical_features', 'go_to_phrases'], vals)}
placeholder="Add signature phrases..."
color="secondary"
helperText="Signature phrases or expressions"
tooltipInfo={corePersonaTooltips.goToPhrases}
/>
<EditableChipArray
label="Avoid Words"
values={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'avoid_words'], [])}
onChange={(vals) => updateField(['linguistic_fingerprint', 'lexical_features', 'avoid_words'], vals)}
placeholder="Add words to avoid..."
color="error"
helperText="Words that should be avoided"
tooltipInfo={corePersonaTooltips.avoidWords}
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Contractions Usage"
value={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'contractions'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'lexical_features', 'contractions'], val)}
placeholder="e.g., Frequent, Occasional, Rare"
helperText="How often contractions are used"
tooltipInfo={corePersonaTooltips.contractions}
/>
<EditableTextField
label="Filler Words"
value={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'filler_words'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'lexical_features', 'filler_words'], val)}
placeholder="e.g., Minimal, Moderate"
helperText="Usage of filler words (um, uh, like, etc.)"
tooltipInfo={corePersonaTooltips.contractions}
/>
<EditableTextField
label="Vocabulary Level"
value={getNestedValue(persona, ['linguistic_fingerprint', 'lexical_features', 'vocabulary_level'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'lexical_features', 'vocabulary_level'], val)}
placeholder="e.g., Advanced, Intermediate, Accessible"
helperText="Overall sophistication of vocabulary"
tooltipInfo={corePersonaTooltips.vocabularyLevel}
/>
</Grid>
</Grid>
</Box>
{/* Rhetorical Devices */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Rhetorical Devices
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Metaphors"
value={getNestedValue(persona, ['linguistic_fingerprint', 'rhetorical_devices', 'metaphors'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'rhetorical_devices', 'metaphors'], val)}
multiline
placeholder="Describe metaphor usage..."
helperText="How metaphors are used in writing"
tooltipInfo={corePersonaTooltips.metaphors}
/>
<EditableTextField
label="Analogies"
value={getNestedValue(persona, ['linguistic_fingerprint', 'rhetorical_devices', 'analogies'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'rhetorical_devices', 'analogies'], val)}
multiline
placeholder="Describe analogy usage..."
helperText="How analogies are used to explain concepts"
tooltipInfo={corePersonaTooltips.analogies}
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Rhetorical Questions"
value={getNestedValue(persona, ['linguistic_fingerprint', 'rhetorical_devices', 'rhetorical_questions'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'rhetorical_devices', 'rhetorical_questions'], val)}
multiline
placeholder="Describe usage of rhetorical questions..."
helperText="How rhetorical questions are employed"
tooltipInfo={corePersonaTooltips.rhetoricalQuestions}
/>
<EditableTextField
label="Storytelling Style"
value={getNestedValue(persona, ['linguistic_fingerprint', 'rhetorical_devices', 'storytelling_style'])}
onChange={(val) => updateField(['linguistic_fingerprint', 'rhetorical_devices', 'storytelling_style'], val)}
multiline
placeholder="Describe storytelling approach..."
helperText="Narrative and storytelling techniques used"
tooltipInfo={corePersonaTooltips.storytelling}
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 3. Tonal Range Section */}
<SectionAccordion
title="Tonal Range"
subtitle="Voice tone and emotional characteristics"
icon={<TuneIcon />}
color="info.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Voice Characteristics
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Default Tone"
value={getNestedValue(persona, ['tonal_range', 'default_tone'])}
onChange={(val) => updateField(['tonal_range', 'default_tone'], val)}
placeholder="e.g., Professional yet approachable"
helperText="The primary tone used in most content"
tooltipInfo={corePersonaTooltips.defaultTone}
/>
<EditableTextField
label="Emotional Range"
value={getNestedValue(persona, ['tonal_range', 'emotional_range'])}
onChange={(val) => updateField(['tonal_range', 'emotional_range'], val)}
multiline
placeholder="Describe the emotional spectrum..."
helperText="Range of emotions expressed in writing"
tooltipInfo={corePersonaTooltips.emotionalRange}
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Permissible Tones"
values={getNestedValue(persona, ['tonal_range', 'permissible_tones'], [])}
onChange={(vals) => updateField(['tonal_range', 'permissible_tones'], vals)}
placeholder="Add acceptable tones..."
color="success"
helperText="Tones that fit this persona"
tooltipInfo={corePersonaTooltips.permissibleTones}
/>
<EditableChipArray
label="Forbidden Tones"
values={getNestedValue(persona, ['tonal_range', 'forbidden_tones'], [])}
onChange={(vals) => updateField(['tonal_range', 'forbidden_tones'], vals)}
placeholder="Add tones to avoid..."
color="error"
helperText="Tones that should be avoided"
tooltipInfo={corePersonaTooltips.forbiddenTones}
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 4. Stylistic Constraints Section */}
<SectionAccordion
title="Stylistic Constraints"
subtitle="Formatting and punctuation preferences"
icon={<FormatIcon />}
color="warning.main"
>
{/* Punctuation */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Punctuation Preferences
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<EditableTextField
label="Ellipses Usage"
value={getNestedValue(persona, ['stylistic_constraints', 'punctuation', 'ellipses'])}
onChange={(val) => updateField(['stylistic_constraints', 'punctuation', 'ellipses'], val)}
placeholder="e.g., Rarely, Never, Occasionally"
helperText="How ellipses (...) are used"
tooltipInfo={corePersonaTooltips.ellipses}
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Em-Dash Usage"
value={getNestedValue(persona, ['stylistic_constraints', 'punctuation', 'em_dash'])}
onChange={(val) => updateField(['stylistic_constraints', 'punctuation', 'em_dash'], val)}
placeholder="e.g., Frequent, Sparingly"
helperText="How em-dashes (—) are used"
tooltipInfo={corePersonaTooltips.emDash}
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Exclamation Points"
value={getNestedValue(persona, ['stylistic_constraints', 'punctuation', 'exclamation_points'])}
onChange={(val) => updateField(['stylistic_constraints', 'punctuation', 'exclamation_points'], val)}
placeholder="e.g., Minimal, Never, For emphasis"
helperText="How exclamation points are used"
tooltipInfo={corePersonaTooltips.exclamations}
/>
</Grid>
</Grid>
</Box>
{/* Formatting */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Formatting Preferences
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<EditableTextField
label="Paragraph Style"
value={getNestedValue(persona, ['stylistic_constraints', 'formatting', 'paragraphs'])}
onChange={(val) => updateField(['stylistic_constraints', 'formatting', 'paragraphs'], val)}
multiline
placeholder="Describe paragraph preferences..."
helperText="Paragraph length and structure"
tooltipInfo={corePersonaTooltips.paragraphs}
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Lists Preference"
value={getNestedValue(persona, ['stylistic_constraints', 'formatting', 'lists'])}
onChange={(val) => updateField(['stylistic_constraints', 'formatting', 'lists'], val)}
multiline
placeholder="Describe list usage..."
helperText="How and when to use lists"
tooltipInfo={corePersonaTooltips.lists}
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Markdown Usage"
value={getNestedValue(persona, ['stylistic_constraints', 'formatting', 'markdown'])}
onChange={(val) => updateField(['stylistic_constraints', 'formatting', 'markdown'], val)}
multiline
placeholder="Describe markdown preferences..."
helperText="Markdown formatting guidelines"
tooltipInfo={corePersonaTooltips.markdown}
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 5. Persona Generation Summary */}
<SectionAccordion
title="Persona Generation Summary"
subtitle="How your persona was created"
icon={<AssessmentIcon />}
color="success.main"
>
<Box sx={{
p: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid #0ea5e9',
borderRadius: 3,
borderLeft: '4px solid #0ea5e9'
}}>
<Typography variant="h6" gutterBottom sx={{ color: '#0c4a6e', fontWeight: 600, mb: 3 }}>
Your AI Writing Persona
</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.8, color: '#0c4a6e' }}>
This persona was generated by analyzing comprehensive data from your website,
competitor research, sitemap analysis, and business context. Our AI examined
your writing style patterns, tone consistency, sentence structure, vocabulary
choices, and brand voice to create an authentic digital replica of your
communication style.
</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.8, color: '#0c4a6e' }}>
The persona includes linguistic fingerprints (sentence metrics, lexical features,
rhetorical devices), tonal guidelines, and stylistic constraints that ensure
content generated across different platforms maintains your unique voice while
optimizing for each platform's best practices.
</Typography>
<Typography variant="body2" sx={{ lineHeight: 1.8, fontStyle: 'italic', color: '#0c4a6e' }}>
You can edit any field above to refine your persona. All changes are saved
automatically and will be used to generate content that truly sounds like you.
</Typography>
</Box>
</SectionAccordion>
</Box>
);
};
export default CorePersonaDisplay;

View File

@@ -0,0 +1,48 @@
# CorePersonaDisplay Tooltip Mappings
## Fields that need tooltipInfo added:
### Linguistic Fingerprint - Sentence Metrics
- avgSentenceLength
- sentenceType
- activePassiveRatio
- complexityLevel
### Linguistic Fingerprint - Lexical Features
- goToWords
- goToPhrases
- avoidWords
- contractions
- vocabularyLevel
### Linguistic Fingerprint - Rhetorical Devices
- metaphors
- analogies
- rhetoricalQuestions
- storytelling
### Tonal Range
- defaultTone
- permissibleTones
- forbiddenTones
- emotionalRange
### Stylistic Constraints - Punctuation
- ellipses
- emDash
- exclamations
### Stylistic Constraints - Formatting
- paragraphs
- lists
- markdown
### Confidence & Analysis
- confidenceScore
- analysisNotes
## Quick Reference for Adding:
```typescript
tooltipInfo={corePersonaTooltips.fieldName}
```

View File

@@ -0,0 +1,621 @@
import React from 'react';
import { Box, Grid, Typography, Chip } from '@mui/material';
import {
ContentPaste as ContentIcon,
TrendingUp as TrendingIcon,
Psychology as StrategyIcon,
EmojiEvents as FeaturesIcon,
Speed as AlgorithmIcon,
Business as ProfessionalIcon,
CheckCircle as BestPracticeIcon
} from '@mui/icons-material';
import { SectionAccordion } from '../components/SectionAccordion';
import { EditableTextField } from '../components/EditableTextField';
import { EditableChipArray } from '../components/EditableChipArray';
import { platformPersonaTooltips } from '../utils/personaTooltips';
interface PlatformPersonaDisplayProps {
platformPersona: any;
platformName: string;
onChange: (updatedPersona: any) => void;
}
/**
* Comprehensive display for Platform-Specific Persona data
* Shows all platform-optimized fields (LinkedIn example shown)
*/
export const PlatformPersonaDisplay: React.FC<PlatformPersonaDisplayProps> = ({
platformPersona,
platformName,
onChange
}) => {
// Helper function to update nested fields
const updateField = (path: string[], value: any) => {
const updatedPersona = { ...platformPersona };
let current = updatedPersona;
for (let i = 0; i < path.length - 1; i++) {
if (!current[path[i]]) {
current[path[i]] = {};
}
current = current[path[i]];
}
current[path[path.length - 1]] = value;
onChange(updatedPersona);
};
// Safe getter for nested properties
const getNestedValue = (obj: any, path: string[], defaultValue: any = '') => {
return path.reduce((current, key) => current?.[key], obj) ?? defaultValue;
};
const isLinkedIn = platformName.toLowerCase() === 'linkedin';
return (
<Box>
{/* Platform Overview */}
<Box sx={{
p: 3,
mb: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
textAlign: 'center'
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 1 }}>
{platformName.charAt(0).toUpperCase() + platformName.slice(1)} Persona
</Typography>
<Chip
label={getNestedValue(platformPersona, ['platform_type'], platformName)}
sx={{
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
fontWeight: 600
}}
size="small"
/>
</Box>
{/* 1. Content Format Rules Section */}
<SectionAccordion
title="Content Format Rules"
subtitle="Platform-specific formatting guidelines"
icon={<ContentIcon />}
defaultExpanded={true}
color="primary.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Content Guidelines
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Character Limit"
value={getNestedValue(platformPersona, ['content_format_rules', 'character_limit'], '')}
onChange={(val) => updateField(['content_format_rules', 'character_limit'], Number(val))}
type="number"
placeholder="e.g., 3000"
helperText="Maximum characters allowed per post"
tooltipInfo={platformPersonaTooltips.characterLimit}
/>
<EditableTextField
label="Paragraph Structure"
value={getNestedValue(platformPersona, ['content_format_rules', 'paragraph_structure'])}
onChange={(val) => updateField(['content_format_rules', 'paragraph_structure'], val)}
multiline
placeholder="Describe ideal paragraph structure..."
helperText="How to structure paragraphs for this platform"
tooltipInfo={platformPersonaTooltips.paragraphStructure}
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Call-to-Action Style"
value={getNestedValue(platformPersona, ['content_format_rules', 'call_to_action_style'])}
onChange={(val) => updateField(['content_format_rules', 'call_to_action_style'], val)}
multiline
placeholder="Describe CTA approach..."
helperText="How to craft effective CTAs"
tooltipInfo={platformPersonaTooltips.ctaStyle}
/>
<EditableTextField
label="Link Placement"
value={getNestedValue(platformPersona, ['content_format_rules', 'link_placement'])}
onChange={(val) => updateField(['content_format_rules', 'link_placement'], val)}
multiline
placeholder="Where and how to place links..."
helperText="Best practices for link positioning"
tooltipInfo={platformPersonaTooltips.linkPlacement}
/>
</Grid>
</Grid>
</Box>
{/* Sentence Metrics */}
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Sentence Metrics
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<EditableTextField
label="Max Sentence Length"
value={getNestedValue(platformPersona, ['sentence_metrics', 'max_sentence_length'], '')}
onChange={(val) => updateField(['sentence_metrics', 'max_sentence_length'], Number(val))}
type="number"
placeholder="e.g., 25"
helperText="Maximum words per sentence"
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Optimal Sentence Length"
value={getNestedValue(platformPersona, ['sentence_metrics', 'optimal_sentence_length'], '')}
onChange={(val) => updateField(['sentence_metrics', 'optimal_sentence_length'], Number(val))}
type="number"
placeholder="e.g., 15"
helperText="Ideal words per sentence"
/>
</Grid>
<Grid item xs={12} md={4}>
<EditableTextField
label="Sentence Variety"
value={getNestedValue(platformPersona, ['sentence_metrics', 'sentence_variety'])}
onChange={(val) => updateField(['sentence_metrics', 'sentence_variety'], val)}
placeholder="e.g., High, Moderate, Low"
helperText="Variety in sentence structure"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 2. Engagement Strategy Section */}
<SectionAccordion
title="Engagement Strategy"
subtitle="Posting and community interaction tactics"
icon={<TrendingIcon />}
color="secondary.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Community Engagement
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Posting Frequency"
value={getNestedValue(platformPersona, ['engagement_patterns', 'posting_frequency'])}
onChange={(val) => updateField(['engagement_patterns', 'posting_frequency'], val)}
placeholder="e.g., 3-5 times per week"
helperText="Recommended posting frequency"
tooltipInfo={platformPersonaTooltips.postingFrequency}
/>
<EditableTextField
label="Community Interaction"
value={getNestedValue(platformPersona, ['engagement_patterns', 'community_interaction'])}
onChange={(val) => updateField(['engagement_patterns', 'community_interaction'], val)}
multiline
placeholder="Describe community engagement approach..."
helperText="How to interact with community"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Optimal Posting Times"
values={getNestedValue(platformPersona, ['engagement_patterns', 'optimal_posting_times'], [])}
onChange={(vals) => updateField(['engagement_patterns', 'optimal_posting_times'], vals)}
placeholder="Add posting times..."
color="primary"
helperText="Best times to post for engagement"
tooltipInfo={platformPersonaTooltips.optimalTimes}
/>
<EditableChipArray
label="Engagement Tactics"
values={getNestedValue(platformPersona, ['engagement_patterns', 'engagement_tactics'], [])}
onChange={(vals) => updateField(['engagement_patterns', 'engagement_tactics'], vals)}
placeholder="Add engagement tactics..."
color="secondary"
helperText="Specific tactics to boost engagement"
tooltipInfo={platformPersonaTooltips.engagementTactics}
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 3. Lexical Adaptations Section */}
<SectionAccordion
title="Lexical Adaptations"
subtitle="Platform-specific language and expressions"
icon={<StrategyIcon />}
color="info.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Language & Expression
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Platform-Specific Words"
values={getNestedValue(platformPersona, ['lexical_adaptations', 'platform_specific_words'], [])}
onChange={(vals) => updateField(['lexical_adaptations', 'platform_specific_words'], vals)}
placeholder="Add platform-specific terms..."
color="primary"
helperText="Words and terms unique to this platform"
/>
<EditableTextField
label="Hashtag Strategy"
value={getNestedValue(platformPersona, ['lexical_adaptations', 'hashtag_strategy'])}
onChange={(val) => updateField(['lexical_adaptations', 'hashtag_strategy'], val)}
multiline
placeholder="Describe hashtag approach..."
helperText="How to use hashtags effectively"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Emoji Usage"
value={getNestedValue(platformPersona, ['lexical_adaptations', 'emoji_usage'])}
onChange={(val) => updateField(['lexical_adaptations', 'emoji_usage'], val)}
multiline
placeholder="Describe emoji usage..."
helperText="When and how to use emojis"
/>
<EditableTextField
label="Mention Strategy"
value={getNestedValue(platformPersona, ['lexical_adaptations', 'mention_strategy'])}
onChange={(val) => updateField(['lexical_adaptations', 'mention_strategy'], val)}
multiline
placeholder="Describe mention approach..."
helperText="How to mention others effectively"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* LinkedIn-specific sections */}
{isLinkedIn && (
<>
{/* 4. LinkedIn Features Section */}
<SectionAccordion
title="LinkedIn Features Optimization"
subtitle="Leverage LinkedIn-specific features"
icon={<FeaturesIcon />}
color="warning.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Platform Features
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Articles Strategy"
value={getNestedValue(platformPersona, ['linkedin_features', 'articles_strategy'])}
onChange={(val) => updateField(['linkedin_features', 'articles_strategy'], val)}
multiline
placeholder="How to use LinkedIn Articles..."
helperText="Strategy for long-form articles"
/>
<EditableTextField
label="Polls Optimization"
value={getNestedValue(platformPersona, ['linkedin_features', 'polls_optimization'])}
onChange={(val) => updateField(['linkedin_features', 'polls_optimization'], val)}
multiline
placeholder="How to create engaging polls..."
helperText="Best practices for LinkedIn polls"
/>
<EditableTextField
label="Events Networking"
value={getNestedValue(platformPersona, ['linkedin_features', 'events_networking'])}
onChange={(val) => updateField(['linkedin_features', 'events_networking'], val)}
multiline
placeholder="How to leverage LinkedIn Events..."
helperText="Strategy for events and networking"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Carousels Education"
value={getNestedValue(platformPersona, ['linkedin_features', 'carousels_education'])}
onChange={(val) => updateField(['linkedin_features', 'carousels_education'], val)}
multiline
placeholder="How to create carousel posts..."
helperText="Strategy for educational carousels"
/>
<EditableTextField
label="Live Discussions"
value={getNestedValue(platformPersona, ['linkedin_features', 'live_discussions'])}
onChange={(val) => updateField(['linkedin_features', 'live_discussions'], val)}
multiline
placeholder="How to host LinkedIn Live..."
helperText="Approach to live streaming"
/>
<EditableTextField
label="Native Video"
value={getNestedValue(platformPersona, ['linkedin_features', 'native_video'])}
onChange={(val) => updateField(['linkedin_features', 'native_video'], val)}
multiline
placeholder="Video content strategy..."
helperText="Best practices for native video"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 5. Algorithm Optimization Section */}
<SectionAccordion
title="Algorithm Optimization"
subtitle="Maximize reach and engagement"
icon={<AlgorithmIcon />}
color="error.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Algorithm Strategies
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Engagement Patterns"
values={getNestedValue(platformPersona, ['algorithm_optimization', 'engagement_patterns'], [])}
onChange={(vals) => updateField(['algorithm_optimization', 'engagement_patterns'], vals)}
placeholder="Add engagement patterns..."
color="primary"
helperText="Patterns that boost algorithmic reach"
/>
<EditableChipArray
label="Content Timing"
values={getNestedValue(platformPersona, ['algorithm_optimization', 'content_timing'], [])}
onChange={(vals) => updateField(['algorithm_optimization', 'content_timing'], vals)}
placeholder="Add timing strategies..."
color="secondary"
helperText="Timing strategies for maximum reach"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Professional Value Metrics"
values={getNestedValue(platformPersona, ['algorithm_optimization', 'professional_value_metrics'], [])}
onChange={(vals) => updateField(['algorithm_optimization', 'professional_value_metrics'], vals)}
placeholder="Add value metrics..."
color="info"
helperText="Metrics the algorithm values"
/>
<EditableChipArray
label="Network Interaction Strategies"
values={getNestedValue(platformPersona, ['algorithm_optimization', 'network_interaction_strategies'], [])}
onChange={(vals) => updateField(['algorithm_optimization', 'network_interaction_strategies'], vals)}
placeholder="Add interaction strategies..."
color="success"
helperText="How to interact with network"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 6. Professional Networking Section */}
<SectionAccordion
title="Professional Networking"
subtitle="Build thought leadership and authority"
icon={<ProfessionalIcon />}
color="success.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Leadership & Authority
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Thought Leadership Positioning"
value={getNestedValue(platformPersona, ['professional_networking', 'thought_leadership_positioning'])}
onChange={(val) => updateField(['professional_networking', 'thought_leadership_positioning'], val)}
multiline
placeholder="How to position as thought leader..."
helperText="Strategy for thought leadership"
/>
<EditableTextField
label="Industry Authority Building"
value={getNestedValue(platformPersona, ['professional_networking', 'industry_authority_building'])}
onChange={(val) => updateField(['professional_networking', 'industry_authority_building'], val)}
multiline
placeholder="How to build industry authority..."
helperText="Approach to establishing authority"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableChipArray
label="Professional Relationship Strategies"
values={getNestedValue(platformPersona, ['professional_networking', 'professional_relationship_strategies'], [])}
onChange={(vals) => updateField(['professional_networking', 'professional_relationship_strategies'], vals)}
placeholder="Add relationship strategies..."
color="primary"
helperText="Strategies for building relationships"
/>
<EditableTextField
label="Career Advancement Focus"
value={getNestedValue(platformPersona, ['professional_networking', 'career_advancement_focus'])}
onChange={(val) => updateField(['professional_networking', 'career_advancement_focus'], val)}
multiline
placeholder="Career advancement approach..."
helperText="How to focus on career growth"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
{/* 7. Professional Context Optimization */}
<SectionAccordion
title="Professional Context Optimization"
subtitle="Industry and audience-specific adaptations"
icon={<ProfessionalIcon />}
color="primary.dark"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3,
mb: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Context & Positioning
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EditableTextField
label="Industry-Specific Positioning"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'industry_specific_positioning'])}
onChange={(val) => updateField(['professional_context_optimization', 'industry_specific_positioning'], val)}
multiline
placeholder="Industry-specific approach..."
helperText="How to position within your industry"
/>
<EditableTextField
label="Expertise Level Adaptation"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'expertise_level_adaptation'])}
onChange={(val) => updateField(['professional_context_optimization', 'expertise_level_adaptation'], val)}
multiline
placeholder="Expertise positioning..."
helperText="How to communicate expertise level"
/>
<EditableTextField
label="Company Size Considerations"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'company_size_considerations'])}
onChange={(val) => updateField(['professional_context_optimization', 'company_size_considerations'], val)}
multiline
placeholder="Company size strategy..."
helperText="Adaptations based on company size"
/>
<EditableTextField
label="Business Model Alignment"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'business_model_alignment'])}
onChange={(val) => updateField(['professional_context_optimization', 'business_model_alignment'], val)}
multiline
placeholder="Business model approach..."
helperText="How to align with business model"
/>
</Grid>
<Grid item xs={12} md={6}>
<EditableTextField
label="Professional Role Authority"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'professional_role_authority'])}
onChange={(val) => updateField(['professional_context_optimization', 'professional_role_authority'], val)}
multiline
placeholder="Role authority strategy..."
helperText="How to leverage professional role"
/>
<EditableChipArray
label="Demographic Targeting"
values={getNestedValue(platformPersona, ['professional_context_optimization', 'demographic_targeting'], [])}
onChange={(vals) => updateField(['professional_context_optimization', 'demographic_targeting'], vals)}
placeholder="Add target demographics..."
color="info"
helperText="Target audience demographics"
/>
<EditableTextField
label="Psychographic Engagement"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'psychographic_engagement'])}
onChange={(val) => updateField(['professional_context_optimization', 'psychographic_engagement'], val)}
multiline
placeholder="Psychographic approach..."
helperText="Engagement based on psychographics"
/>
<EditableTextField
label="Conversion Optimization"
value={getNestedValue(platformPersona, ['professional_context_optimization', 'conversion_optimization'])}
onChange={(val) => updateField(['professional_context_optimization', 'conversion_optimization'], val)}
multiline
placeholder="Conversion strategy..."
helperText="How to optimize for conversions"
/>
</Grid>
</Grid>
</Box>
</SectionAccordion>
</>
)}
{/* 8. Best Practices Section (for all platforms) */}
<SectionAccordion
title="Platform Best Practices"
subtitle="Recommended practices and tips"
icon={<BestPracticeIcon />}
color="success.main"
>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
borderRadius: 3
}}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 3 }}>
Best Practices
</Typography>
<EditableChipArray
label="Platform Best Practices"
values={getNestedValue(platformPersona, ['platform_best_practices'], [])}
onChange={(vals) => updateField(['platform_best_practices'], vals)}
placeholder="Add best practices..."
color="success"
helperText="Platform-specific recommendations and tips"
/>
</Box>
</SectionAccordion>
</Box>
);
};
export default PlatformPersonaDisplay;

View File

@@ -0,0 +1,8 @@
/**
* Persona Step Sections Index
* Export all persona display sections
*/
export { CorePersonaDisplay } from './CorePersonaDisplay';
export { PlatformPersonaDisplay } from './PlatformPersonaDisplay';

View File

@@ -0,0 +1,343 @@
/**
* Persona Tooltips and Insights
* Comprehensive explanations for every metric and field
*/
export interface TooltipInfo {
title: string;
description: string;
howWeCalculated: string;
whyItMatters: string;
example?: string;
}
/**
* Core Persona Tooltips
*/
export const corePersonaTooltips = {
// Identity Section
personaName: {
title: "Persona Name",
description: "A descriptive name that captures the essence of your writing personality and brand identity.",
howWeCalculated: "Generated by analyzing your writing style patterns, tone consistency, and brand positioning across all analyzed content.",
whyItMatters: "A memorable persona name helps you maintain consistency and makes it easier to switch between different writing contexts.",
example: "E.g., 'The Tech Educator', 'Strategic Storyteller', 'Data-Driven Advisor'"
},
archetype: {
title: "Writing Archetype",
description: "The fundamental character or role your writing embodies - defines how readers perceive you.",
howWeCalculated: "AI analyzed your content themes, communication style, and how you position yourself relative to your audience (teacher, peer, expert, etc.).",
whyItMatters: "Your archetype guides tone, structure, and content approach - ensuring your writing consistently reflects your intended professional image.",
example: "Expert Educator teaches, Innovator challenges conventions, Sage provides wisdom"
},
coreBelief: {
title: "Core Belief",
description: "The fundamental philosophy or conviction that drives your content and messaging.",
howWeCalculated: "Extracted from recurring themes, value statements, and the underlying message across your content. We looked at what you emphasize repeatedly.",
whyItMatters: "Your core belief creates authentic, purpose-driven content that resonates with your audience and builds trust over time.",
example: "'Knowledge should be accessible to everyone' or 'Data-driven decisions lead to success'"
},
brandVoice: {
title: "Brand Voice Description",
description: "A comprehensive characterization of your unique communication style and personality.",
howWeCalculated: "Synthesized from analyzing tone patterns, word choices, sentence structure, and how you engage with different topics across platforms.",
whyItMatters: "Consistent brand voice makes your content instantly recognizable and builds a stronger connection with your audience.",
example: "Professional yet approachable, confident without being arrogant, educational while staying engaging"
},
// Linguistic Fingerprint - Sentence Metrics
avgSentenceLength: {
title: "Average Sentence Length",
description: "The typical number of words per sentence in your writing - affects readability and pacing.",
howWeCalculated: "Analyzed 100+ sentences across your content, calculated mean word count, and identified your natural rhythm.",
whyItMatters: "Shorter sentences (10-15 words) are punchy and clear. Longer sentences (20-30 words) allow for more complex ideas. Your natural length affects engagement.",
example: "15 words = 'Clean and digestible'; 25 words = 'Detailed and thoughtful'"
},
sentenceType: {
title: "Preferred Sentence Type",
description: "The grammatical structure you naturally favor (simple, compound, complex, or compound-complex).",
howWeCalculated: "Parsed sentence structures using NLP to identify patterns in how you combine independent and dependent clauses.",
whyItMatters: "Sentence variety keeps readers engaged. Your preferred type reflects your communication sophistication and should match audience expectations.",
example: "Simple (one idea), Compound (two related ideas), Complex (main + supporting idea)"
},
activePassiveRatio: {
title: "Active to Passive Voice Ratio",
description: "How often you use active voice ('I analyzed data') vs passive voice ('Data was analyzed').",
howWeCalculated: "Used linguistic analysis to identify verb constructions and calculate the percentage of active vs passive sentences.",
whyItMatters: "Active voice (80:20 ratio) is more engaging and direct. Passive voice can add formality or objectivity when needed. Your ratio shows your natural authority level.",
example: "80:20 = Direct and engaging; 50:50 = More formal/academic"
},
complexityLevel: {
title: "Sentence Complexity Level",
description: "Overall sophistication of your sentence structures - simple, moderate, or complex.",
howWeCalculated: "Evaluated using Flesch-Kincaid readability metrics, clause depth, and vocabulary difficulty across your content.",
whyItMatters: "Complexity should match audience education level. Too simple feels condescending; too complex loses readers. We found your natural sweet spot.",
example: "Simple = Grade 8, Moderate = Grade 10-12, Complex = College+"
},
// Linguistic Fingerprint - Lexical Features
goToWords: {
title: "Go-To Words",
description: "Words and terms you use frequently that define your communication style.",
howWeCalculated: "Performed frequency analysis excluding common words, identified terms appearing 3x more than average in your industry.",
whyItMatters: "These signature words make your voice distinctive and memorable. They reveal your focus areas and expertise.",
example: "'innovative', 'strategic', 'actionable', 'framework', 'optimize'"
},
goToPhrases: {
title: "Go-To Phrases",
description: "Signature expressions and turns of phrase that make your writing uniquely yours.",
howWeCalculated: "Used n-gram analysis to find frequently repeated 2-5 word phrases unique to your writing style.",
whyItMatters: "Signature phrases build recognition and trust. They're your verbal brand markers that audiences associate with you.",
example: "'in my experience', 'here's the thing', 'let me show you', 'the reality is'"
},
avoidWords: {
title: "Words to Avoid",
description: "Terms that don't fit your authentic voice or that your analysis rarely uses.",
howWeCalculated: "Identified words common in your industry but conspicuously absent or rare in your content, suggesting conscious avoidance.",
whyItMatters: "Knowing what NOT to say is as important as what to say. These words might feel inauthentic or overused in your industry.",
example: "'basically', 'literally', 'synergy', 'leverage', 'disrupt' (if you avoid business jargon)"
},
contractions: {
title: "Contractions Usage",
description: "How often you use contractions ('don't' vs 'do not') - affects formality level.",
howWeCalculated: "Counted contraction frequency and compared to total verb phrases to determine your natural usage pattern.",
whyItMatters: "Frequent contractions = conversational and approachable. Rare contractions = formal and professional. Your pattern reflects your relationship with readers.",
example: "Frequent = casual/friendly; Occasional = balanced; Rare = formal/academic"
},
vocabularyLevel: {
title: "Vocabulary Level",
description: "The sophistication of words you choose - accessible, intermediate, or advanced.",
howWeCalculated: "Analyzed using Dale-Chall word lists and academic word frequency databases to classify your typical vocabulary tier.",
whyItMatters: "Vocabulary level must match your audience. Too basic = not credible; too advanced = loses readers. We found your effective range.",
example: "Accessible = common words; Intermediate = some technical terms; Advanced = specialized jargon"
},
// Linguistic Fingerprint - Rhetorical Devices
metaphors: {
title: "Metaphor Usage",
description: "How you use metaphorical language to explain concepts ('the market is a battlefield').",
howWeCalculated: "Identified figurative language patterns and categorized types of comparisons you frequently employ.",
whyItMatters: "Effective metaphors make complex ideas accessible and memorable. Your metaphor style reveals how you think and teach.",
example: "Business metaphors, sports analogies, nature comparisons, journey narratives"
},
analogies: {
title: "Analogy Strategy",
description: "How you use analogies to connect new concepts to familiar ones.",
howWeCalculated: "Detected 'like/as/similar to' patterns and analyzed the domains you draw comparisons from.",
whyItMatters: "Good analogies bridge knowledge gaps. Your analogy sources should resonate with your specific audience's experiences.",
example: "Tech explained through cooking, business through sports, strategy through chess"
},
rhetoricalQuestions: {
title: "Rhetorical Questions",
description: "How you use questions to engage readers without expecting literal answers.",
howWeCalculated: "Identified question patterns that appear in non-interrogative contexts and analyzed their positioning (openings, transitions).",
whyItMatters: "Rhetorical questions grab attention, create curiosity, and make readers think. Your usage pattern affects engagement flow.",
example: "'What if I told you...?', 'Sound familiar?', 'Here's the question:'"
},
storytelling: {
title: "Storytelling Style",
description: "How you incorporate narrative elements to make content engaging and relatable.",
howWeCalculated: "Detected narrative structures, personal anecdotes, and story-based explanations in your content.",
whyItMatters: "Stories make content memorable and emotional. Your storytelling approach determines how deeply readers connect with your message.",
example: "Personal anecdotes, case studies, hypothetical scenarios, customer journeys"
},
// Tonal Range
defaultTone: {
title: "Default Tone",
description: "The baseline emotional quality and attitude of your writing.",
howWeCalculated: "Sentiment analysis across 100+ pieces of content to identify your consistent emotional baseline and communication approach.",
whyItMatters: "Your default tone sets expectations. It should align with your brand and make your audience comfortable engaging with your content.",
example: "Professional yet approachable, confident and authoritative, friendly and supportive"
},
permissibleTones: {
title: "Permissible Tones",
description: "Tones you can authentically use while staying true to your brand voice.",
howWeCalculated: "Identified tonal variations that appeared naturally in your content without feeling forced or inconsistent.",
whyItMatters: "Tonal flexibility prevents monotony while maintaining authenticity. These tones expand your range without diluting your brand.",
example: "Inspirational, educational, analytical, conversational, empathetic"
},
forbiddenTones: {
title: "Forbidden Tones",
description: "Tones that feel inauthentic or contradict your established voice and brand.",
howWeCalculated: "Identified tones absent from your content that commonly appear in your industry, suggesting intentional avoidance.",
whyItMatters: "Knowing what to avoid prevents off-brand content. These tones would erode trust and confuse your audience.",
example: "Overly salesy, condescending, apologetic, pessimistic, aggressive"
},
emotionalRange: {
title: "Emotional Range",
description: "The spectrum of emotions you express in your writing, from calm to enthusiastic.",
howWeCalculated: "Analyzed emotional vocabulary, punctuation intensity, and sentiment strength across different content types.",
whyItMatters: "Emotional range creates engaging content that resonates. Too narrow = boring; too wide = inconsistent. Your range fits your brand.",
example: "Calm to moderately enthusiastic, thoughtful to inspired, objective to passionate"
},
// Stylistic Constraints - Punctuation
ellipses: {
title: "Ellipses Usage (...)",
description: "How you use ellipses for pauses, trailing thoughts, or dramatic effect.",
howWeCalculated: "Counted ellipses frequency and analyzed their contextual usage patterns in your writing.",
whyItMatters: "Ellipses create suspense or informality. Overuse can seem unprofessional; strategic use adds personality.",
example: "Rarely = professional; Occasionally = conversational; Frequent = very casual"
},
emDash: {
title: "Em-Dash Usage (—)",
description: "How you use em-dashes for emphasis, interruption, or additional information.",
howWeCalculated: "Analyzed em-dash frequency and function (parenthetical, emphasis, or dramatic pause).",
whyItMatters: "Em-dashes add sophistication and flow. They're more dynamic than commas but less formal than semicolons.",
example: "Frequent = sophisticated writer; Sparingly = traditional; Never = very formal"
},
exclamations: {
title: "Exclamation Points (!)",
description: "How you use exclamation points for emphasis and excitement.",
howWeCalculated: "Counted exclamation frequency and context (announcements, enthusiasm, urgency).",
whyItMatters: "Exclamations convey energy and emotion. Too many seem unprofessional; too few seem cold. Your usage fits your brand.",
example: "Minimal = very professional; Moderate = enthusiastic; Frequent = highly energetic"
},
// Stylistic Constraints - Formatting
paragraphs: {
title: "Paragraph Structure",
description: "Your typical paragraph length and organization style.",
howWeCalculated: "Analyzed average sentences per paragraph, paragraph transitions, and whitespace patterns.",
whyItMatters: "Paragraph length affects readability. Short paragraphs (3-4 sentences) are scannable; longer ones (6-8) are detailed. Your style fits your medium.",
example: "Short paragraphs = blog/social; Medium = articles; Long = academic/formal"
},
lists: {
title: "Lists Preference",
description: "How and when you use bulleted or numbered lists in your content.",
howWeCalculated: "Detected list frequency, type preferences (bullets vs numbers), and usage contexts.",
whyItMatters: "Lists improve scannability and comprehension. Your list style affects how readers process information.",
example: "Frequent bullets = practical/actionable; Numbered = sequential/ranked; Rare = narrative-focused"
},
markdown: {
title: "Markdown/Formatting Usage",
description: "How you use formatting like bold, italics, headers, and other text styling.",
howWeCalculated: "Analyzed formatting markup patterns across different content platforms and types.",
whyItMatters: "Strategic formatting guides attention and improves reading flow. Your style balances visual hierarchy with readability.",
example: "Heavy formatting = attention-guiding; Minimal = clean/traditional; Moderate = balanced"
},
};
/**
* Platform Persona Tooltips (LinkedIn-specific shown, similar for others)
*/
export const platformPersonaTooltips = {
// Content Format Rules
characterLimit: {
title: "Character Limit",
description: "Platform-specific maximum character count per post.",
howWeCalculated: "Based on official platform limits and optimal engagement data from platform research.",
whyItMatters: "Staying within limits ensures content isn't truncated. Knowing optimal ranges (often 50-70% of max) drives better engagement.",
example: "LinkedIn: 3,000 chars max, optimal 1,300-2,000 for highest engagement"
},
paragraphStructure: {
title: "Paragraph Structure",
description: "Platform-optimized paragraph formatting for maximum readability and engagement.",
howWeCalculated: "Analyzed top-performing content on this platform to identify optimal paragraph patterns and whitespace usage.",
whyItMatters: "Each platform has different reading behaviors. Mobile-first platforms need shorter paragraphs; desktop allows longer.",
example: "LinkedIn: 2-3 sentence paragraphs with line breaks for scannability"
},
ctaStyle: {
title: "Call-to-Action Style",
description: "How to craft effective CTAs that drive engagement on this specific platform.",
howWeCalculated: "Analyzed your successful CTAs and platform best practices to determine what drives action from your audience.",
whyItMatters: "Platform-specific CTA styles align with user behavior. LinkedIn users respond to professional invitations; Instagram to emotional appeals.",
example: "LinkedIn: 'What's your experience with this?' drives comments; 'Share your thoughts' drives shares"
},
linkPlacement: {
title: "Link Placement Strategy",
description: "Where and how to place links for optimal visibility and click-through rates.",
howWeCalculated: "Based on platform algorithms, user behavior data, and A/B testing results showing highest link engagement.",
whyItMatters: "Link placement affects both algorithm visibility and user clicks. Wrong placement can reduce reach by 50%+.",
example: "LinkedIn: First comment often better than in post body for algorithm"
},
// Engagement Strategy
postingFrequency: {
title: "Posting Frequency",
description: "Optimal posting cadence for maintaining visibility without overwhelming your audience.",
howWeCalculated: "Analyzed your historical engagement patterns, follower growth, and platform algorithm preferences for your niche.",
whyItMatters: "Posting too much causes unfollows; too little reduces visibility. Your optimal frequency balances growth and sustainability.",
example: "LinkedIn: 3-5x/week for max reach; daily can work for established accounts"
},
optimalTimes: {
title: "Optimal Posting Times",
description: "When your specific audience is most active and engaged on this platform.",
howWeCalculated: "Analyzed your audience timezone data, historical engagement patterns, and industry benchmarks for your sector.",
whyItMatters: "Posting when your audience is active increases initial engagement, which signals algorithms to boost your reach.",
example: "Tue-Thu 8-10am and 12-2pm often best for B2B; adjust for your audience"
},
engagementTactics: {
title: "Engagement Tactics",
description: "Specific strategies to increase likes, comments, shares, and meaningful interactions.",
howWeCalculated: "Identified tactics that correlate with your highest-performing content and match platform algorithm priorities.",
whyItMatters: "Platform algorithms reward engagement. These tactics are proven to work for your content type and audience.",
example: "Ask specific questions, respond within 60 mins, use polls, tag relevant people"
},
// Algorithm Optimization
algorithmInsights: {
title: "Algorithm Optimization",
description: "Platform-specific strategies to maximize content visibility and reach.",
howWeCalculated: "Based on documented platform algorithm factors, reverse-engineering of high-performing content, and your historical data.",
whyItMatters: "Algorithms determine who sees your content. Optimization can increase organic reach by 200-500%.",
example: "LinkedIn values dwell time, meaningful conversations, and professional network engagement"
}
};
/**
* Get tooltip info for a specific field
*/
export const getTooltip = (category: 'core' | 'platform', fieldKey: string): TooltipInfo | null => {
if (category === 'core') {
return corePersonaTooltips[fieldKey as keyof typeof corePersonaTooltips] || null;
} else {
return platformPersonaTooltips[fieldKey as keyof typeof platformPersonaTooltips] || null;
}
};
/**
* Format tooltip content for display
*/
export const formatTooltipContent = (tooltip: TooltipInfo): string => {
return `
📊 ${tooltip.title}
${tooltip.description}
🔍 How we calculated this:
${tooltip.howWeCalculated}
💡 Why it matters:
${tooltip.whyItMatters}
${tooltip.example ? `📝 Example: ${tooltip.example}` : ''}
`.trim();
};

View File

@@ -0,0 +1,185 @@
/**
* CompetitorsGrid Component
* Displays discovered competitors in a grid layout
*/
import React from 'react';
import {
Typography,
Grid,
Card,
CardContent,
CardActions,
Chip,
Avatar,
Button,
Box
} from '@mui/material';
import {
Business as BusinessIcon,
OpenInNew as OpenInNewIcon
} from '@mui/icons-material';
export interface Competitor {
url: string;
domain: string;
title: string;
summary: string;
relevance_score: number;
highlights?: string[];
favicon?: string;
image?: string;
published_date?: string;
author?: string;
competitive_insights: {
business_model: string;
target_audience: string;
};
content_insights: {
content_focus: string;
content_quality: string;
};
}
interface CompetitorsGridProps {
competitors: Competitor[];
onShowHighlights: (competitor: Competitor) => void;
}
// Utility function to get favicon URL
const getFaviconUrl = (url: string): string => {
try {
const domain = new URL(url).hostname;
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
} catch {
return '';
}
};
const CompetitorsGrid: React.FC<CompetitorsGridProps> = ({
competitors,
onShowHighlights
}) => {
return (
<>
<Typography
variant="h6"
gutterBottom
fontWeight={600}
mb={3}
sx={{ color: '#1a202c !important' }} // Force dark text
>
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#667eea !important' }} />
Discovered Competitors ({competitors.length})
</Typography>
<Grid container spacing={3}>
{competitors.map((competitor, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} key={index}>
<Card sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
boxShadow: '0 4px 12px rgba(3, 169, 244, 0.15)',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(3, 169, 244, 0.25)'
}
}}>
<CardContent sx={{ flexGrow: 1 }}>
<Box display="flex" alignItems="flex-start" gap={2} mb={2}>
<Avatar
sx={{
width: 40,
height: 40,
backgroundColor: '#f8fafc',
border: '1px solid #e2e8f0'
}}
src={competitor.favicon || getFaviconUrl(competitor.url)}
onError={(e) => {
// Hide the image if it fails to load
(e.target as HTMLImageElement).style.display = 'none';
}}
>
<BusinessIcon sx={{ color: '#667eea' }} />
</Avatar>
<Box flex={1}>
<Typography
variant="h6"
fontWeight={600}
gutterBottom
sx={{ color: '#1a202c !important' }} // Force dark text for readability
>
{competitor.title}
</Typography>
<Typography
variant="body2"
gutterBottom
sx={{ color: '#4a5568 !important' }} // Force dark text for readability
>
{competitor.domain}
</Typography>
<Box display="flex" gap={1} flexWrap="wrap">
<Chip
label={`${Math.round(competitor.relevance_score * 100)}% Match`}
color="primary"
size="small"
/>
{competitor.published_date && (
<Chip
label={new Date(competitor.published_date).toLocaleDateString()}
variant="outlined"
size="small"
sx={{
fontSize: '0.7rem',
height: 20,
'& .MuiChip-label': { px: 1 }
}}
/>
)}
</Box>
</Box>
</Box>
<Typography
variant="body2"
mb={2}
sx={{ color: '#2d3748 !important' }} // Force dark text for readability
>
{competitor.summary.length > 150
? `${competitor.summary.substring(0, 150)}...`
: competitor.summary
}
</Typography>
</CardContent>
<CardActions sx={{ p: 2, pt: 0 }}>
<Button
size="small"
startIcon={<OpenInNewIcon />}
onClick={() => window.open(competitor.url, '_blank')}
>
Visit Website
</Button>
{competitor.highlights && competitor.highlights.length > 0 && (
<Button
size="small"
variant="outlined"
onClick={() => onShowHighlights(competitor)}
>
Highlights
</Button>
)}
</CardActions>
</Card>
</Grid>
))}
</Grid>
</>
);
};
export default CompetitorsGrid;

View File

@@ -0,0 +1,593 @@
/**
* SitemapAnalysisResults Component
* Displays sitemap analysis results with competitive insights
*/
import React from 'react';
import {
Typography,
Paper,
Grid,
Box,
Chip,
Card,
CardContent,
Divider,
LinearProgress
} from '@mui/material';
import {
Assessment as AssessmentIcon,
TrendingUp as TrendingUpIcon,
Lightbulb as LightbulbIcon,
Business as BusinessIcon,
Analytics as AnalyticsIcon
} from '@mui/icons-material';
interface StructureAnalysis {
total_urls?: number;
average_path_depth?: number;
url_patterns?: { [key: string]: number };
}
interface ContentTrends {
publishing_velocity?: number;
date_range?: {
span_days: number;
};
}
interface PublishingPatterns {
monthly_distribution?: { [key: string]: number };
}
interface OnboardingInsights {
competitive_positioning?: string;
content_gaps?: string[];
growth_opportunities?: string[];
industry_benchmarks?: string[];
strategic_recommendations?: string[];
}
interface SitemapAnalysisData {
structure_analysis?: StructureAnalysis;
content_trends?: ContentTrends;
publishing_patterns?: PublishingPatterns;
onboarding_insights?: OnboardingInsights;
}
interface SitemapAnalysisResultsProps {
analysisData: SitemapAnalysisData;
userUrl: string;
sitemapUrl: string;
isLoading?: boolean;
discoveryMethod?: string;
}
const SitemapAnalysisResults: React.FC<SitemapAnalysisResultsProps> = ({
analysisData,
userUrl,
sitemapUrl,
isLoading = false,
discoveryMethod
}) => {
const structureAnalysis: StructureAnalysis = analysisData.structure_analysis || {};
const contentTrends: ContentTrends = analysisData.content_trends || {};
const publishingPatterns: PublishingPatterns = analysisData.publishing_patterns || {};
const onboardingInsights: OnboardingInsights = analysisData.onboarding_insights || {};
if (isLoading) {
return (
<Paper sx={{
p: 3,
mb: 4,
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
borderRadius: 2
}}>
<Box display="flex" alignItems="center" gap={2} mb={3}>
<AssessmentIcon sx={{ color: '#667eea', fontSize: '2rem' }} />
<Box>
<Typography variant="h5" fontWeight={600} sx={{ color: '#1a202c !important' }}>
Analyzing Your Website Structure
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Examining sitemap and content patterns...
</Typography>
</Box>
</Box>
<LinearProgress sx={{ height: 8, borderRadius: 4 }} />
</Paper>
);
}
return (
<Box>
{/* Header */}
<Paper sx={{
p: 3,
mb: 4,
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
borderRadius: 2,
boxShadow: '0 4px 12px rgba(3, 169, 244, 0.15)'
}}>
<Box display="flex" alignItems="center" gap={2} mb={2}>
<AssessmentIcon sx={{ color: '#667eea', fontSize: '2rem' }} />
<Box>
<Typography variant="h5" fontWeight={600} sx={{ color: '#1a202c !important' }}>
Website Structure Analysis
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Sitemap: {sitemapUrl}
</Typography>
{discoveryMethod && discoveryMethod !== 'fallback' && (
<Typography variant="caption" sx={{ color: '#059669 !important', fontStyle: 'italic' }}>
Discovered via {discoveryMethod.replace('_', ' ')}
</Typography>
)}
</Box>
</Box>
{/* Key Metrics */}
<Grid container spacing={3} mt={1}>
<Grid item xs={12} sm={6} md={3}>
<Box textAlign="center">
<Typography variant="h4" fontWeight={700} color="primary">
{structureAnalysis.total_urls || 0}
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Total URLs
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box textAlign="center">
<Typography variant="h4" fontWeight={700} color="primary">
{structureAnalysis.average_path_depth?.toFixed(1) || '0.0'}
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Avg. Path Depth
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box textAlign="center">
<Typography variant="h4" fontWeight={700} color="primary">
{contentTrends.publishing_velocity?.toFixed(2) || '0.00'}
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Posts/Day
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box textAlign="center">
<Typography variant="h4" fontWeight={700} color="primary">
{Object.keys(structureAnalysis.url_patterns || {}).length}
</Typography>
<Typography variant="body2" sx={{ color: '#4a5568 !important' }}>
Content Categories
</Typography>
</Box>
</Grid>
</Grid>
</Paper>
{/* Competitive Positioning */}
{onboardingInsights.competitive_positioning && (
<Paper sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '2px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '4px',
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)'
}
}}>
<Typography
variant="h6"
fontWeight={700}
sx={{
color: '#1a202c !important',
mb: 3,
display: 'flex',
alignItems: 'center',
gap: 1.5,
fontSize: '1.25rem'
}}
>
<Box sx={{
p: 1,
borderRadius: 2,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<BusinessIcon sx={{ color: '#ffffff !important', fontSize: '1.25rem' }} />
</Box>
Competitive Positioning
</Typography>
<Box sx={{
p: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
borderRadius: 2,
border: '1px solid #e2e8f0'
}}>
<Box sx={{
color: '#2d3748 !important',
lineHeight: 1.7,
fontSize: '1rem',
'& strong': {
color: '#1a202c !important',
fontWeight: 600
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
color: '#1a202c !important',
fontWeight: 600,
marginTop: 2,
marginBottom: 1
},
'& ul, & ol': {
paddingLeft: 3,
marginBottom: 2
},
'& li': {
marginBottom: 0.5
},
'& p': {
marginBottom: 1.5
}
}}>
{onboardingInsights.competitive_positioning?.split('\n').map((line, index) => {
// Handle bold text with **text**
if (line.includes('**')) {
const parts = line.split(/(\*\*.*?\*\*)/g);
return (
<Box key={index} sx={{ mb: line.trim() === '' ? 0 : 1 }}>
{parts.map((part, partIndex) => {
if (part.startsWith('**') && part.endsWith('**')) {
return (
<Box component="span" key={partIndex} sx={{ fontWeight: 600, color: '#1a202c !important' }}>
{part.slice(2, -2)}
</Box>
);
}
return part;
})}
</Box>
);
}
// Handle bullet points with -
if (line.trim().startsWith('- ')) {
return (
<Box key={index} sx={{
display: 'flex',
alignItems: 'flex-start',
mb: 0.5,
ml: 2
}}>
<Box sx={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: '#667eea',
mt: 0.75,
mr: 1.5,
flexShrink: 0
}} />
<Box sx={{ color: '#2d3748 !important' }}>
{line.replace(/^- /, '')}
</Box>
</Box>
);
}
// Handle numbered lists
if (/^\d+\./.test(line.trim())) {
return (
<Box key={index} sx={{
display: 'flex',
alignItems: 'flex-start',
mb: 0.5,
ml: 2
}}>
<Box sx={{
color: '#667eea',
fontWeight: 600,
mr: 1.5,
minWidth: '20px',
flexShrink: 0
}}>
{line.match(/^\d+\./)?.[0]}
</Box>
<Box sx={{ color: '#2d3748 !important' }}>
{line.replace(/^\d+\.\s*/, '')}
</Box>
</Box>
);
}
// Handle headings
if (line.trim().startsWith('#')) {
const level = line.match(/^#+/)?.[0].length || 1;
const text = line.replace(/^#+\s*/, '');
const Component = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
return (
<Box
key={index}
component={Component}
sx={{
color: '#1a202c !important',
fontWeight: 600,
fontSize: level === 1 ? '1.25rem' : level === 2 ? '1.125rem' : '1rem',
mt: index === 0 ? 0 : 2,
mb: 1
}}
>
{text}
</Box>
);
}
// Regular text
if (line.trim()) {
return (
<Box key={index} sx={{ mb: 1 }}>
{line}
</Box>
);
}
// Empty lines
return <Box key={index} sx={{ height: '8px' }} />;
})}
</Box>
</Box>
</Paper>
)}
{/* Content Gaps & Opportunities */}
<Grid container spacing={3} mb={3}>
{/* Content Gaps */}
{onboardingInsights.content_gaps && onboardingInsights.content_gaps.length > 0 && (
<Grid item xs={12} md={6}>
<Card sx={{
height: '100%',
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid #f59e0b',
boxShadow: '0 4px 12px rgba(245, 158, 11, 0.15)'
}}>
<CardContent>
<Typography
variant="h6"
fontWeight={600}
mb={2}
sx={{ color: '#1a202c !important' }}
>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#d97706 !important' }} />
Content Gaps
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{onboardingInsights.content_gaps.map((gap: string, index: number) => (
<Typography
key={index}
component="li"
variant="body2"
sx={{
mb: 1,
color: '#2d3748 !important',
lineHeight: 1.5
}}
>
{gap}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Grid>
)}
{/* Growth Opportunities */}
{onboardingInsights.growth_opportunities && onboardingInsights.growth_opportunities.length > 0 && (
<Grid item xs={12} md={6}>
<Card sx={{
height: '100%',
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid #10b981',
boxShadow: '0 4px 12px rgba(16, 185, 129, 0.15)'
}}>
<CardContent>
<Typography
variant="h6"
fontWeight={600}
mb={2}
sx={{ color: '#1a202c !important' }}
>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#059669 !important' }} />
Growth Opportunities
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{onboardingInsights.growth_opportunities.map((opportunity: string, index: number) => (
<Typography
key={index}
component="li"
variant="body2"
sx={{
mb: 1,
color: '#2d3748 !important',
lineHeight: 1.5
}}
>
{opportunity}
</Typography>
))}
</Box>
</CardContent>
</Card>
</Grid>
)}
</Grid>
{/* Strategic Recommendations */}
{onboardingInsights.strategic_recommendations && onboardingInsights.strategic_recommendations.length > 0 && (
<Paper sx={{
p: 3,
mb: 3,
background: 'linear-gradient(135deg, #fdf2f8 0%, #fce7f3 100%)',
border: '1px solid #ec4899',
borderRadius: 2
}}>
<Typography
variant="h6"
fontWeight={600}
mb={2}
sx={{ color: '#1a202c !important' }}
>
<LightbulbIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#be185d !important' }} />
Strategic Recommendations
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{onboardingInsights.strategic_recommendations.map((recommendation: string, index: number) => (
<Typography
key={index}
component="li"
variant="body1"
sx={{
mb: 1.5,
color: '#2d3748 !important',
lineHeight: 1.6
}}
>
{recommendation}
</Typography>
))}
</Box>
</Paper>
)}
{/* Content Categories */}
{structureAnalysis.url_patterns && Object.keys(structureAnalysis.url_patterns).length > 0 && (
<Paper sx={{
p: 4,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '2px solid #e2e8f0',
borderRadius: 3,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '4px',
background: 'linear-gradient(90deg, #64748b 0%, #475569 100%)'
}
}}>
<Typography
variant="h6"
fontWeight={700}
sx={{
color: '#1a202c !important',
mb: 3,
display: 'flex',
alignItems: 'center',
gap: 1.5,
fontSize: '1.25rem'
}}
>
<Box sx={{
p: 1,
borderRadius: 2,
background: 'linear-gradient(135deg, #64748b 0%, #475569 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<AnalyticsIcon sx={{ color: '#ffffff !important', fontSize: '1.25rem' }} />
</Box>
Content Categories
</Typography>
<Box display="flex" flexWrap="wrap" gap={1.5}>
{Object.entries(structureAnalysis.url_patterns)
.sort(([,a], [,b]) => (b as number) - (a as number))
.slice(0, 12)
.map(([category, count], index) => {
// Create different color schemes for variety
const colorSchemes = [
{ bg: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)', border: '#3b82f6', text: '#1e40af', hover: '#93c5fd' },
{ bg: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)', border: '#22c55e', text: '#15803d', hover: '#86efac' },
{ bg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)', border: '#f59e0b', text: '#d97706', hover: '#fcd34d' },
{ bg: 'linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)', border: '#ec4899', text: '#be185d', hover: '#f9a8d4' },
{ bg: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)', border: '#6366f1', text: '#4338ca', hover: '#a5b4fc' },
{ bg: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)', border: '#16a34a', text: '#15803d', hover: '#86efac' }
];
const scheme = colorSchemes[index % colorSchemes.length];
return (
<Chip
key={category}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: scheme.text }}>
{category}
</Typography>
<Typography variant="caption" sx={{
color: scheme.text,
opacity: 0.8,
fontWeight: 500,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
px: 1,
py: 0.25,
borderRadius: 1
}}>
{count}
</Typography>
</Box>
}
sx={{
background: scheme.bg,
border: `2px solid ${scheme.border}`,
color: scheme.text,
fontWeight: 600,
height: 'auto',
py: 1,
px: 1.5,
borderRadius: 2,
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
transition: 'all 0.2s ease-in-out',
'&:hover': {
background: scheme.hover,
transform: 'translateY(-2px)',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.15)',
},
'& .MuiChip-label': {
px: 0
}
}}
/>
);
})}
</Box>
</Paper>
)}
</Box>
);
};
export default SitemapAnalysisResults;

View File

@@ -0,0 +1,107 @@
/**
* SocialMediaPresenceSection Component
* Displays social media accounts and their links
*/
import React from 'react';
import {
Typography,
Grid,
Card,
CardContent,
Avatar,
Button,
Box
} from '@mui/material';
import {
Share as ShareIcon,
Facebook as FacebookIcon,
Instagram as InstagramIcon,
LinkedIn as LinkedInIcon,
YouTube as YouTubeIcon,
Twitter as TwitterIcon
} from '@mui/icons-material';
interface SocialMediaPresenceSectionProps {
socialMediaAccounts: { [key: string]: string };
}
const SocialMediaPresenceSection: React.FC<SocialMediaPresenceSectionProps> = ({
socialMediaAccounts
}) => {
// Don't render if no social media accounts
if (Object.keys(socialMediaAccounts).length === 0) {
return null;
}
const platformIcons: { [key: string]: React.ReactNode } = {
facebook: <FacebookIcon />,
instagram: <InstagramIcon />,
linkedin: <LinkedInIcon />,
youtube: <YouTubeIcon />,
twitter: <TwitterIcon />,
tiktok: <ShareIcon /> // Fallback icon for TikTok
};
return (
<>
<Typography
variant="h6"
gutterBottom
fontWeight={600}
mb={3}
sx={{ color: '#1a202c !important' }} // Force dark text
>
<ShareIcon sx={{ mr: 1, verticalAlign: 'middle', color: '#667eea !important' }} />
Social Media Presence
</Typography>
<Grid container spacing={2} mb={4}>
{Object.entries(socialMediaAccounts).map(([platform, url]) => {
if (!url) return null;
return (
<Grid item xs={12} sm={6} md={4} lg={3} xl={2} key={platform}>
<Card sx={{
height: '100%',
background: 'linear-gradient(135deg, #e0f2fe 0%, #b3e5fc 100%)',
border: '1px solid #81d4fa',
boxShadow: '0 4px 12px rgba(3, 169, 244, 0.15)',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(3, 169, 244, 0.25)'
}
}}>
<CardContent>
<Box display="flex" alignItems="center" gap={2}>
<Avatar sx={{ width: 40, height: 40, bgcolor: 'primary.main' }}>
{platformIcons[platform] || <ShareIcon />}
</Avatar>
<Box flex={1}>
<Typography variant="h6" fontWeight={600} textTransform="capitalize">
{platform}
</Typography>
<Button
variant="text"
size="small"
href={url as string}
target="_blank"
rel="noopener noreferrer"
sx={{ p: 0, minWidth: 'auto', textTransform: 'none' }}
>
View Profile
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
);
})}
</Grid>
</>
);
};
export default SocialMediaPresenceSection;

View File

@@ -9,3 +9,7 @@ export { default as EnhancedGuidelinesSection } from './EnhancedGuidelinesSectio
export { default as KeyInsightsGrid } from './KeyInsightsGrid';
export { default as ContentCharacteristicsSection } from './ContentCharacteristicsSection';
export { default as TargetAudienceAnalysisSection } from './TargetAudienceAnalysisSection';
export { default as SocialMediaPresenceSection } from './SocialMediaPresenceSection';
export { default as CompetitorsGrid } from './CompetitorsGrid';
export { default as SitemapAnalysisResults } from './SitemapAnalysisResults';
export type { Competitor } from './CompetitorsGrid';

View File

@@ -73,6 +73,8 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
p: 2.5,
mb: 0,
borderRadius: 2.5,
// Force high-contrast base color so nested text never inherits a light color
color: isDark ? '#ffffff' : '#1a202c',
background: isDark
? `linear-gradient(135deg, ${alpha(paletteColor.main, 0.08)} 0%, ${alpha(paletteColor.main, 0.04)} 100%)`
: `linear-gradient(135deg, ${alpha(paletteColor.main, 0.06)} 0%, ${alpha(paletteColor.light, 0.08)} 100%)`,
@@ -116,11 +118,12 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
<Typography
variant="caption"
sx={{
fontWeight: 700,
fontSize: '0.75rem',
letterSpacing: '0.5px',
fontWeight: 800,
fontSize: '0.78rem',
letterSpacing: '0.6px',
textTransform: 'uppercase',
color: isDark ? '#ffffff !important' : '#000000 !important', // Maximum contrast
color: isDark ? '#ffffff !important' : '#0f172a !important',
textShadow: isDark ? 'none' : '0 1px 0 rgba(255,255,255,0.6)',
mb: 0.5,
display: 'block'
}}
@@ -130,10 +133,10 @@ const KeyInsightCard: React.FC<KeyInsightProps> = ({
<Typography
variant="body1"
sx={{
fontWeight: 600,
fontSize: '1.05rem',
color: isDark ? '#ffffff !important' : '#000000 !important', // Maximum contrast
lineHeight: 1.4
fontWeight: 700,
fontSize: '1.1rem',
color: isDark ? '#ffffff !important' : '#0b1220 !important',
lineHeight: 1.35
}}
>
{Array.isArray(value) ? value.join(', ') : value}

View File

@@ -1,37 +1,23 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import {
Box,
Stepper,
Step,
StepLabel,
Button,
Typography,
Paper,
LinearProgress,
Fade,
Slide,
useTheme,
useMediaQuery,
IconButton,
Tooltip,
Container
useMediaQuery
} from '@mui/material';
import {
ArrowBack,
ArrowForward,
CheckCircle,
HelpOutline,
Close
} from '@mui/icons-material';
import UserBadge from '../shared/UserBadge';
import { startOnboarding, getCurrentStep, setCurrentStep, getProgress } from '../../api/onboarding';
import { getCurrentStep, setCurrentStep } from '../../api/onboarding';
import { apiClient } from '../../api/client';
import ApiKeyStep from './ApiKeyStep';
import WebsiteStep from './WebsiteStep';
import CompetitorAnalysisStep from './CompetitorAnalysisStep';
import PersonalizationStep from './PersonalizationStep';
import PersonaStep from './PersonaStep';
import IntegrationsStep from './IntegrationsStep';
import FinalStep from './FinalStep';
import { WizardHeader } from './common/WizardHeader';
import { WizardNavigation } from './common/WizardNavigation';
import { WizardLoadingState } from './common/WizardLoadingState';
const steps = [
{ label: 'API Keys', description: 'Connect your AI services', icon: '🔑' },
@@ -61,14 +47,116 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const [progressMessage, setProgressMessage] = useState('');
// sessionId removed - backend uses Clerk user ID from auth token
const [stepData, setStepData] = useState<any>(null);
const [competitorDataCollector, setCompetitorDataCollector] = useState<(() => any) | null>(null);
const [isCurrentStepValid, setIsCurrentStepValid] = useState<boolean>(false);
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
title: steps[0].label,
description: steps[0].description
});
// Step validation function
const isStepDataValid = useCallback((step: number, data: any): boolean => {
console.log(`Wizard: Validating step ${step} with data:`, data);
switch (step) {
case 0: // API Keys
const hasApiKeys = data && data.api_keys && Object.keys(data.api_keys).length > 0;
console.log(`Wizard: Step 0 (API Keys) validation:`, hasApiKeys);
return hasApiKeys;
case 1: // Website Analysis
const hasWebsite = data && (data.website || data.website_url);
console.log(`Wizard: Step 1 (Website) validation:`, hasWebsite);
return hasWebsite;
case 2: // Competitor Analysis
const hasCompetitorData = data && (data.competitors || data.researchSummary || data.sitemapAnalysis);
console.log(`Wizard: Step 2 (Competitor Analysis) validation:`, hasCompetitorData, 'Data keys:', data ? Object.keys(data) : 'no data');
return hasCompetitorData;
case 3: // Persona Generation
const hasValidPersonaData = data &&
data.corePersona &&
data.platformPersonas &&
Object.keys(data.platformPersonas).length > 0 &&
data.qualityMetrics;
console.log(`Wizard: Step 3 (Persona Generation) validation:`, {
hasValidPersonaData,
hasCorePersona: !!(data && data.corePersona),
hasPlatformPersonas: !!(data && data.platformPersonas),
platformPersonasCount: data && data.platformPersonas ? Object.keys(data.platformPersonas).length : 0,
hasQualityMetrics: !!(data && data.qualityMetrics),
dataKeys: data ? Object.keys(data) : 'no data'
});
return hasValidPersonaData;
case 4: // Integrations
console.log(`Wizard: Step 4 (Integrations) validation: always true (optional)`);
return true; // Integrations step is optional
case 5: // Final Step
console.log(`Wizard: Step 5 (Final) validation: always true`);
return true; // Final step is always valid
default:
console.log(`Wizard: Unknown step ${step} validation: false`);
return false;
}
}, []);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Use refs to avoid dependency cycles
const stepDataRef = useRef(stepData);
const competitorDataCollectorRef = useRef(competitorDataCollector);
const personaStepRef = useRef<{ handleContinue: () => void } | null>(null);
// Keep refs in sync with state
useEffect(() => {
stepDataRef.current = stepData;
console.log('Wizard: stepData changed:', stepData);
}, [stepData]);
useEffect(() => {
competitorDataCollectorRef.current = competitorDataCollector;
console.log('Wizard: competitorDataCollector changed:', competitorDataCollector);
}, [competitorDataCollector]);
// Validate current step data
useEffect(() => {
console.log(`Wizard: Validation effect triggered - activeStep: ${activeStep}, stepData:`, stepData);
console.log(`Wizard: stepData type:`, typeof stepData, 'keys:', stepData ? Object.keys(stepData) : 'no data');
// For CompetitorAnalysisStep, also check the competitorDataCollector data
let dataToValidate = stepData;
if (activeStep === 2 && competitorDataCollector) {
console.log(`Wizard: Using competitorDataCollector data for validation:`, competitorDataCollector);
dataToValidate = competitorDataCollector;
}
const isValid = isStepDataValid(activeStep, dataToValidate);
console.log(`Wizard: Validation result for step ${activeStep}:`, isValid);
console.log(`Wizard: Setting isCurrentStepValid to:`, isValid);
setIsCurrentStepValid(isValid);
}, [activeStep, stepData, isStepDataValid, competitorDataCollector]);
// Debug: log all state changes
useEffect(() => {
console.log('Wizard: Render triggered - activeStep:', activeStep, 'direction:', direction);
}, [activeStep, direction]);
// Memoize the onDataReady callback to prevent infinite loops
const handleCompetitorDataReady = useCallback((dataCollector: (() => any) | undefined) => {
console.log('Wizard: onDataReady called with:', dataCollector);
console.log('Wizard: dataCollector type:', typeof dataCollector);
if (typeof dataCollector === 'function') {
setCompetitorDataCollector(dataCollector);
} else {
console.error('Wizard: dataCollector is not a function:', dataCollector);
}
}, []);
useEffect(() => {
console.log('Wizard: Component mounted');
const init = async () => {
@@ -82,21 +170,39 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
if (cachedInit) {
console.log('Wizard: Using cached init data from batch endpoint');
const data = JSON.parse(cachedInit);
// Extract data from batch response
const { user, onboarding, session } = data;
const { onboarding, session } = data;
// Load step data, especially research data from step 3 and persona data from step 4
if (onboarding.steps && Array.isArray(onboarding.steps)) {
// Load research preferences from step 3
const step3Data = onboarding.steps.find((step: any) => step.step_number === 3);
if (step3Data && step3Data.data) {
console.log('Wizard: Loading research data from step 3:', Object.keys(step3Data.data));
setStepData((prevData: any) => ({ ...prevData, ...step3Data.data }));
}
// Load persona data from step 4
const step4Data = onboarding.steps.find((step: any) => step.step_number === 4);
if (step4Data && step4Data.data) {
console.log('Wizard: Loading persona data from step 4:', Object.keys(step4Data.data));
setStepData((prevData: any) => ({ ...prevData, ...step4Data.data }));
}
}
// Set state from cached data - NO API CALLS NEEDED!
setActiveStep(onboarding.current_step - 1);
setProgressState(onboarding.completion_percentage);
// Note: Session managed by Clerk auth, no need to track separately
console.log('Wizard: Initialized from cache:', {
step: onboarding.current_step,
progress: onboarding.completion_percentage,
userId: session.session_id // Clerk user ID from backend
userId: session.session_id, // Clerk user ID from backend
hasPersonaData: !!stepData
});
setLoading(false);
return; // ← Skip redundant API calls!
}
@@ -104,20 +210,38 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// Fallback: If no cached data (shouldn't happen), make batch call
console.log('Wizard: No cached data, making batch init call');
const response = await apiClient.get('/api/onboarding/init');
const { user, onboarding, session } = response.data;
const { onboarding, session } = response.data;
// Load step data, especially research data from step 3 and persona data from step 4
if (onboarding.steps && Array.isArray(onboarding.steps)) {
// Load research preferences from step 3
const step3Data = onboarding.steps.find((step: any) => step.step_number === 3);
if (step3Data && step3Data.data) {
console.log('Wizard: Loading research data from step 3 API call:', Object.keys(step3Data.data));
setStepData((prevData: any) => ({ ...prevData, ...step3Data.data }));
}
// Load persona data from step 4
const step4Data = onboarding.steps.find((step: any) => step.step_number === 4);
if (step4Data && step4Data.data) {
console.log('Wizard: Loading persona data from step 4 API call:', Object.keys(step4Data.data));
setStepData((prevData: any) => ({ ...prevData, ...step4Data.data }));
}
}
// Cache for future use
sessionStorage.setItem('onboarding_init', JSON.stringify(response.data));
// Set state from API response
setActiveStep(onboarding.current_step - 1);
setProgressState(onboarding.completion_percentage);
// Note: Session managed by Clerk auth, no need to track separately
console.log('Wizard: Initialized from API:', {
step: onboarding.current_step,
progress: onboarding.completion_percentage,
userId: session.session_id // Clerk user ID from backend
userId: session.session_id, // Clerk user ID from backend
hasPersonaData: !!stepData
});
} catch (error) {
console.error('Error initializing onboarding:', error);
@@ -126,9 +250,16 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
}
};
init();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Run only once on mount - stepData is used for logging only
const handleNext = async (rawStepData?: any) => {
const handleNext = useCallback(async (rawStepData?: any) => {
console.log('Wizard: handleNext called');
console.log('Wizard: Current step:', activeStep);
console.log('Wizard: Step data:', stepDataRef.current);
console.log('Wizard: competitorDataCollector:', competitorDataCollectorRef.current);
console.log('Wizard: competitorDataCollector type:', typeof competitorDataCollectorRef.current);
if (rawStepData && typeof rawStepData === 'object') {
if (typeof rawStepData.preventDefault === 'function') {
rawStepData.preventDefault();
@@ -138,10 +269,110 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
}
}
const currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData
let currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData
? undefined
: rawStepData;
// Special handling for CompetitorAnalysisStep (step 2)
if (activeStep === 2) {
console.log('Wizard: Handling CompetitorAnalysisStep data...');
// If we have data from onContinue, use it
if (currentStepData) {
console.log('Wizard: Using data from CompetitorAnalysisStep onContinue:', currentStepData);
} else {
// Fallback: try to get data from collector
const collector = competitorDataCollectorRef.current;
if (collector && typeof collector === 'function') {
console.log('Wizard: Collecting data from CompetitorAnalysisStep collector...');
currentStepData = collector();
} else if (collector && typeof collector === 'object') {
console.warn('Wizard: competitorDataCollector is an object; using it directly as step data');
currentStepData = collector;
} else {
console.warn('Wizard: competitorDataCollector not available; using empty data');
// Fallback: create minimal data structure to prevent errors
const currentData = stepDataRef.current;
currentStepData = {
competitors: [],
researchSummary: null,
sitemapAnalysis: null,
userUrl: currentData?.website || '',
industryContext: currentData?.industryContext,
analysisTimestamp: new Date().toISOString()
};
}
}
}
// Merge research data with existing step data for CompetitorAnalysisStep
if (activeStep === 2 && currentStepData) {
console.log('Wizard: Merging CompetitorAnalysisStep data with existing step data...');
// Merge research data with existing step data
const currentData = stepDataRef.current || {};
const researchData = currentStepData || {};
// Ensure we have research data
if (researchData.competitors || researchData.researchSummary || researchData.sitemapAnalysis) {
currentStepData = {
...currentData, // Preserve existing data (website, etc.)
...researchData, // Add/update research data
// Ensure all required research fields are present
competitors: researchData.competitors || currentData.competitors,
researchSummary: researchData.researchSummary || currentData.researchSummary,
sitemapAnalysis: researchData.sitemapAnalysis || currentData.sitemapAnalysis,
// Mark this as the research step
stepType: 'research',
completedAt: new Date().toISOString()
};
console.log('Wizard: Merged research data:', currentStepData);
} else {
console.warn('Wizard: No research data provided, using existing step data');
currentStepData = currentData;
}
}
// Special handling for PersonaStep (step 3)
if (activeStep === 3) {
console.log('Wizard: Handling PersonaStep data...');
// If we have data from onContinue, use it
if (currentStepData && currentStepData.corePersona && currentStepData.qualityMetrics) {
console.log('Wizard: Using persona data from PersonaStep onContinue:', currentStepData);
// Data is already in currentStepData, no need to modify it
} else {
// Check if we have valid persona data in stepData
const currentData = stepDataRef.current || {};
const hasValidPersonaData = currentData.corePersona &&
currentData.platformPersonas &&
Object.keys(currentData.platformPersonas).length > 0 &&
currentData.qualityMetrics;
console.log('Wizard: Persona data validation:', {
hasCorePersona: !!currentData.corePersona,
hasPlatformPersonas: !!currentData.platformPersonas,
platformPersonasCount: currentData.platformPersonas ? Object.keys(currentData.platformPersonas).length : 0,
hasQualityMetrics: !!currentData.qualityMetrics,
hasValidPersonaData
});
if (hasValidPersonaData) {
console.log('Wizard: Using existing valid persona data from stepData');
currentStepData = currentData;
} else {
console.warn('Wizard: No valid persona data available for PersonaStep - cannot complete step');
// Don't try to complete the step if we don't have valid persona data
console.log('Wizard: Aborting step completion - missing valid persona data');
setLoading(false);
setShowProgressMessage(false);
setProgressMessage('');
return;
}
}
}
// Store step data in state
if (currentStepData) {
setStepData(currentStepData);
@@ -169,7 +400,31 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// Complete the current step (activeStep + 1 because steps are 1-indexed)
const currentStepNumber = activeStep + 1;
const stepWasCompleted = currentStepData && typeof currentStepData === 'object' && (currentStepData.website || currentStepData.businessData);
const stepWasCompleted = currentStepData && typeof currentStepData === 'object' && (
currentStepData.website ||
currentStepData.businessData ||
currentStepData.competitors ||
currentStepData.researchSummary ||
currentStepData.sitemapAnalysis ||
currentStepData.corePersona ||
currentStepData.platformPersonas ||
currentStepData.qualityMetrics
);
console.log('Wizard: Step completion check:', {
currentStepNumber,
hasData: !!currentStepData,
dataKeys: currentStepData ? Object.keys(currentStepData) : [],
stepWasCompleted,
website: !!currentStepData?.website,
businessData: !!currentStepData?.businessData,
competitors: !!currentStepData?.competitors,
researchSummary: !!currentStepData?.researchSummary,
sitemapAnalysis: !!currentStepData?.sitemapAnalysis,
corePersona: !!currentStepData?.corePersona,
platformPersonas: !!currentStepData?.platformPersonas,
qualityMetrics: !!currentStepData?.qualityMetrics
});
if (!stepWasCompleted) {
console.warn('Wizard: No serialized step data supplied; skipping backend completion for step', currentStepNumber);
@@ -204,9 +459,9 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
} else {
console.log('Wizard: Not the final step, continuing to next step');
}
};
}, [activeStep, onComplete]);
const handleBack = async () => {
const handleBack = useCallback(async () => {
setDirection('left');
const prevStep = activeStep - 1;
setActiveStep(prevStep);
@@ -216,7 +471,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// Update progress
const newProgress = ((prevStep + 1) / steps.length) * 100;
setProgressState(newProgress);
};
}, [activeStep]);
const handleStepClick = (stepIndex: number) => {
if (stepIndex <= activeStep) {
@@ -227,10 +482,15 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
};
const updateHeaderContent = useCallback((content: StepHeaderContent) => {
setStepHeaderContent(content);
setStepHeaderContent(prev => {
if (prev.title === content.title && prev.description === content.description) {
return prev;
}
return content;
});
}, []);
const handleComplete = async () => {
const handleComplete = useCallback(async () => {
console.log('Wizard: handleComplete called - completing onboarding');
try {
// Call onComplete to notify parent component
@@ -238,11 +498,24 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
} catch (error) {
console.error('Error completing onboarding:', error);
}
};
}, [onComplete]);
// Memoize data objects passed as props to avoid recreating them each render
const personaOnboardingData = useMemo(() => ({
websiteAnalysis: stepData?.analysis,
competitorResearch: stepData?.competitors,
sitemapAnalysis: stepData?.sitemapAnalysis,
businessData: stepData?.businessData
}), [stepData?.analysis, stepData?.competitors, stepData?.sitemapAnalysis, stepData?.businessData]);
const personaStepData = useMemo(() => ({
corePersona: stepData?.corePersona,
platformPersonas: stepData?.platformPersonas,
qualityMetrics: stepData?.qualityMetrics,
selectedPlatforms: stepData?.selectedPlatforms
}), [stepData?.corePersona, stepData?.platformPersonas, stepData?.qualityMetrics, stepData?.selectedPlatforms]);
const renderStepContent = (step: number) => {
console.log('Wizard: renderStepContent called with step:', step, 'stepData:', stepData);
const stepComponents = [
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
@@ -252,14 +525,21 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
onBack={handleBack}
userUrl={stepData?.website || ''}
industryContext={stepData?.industryContext}
onDataReady={handleCompetitorDataReady}
/>,
<PersonaStep
key="personalization"
onContinue={handleNext}
updateHeaderContent={updateHeaderContent}
onboardingData={personaOnboardingData}
stepData={personaStepData}
/>,
<PersonalizationStep key="personalization" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<IntegrationsStep key="integrations" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
];
return (
<Slide direction={direction} in={true} mountOnEnter unmountOnExit>
<Slide direction={direction} in={true} mountOnEnter unmountOnExit key={`step-${step}`}>
<Box sx={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
{stepComponents[step]}
</Box>
@@ -267,49 +547,9 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
);
};
// Show loading state if loading
if (loading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<Fade in={true}>
<Paper
elevation={24}
sx={{
p: 4,
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
maxWidth: 400,
width: '100%',
}}
>
<Typography variant="h5" align="center" gutterBottom sx={{ fontWeight: 600 }}>
Setting up your workspace...
</Typography>
<LinearProgress
sx={{
mt: 3,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
}}
/>
</Paper>
</Fade>
</Box>
);
return <WizardLoadingState loading={loading} />;
}
return (
@@ -337,10 +577,10 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
<Paper
elevation={24}
sx={{
maxWidth: { xs: '100%', md: '1200px' },
maxWidth: '100%',
width: '100%',
borderRadius: 4,
overflow: 'hidden',
overflow: 'visible',
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
@@ -349,276 +589,37 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
}}
>
{/* Header with Stepper */}
<Box
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
p: { xs: 3, md: 4 },
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
{/* Progress Message */}
{showProgressMessage && (
<Fade in={showProgressMessage}>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
background: 'rgba(16, 185, 129, 0.9)',
color: 'white',
p: 2,
textAlign: 'center',
zIndex: 10,
backdropFilter: 'blur(10px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{progressMessage}
</Typography>
</Box>
</Fade>
)}
{/* Top Row - Title and Actions */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ flex: 1 }}>
<UserBadge colorMode="dark" />
</Box>
<Box sx={{ flex: 2, textAlign: 'center' }}>
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
{stepHeaderContent.title}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, flex: 1, justifyContent: 'flex-end' }}>
<Tooltip title="Get Help" arrow>
<IconButton
onClick={() => setShowHelp(!showHelp)}
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<HelpOutline />
</IconButton>
</Tooltip>
<Tooltip title="Skip for now" arrow>
<IconButton
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<Close />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Progress Bar */}
<Box sx={{ mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 500 }}>
Setup Progress
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 600 }}>
{Math.round(progress)}% Complete
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.2)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}
}}
/>
</Box>
{/* Stepper in Header */}
<Box sx={{ position: 'relative', zIndex: 1 }}>
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
sx={{
'& .MuiStepLabel-root': {
cursor: 'pointer',
},
'& .MuiStepLabel-label': {
fontSize: '0.875rem',
fontWeight: 600,
color: 'white',
},
'& .MuiStepLabel-labelContainer': {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
'& .MuiStepLabel-label.Mui-completed': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiStepLabel-label.Mui-active': {
color: 'white',
},
'& .MuiStepLabel-label.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
}}
>
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel
onClick={() => handleStepClick(index)}
sx={{
cursor: index <= activeStep ? 'pointer' : 'default',
'& .MuiStepLabel-iconContainer': {
background: index <= activeStep
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50%',
width: 40,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: index <= activeStep ? 'white' : 'rgba(255, 255, 255, 0.6)',
fontSize: '1.2rem',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: index <= activeStep
? '0 4px 12px rgba(255, 255, 255, 0.2)'
: 'none',
'&:hover': {
transform: index <= activeStep ? 'scale(1.05)' : 'none',
boxShadow: index <= activeStep
? '0 6px 16px rgba(255, 255, 255, 0.3)'
: 'none',
}
},
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="h6" sx={{ mb: 0.5 }}>
{step.icon}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'center' }}>
{step.label}
</Typography>
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
</Box>
<WizardHeader
activeStep={activeStep}
progress={progress}
stepHeaderContent={stepHeaderContent}
showProgressMessage={showProgressMessage}
progressMessage={progressMessage}
showHelp={showHelp}
isMobile={isMobile}
steps={steps}
onStepClick={handleStepClick}
onHelpToggle={() => setShowHelp(!showHelp)}
/>
{/* Content */}
<Box sx={{ p: { xs: 2, md: 3 }, pt: 2 }}>
<Box sx={{ p: { xs: 1, md: 2 }, pt: 1, width: '100%', overflow: 'visible' }}>
<Fade in={true} timeout={400}>
<Box>
<Box sx={{ width: '100%', overflow: 'visible' }}>
{renderStepContent(activeStep)}
</Box>
</Fade>
</Box>
{/* Navigation */}
<Box
sx={{
p: { xs: 2, md: 3 },
pt: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderTop: '1px solid rgba(0,0,0,0.08)',
background: 'rgba(0,0,0,0.02)',
}}
>
<Button
variant="outlined"
onClick={handleBack}
disabled={activeStep === 0}
startIcon={<ArrowBack />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
}}
>
Back
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.7, fontWeight: 500 }}>
Step {activeStep + 1} of {steps.length}
</Typography>
{activeStep === steps.length - 1 && (
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
)}
</Box>
<Button
variant="contained"
onClick={handleNext}
disabled={activeStep === steps.length - 1}
endIcon={activeStep === steps.length - 1 ? <CheckCircle /> : <ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
{activeStep === steps.length - 1 ? 'Complete Setup' : 'Continue'}
</Button>
</Box>
<WizardNavigation
activeStep={activeStep}
totalSteps={steps.length}
onBack={handleBack}
onNext={handleNext}
isLastStep={activeStep === steps.length - 1}
isCurrentStepValid={isCurrentStepValid}
/>
</Paper>
</Box>
);

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { Paper, Typography, Grid, Stack, Box } from '@mui/material';
import { AutoAwesome as AutoAwesomeIcon, TrendingUp as TrendingUpIcon, ContentPaste as ContentPasteIcon } from '@mui/icons-material';
const BenefitsSummary: React.FC = () => {
return (
<Paper
sx={{
p: 3,
backgroundColor: '#f8fafc',
border: '1px solid #e2e8f0',
borderRadius: 2
}}
>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 2 }}>
Why Connect Your Platforms?
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<AutoAwesomeIcon sx={{ color: '#3b82f6', mt: 0.5 }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#1e293b' }}>
Automated Publishing
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
AI automatically publishes optimized content to your connected platforms
</Typography>
</Box>
</Stack>
</Grid>
<Grid item xs={12} md={4}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<TrendingUpIcon sx={{ color: '#10b981', mt: 0.5 }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#1e293b' }}>
Performance Analytics
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Track content performance across all platforms with unified analytics
</Typography>
</Box>
</Stack>
</Grid>
<Grid item xs={12} md={4}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<ContentPasteIcon sx={{ color: '#8b5cf6', mt: 0.5 }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#1e293b' }}>
Content Optimization
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
AI continuously optimizes content based on platform-specific performance data
</Typography>
</Box>
</Stack>
</Grid>
</Grid>
</Paper>
);
};
export default BenefitsSummary;

View File

@@ -0,0 +1,295 @@
import React from 'react';
import {
Box,
Typography,
Card,
Grid,
Chip,
Stack,
Fade
} from '@mui/material';
import {
Schedule as ScheduleIcon,
AutoAwesome as AutoAwesomeIcon,
Instagram as InstagramIcon
} from '@mui/icons-material';
interface ComingSoonSectionProps {
title?: string;
description?: string;
timeout?: number;
}
const ComingSoonSection: React.FC<ComingSoonSectionProps> = ({
title = "🚀 Coming Soon",
description = "Advanced integrations and features currently in development",
timeout = 1400
}) => {
return (
<Fade in timeout={timeout}>
<div>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 2 }}>
{title}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', mb: 3 }}>
{description}
</Typography>
<Grid container spacing={2}>
{/* LinkedIn & Facebook OAuth Approval */}
<Grid item xs={12} md={6}>
<Card sx={{
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
borderRadius: 2,
p: 2
}}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: 2,
backgroundColor: '#fef3c7',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<ScheduleIcon sx={{ color: '#f59e0b' }} />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
Social Media OAuth
</Typography>
<Chip
label="Awaiting Approval"
size="small"
sx={{
backgroundColor: '#fef3c7',
color: '#92400e',
border: '1px solid #f59e0b',
fontSize: '0.75rem'
}}
/>
</Box>
</Stack>
<Typography variant="body2" sx={{ color: '#64748b', mb: 2 }}>
LinkedIn and Facebook posting capabilities are pending platform approval for OAuth integration.
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
<Chip
label="LinkedIn Posts"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Facebook Posts"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Auto-scheduling"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Stack>
</Card>
</Grid>
{/* WordPress Development */}
<Grid item xs={12} md={6}>
<Card sx={{
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
borderRadius: 2,
p: 2
}}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: 2,
backgroundColor: '#f0f9ff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<AutoAwesomeIcon sx={{ color: '#3b82f6' }} />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
WordPress Integration
</Typography>
<Chip
label="In Development"
size="small"
sx={{
backgroundColor: '#f0f9ff',
color: '#0c4a6e',
border: '1px solid #3b82f6',
fontSize: '0.75rem'
}}
/>
</Box>
</Stack>
<Typography variant="body2" sx={{ color: '#64748b', mb: 2 }}>
Advanced WordPress integration with media management and SEO optimization features.
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
<Chip
label="Media Library"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="SEO Tools"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Auto-publish"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Stack>
</Card>
</Grid>
{/* Instagram Planned */}
<Grid item xs={12}>
<Card sx={{
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
borderRadius: 2,
p: 2
}}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: 2,
backgroundColor: '#f3f4f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<InstagramIcon sx={{ color: '#6b7280' }} />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
Instagram Integration
</Typography>
<Chip
label="Planned"
size="small"
sx={{
backgroundColor: '#f3f4f6',
color: '#374151',
border: '1px solid #9ca3af',
fontSize: '0.75rem'
}}
/>
</Box>
</Stack>
<Typography variant="body2" sx={{ color: '#64748b', mb: 2 }}>
Instagram posting and story creation capabilities are planned for future releases.
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
<Chip
label="Post Creation"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Story Posts"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Hashtag Optimization"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Stack>
</Card>
</Grid>
</Grid>
</Box>
</div>
</Fade>
);
};
export default ComingSoonSection;

View File

@@ -0,0 +1,259 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Card,
CardContent,
TextField,
InputAdornment,
Fade,
Stack,
Chip,
Tooltip,
Alert
} from '@mui/material';
import {
Email as EmailIcon,
Business as BusinessIcon,
TrendingUp as TrendingUpIcon,
Notifications as NotificationsIcon,
Security as SecurityIcon,
Verified as VerifiedIcon
} from '@mui/icons-material';
interface EmailSectionProps {
email: string;
onEmailChange: (email: string) => void;
}
const EmailSection: React.FC<EmailSectionProps> = ({ email, onEmailChange }) => {
const [showBenefits, setShowBenefits] = useState<boolean>(false);
return (
<Fade in timeout={400} mountOnEnter unmountOnExit>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 2 }}>
📧 Your Business Email Address
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', mb: 3 }}>
Help us send you personalized business insights, daily tasks, and growth opportunities
</Typography>
<Card sx={{
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
borderRadius: 2,
mb: 3
}}>
<CardContent sx={{ p: 3 }}>
<TextField
fullWidth
label="Email Address"
value={email}
onChange={(e) => onEmailChange(e.target.value)}
placeholder="your@business.com"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<EmailIcon sx={{ color: '#64748b' }} />
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: '#3b82f6',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#3b82f6',
},
},
'& .MuiInputBase-input': {
color: '#1e293b',
fontWeight: 500,
fontSize: '1rem',
},
'& .MuiInputLabel-root': {
color: '#64748b',
},
'& .MuiInputLabel-root.Mui-focused': {
color: '#3b82f6',
},
}}
/>
{/* Progressive Disclosure - Benefits Section */}
<Box
sx={{
mt: 3,
cursor: 'pointer',
'&:hover': {
'& .benefits-trigger': {
color: '#3b82f6',
}
}
}}
onMouseEnter={() => setShowBenefits(true)}
onMouseLeave={() => setShowBenefits(false)}
>
<Typography
variant="subtitle2"
className="benefits-trigger"
sx={{
fontWeight: 600,
color: '#64748b',
transition: 'color 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: 1
}}
>
Why we need your email:
<Box sx={{
width: 16,
height: 16,
borderRadius: '50%',
backgroundColor: '#e2e8f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
color: '#64748b'
}}>
?
</Box>
</Typography>
{/* Benefits Content - Shows on Hover */}
<Fade in={showBenefits} timeout={300} mountOnEnter unmountOnExit>
<Box sx={{ mt: 2 }}>
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1} sx={{ mb: 2 }}>
<Tooltip title="Get daily AI-generated tasks to review and approve for your business growth" placement="top">
<Chip
icon={<BusinessIcon />}
label="Daily Business Tasks"
size="small"
sx={{
backgroundColor: '#f0f9ff',
color: '#0c4a6e',
border: '1px solid #0ea5e9',
'&:hover': {
backgroundColor: '#e0f2fe',
}
}}
/>
</Tooltip>
<Tooltip title="Receive personalized content strategies and performance insights" placement="top">
<Chip
icon={<TrendingUpIcon />}
label="Growth Insights"
size="small"
sx={{
backgroundColor: '#f0fdf4',
color: '#0c4a6e',
border: '1px solid #10b981',
'&:hover': {
backgroundColor: '#dcfce7',
}
}}
/>
</Tooltip>
<Tooltip title="Get notified about new features, resources, and business opportunities" placement="top">
<Chip
icon={<NotificationsIcon />}
label="Feature Updates"
size="small"
sx={{
backgroundColor: '#fef3c7',
color: '#92400e',
border: '1px solid #f59e0b',
'&:hover': {
backgroundColor: '#fef3c7',
}
}}
/>
</Tooltip>
<Tooltip title="Your email is secure and we never spam - only business-focused content" placement="top">
<Chip
icon={<SecurityIcon />}
label="No Spam Promise"
size="small"
sx={{
backgroundColor: '#f3f4f6',
color: '#374151',
border: '1px solid #9ca3af',
'&:hover': {
backgroundColor: '#e5e7eb',
}
}}
/>
</Tooltip>
</Stack>
{/* AI-First Platform Message */}
<Alert
severity="info"
sx={{
mb: 2,
backgroundColor: '#f0f9ff',
border: '1px solid #0ea5e9',
borderRadius: 2,
'& .MuiAlert-icon': {
color: '#0ea5e9'
}
}}
>
<Stack direction="row" spacing={2} alignItems="center">
<VerifiedIcon sx={{ color: '#0ea5e9' }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#0c4a6e' }}>
AI-First, Human-Approved Platform
</Typography>
<Typography variant="body2" sx={{ color: '#0c4a6e', mt: 0.5 }}>
We generate tasks and insights, but you stay in control. Your email helps us send you
the right opportunities to review and approve for maximum business growth.
</Typography>
</Box>
</Stack>
</Alert>
{/* Security & Privacy Message */}
<Alert
severity="info"
sx={{
backgroundColor: '#f0f9ff',
border: '1px solid #0ea5e9',
borderRadius: 2,
'& .MuiAlert-icon': {
color: '#0ea5e9'
}
}}
>
<Stack direction="row" spacing={2} alignItems="center">
<SecurityIcon sx={{ color: '#0ea5e9' }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#0c4a6e' }}>
Your Data is Secure & Private
</Typography>
<Typography variant="body2" sx={{ color: '#0c4a6e', mt: 0.5 }}>
We use OAuth 2.0 for secure connections. Your credentials are never stored.
You can revoke access anytime from your account settings.
</Typography>
</Box>
</Stack>
</Alert>
</Box>
</Fade>
</Box>
</CardContent>
</Card>
</Box>
</Fade>
);
};
export default EmailSection;

View File

@@ -0,0 +1,204 @@
import React from 'react';
import {
Box,
Button,
Typography,
Card,
CardContent,
Chip,
IconButton,
Tooltip
} from '@mui/material';
import {
Google as GoogleIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
import { gscAPI, type GSCSite } from '../../../api/gsc';
interface GSCPlatformCardProps {
platform: {
id: string;
name: string;
description: string;
icon: React.ReactNode;
status: string;
};
gscSites: GSCSite[] | null;
isLoading: boolean;
onConnect: (platformId: string) => void;
getStatusIcon: (status: string) => React.ReactElement;
getStatusText: (status: string) => string;
getStatusColor: (status: string) => string;
onRefresh?: () => void;
}
const GSCPlatformCard: React.FC<GSCPlatformCardProps> = ({
platform,
gscSites,
isLoading,
onConnect,
getStatusIcon,
getStatusText,
getStatusColor,
onRefresh
}) => {
const handleRefresh = () => {
if (onRefresh) {
onRefresh();
}
};
return (
<Card
sx={{
height: '100%',
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
transform: 'translateY(-2px)'
}
}}
>
<CardContent sx={{ p: 2.5 }}>
{/* Header */}
<Box display="flex" alignItems="center" mb={2}>
<Box sx={{ color: '#64748b', mr: 1 }}>
{platform.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
{platform.name}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', fontSize: '0.875rem' }}>
{platform.description}
</Typography>
</Box>
<Chip
icon={getStatusIcon(platform.status)}
label={getStatusText(platform.status)}
color={getStatusColor(platform.status) as any}
size="small"
/>
</Box>
{/* Connected Sites Display */}
{platform.status === 'connected' && gscSites && gscSites.length > 0 && (
<Box mb={2}>
<Typography variant="body2" sx={{ fontWeight: 500, color: '#1e293b', mb: 1 }}>
Connected Sites:
</Typography>
{gscSites.map((site, index) => (
<Box
key={index}
sx={{
p: 1.5,
border: '1px solid #e2e8f0',
borderRadius: 1,
backgroundColor: '#f8fafc',
fontSize: '0.875rem',
color: '#475569',
fontFamily: 'monospace',
mb: 1
}}
>
{site.siteUrl}
</Box>
))}
</Box>
)}
{/* Features as Chips */}
<Box mb={2} sx={{ minHeight: '32px' }}>
<Box display="flex" flexWrap="wrap" gap={0.5}>
<Chip
label="SEO analytics"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Search performance"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Content optimization"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Box>
</Box>
{/* Actions */}
<Box display="flex" gap={1}>
{platform.status === 'connected' ? (
<>
<Button
variant="outlined"
size="small"
onClick={() => onConnect(platform.id)}
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: '#e2e8f0',
color: '#64748b',
flex: 1
}}
>
Reconnect
</Button>
<Tooltip title="Refresh status">
<IconButton
onClick={handleRefresh}
disabled={isLoading}
size="small"
sx={{ color: '#64748b' }}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</>
) : (
<Button
variant="contained"
size="small"
onClick={() => onConnect(platform.id)}
disabled={isLoading}
sx={{
textTransform: 'none',
fontWeight: 600,
flex: 1
}}
>
Connect GSC
</Button>
)}
</Box>
</CardContent>
</Card>
);
};
export default GSCPlatformCard;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { Card, CardContent, Stack, Box, Typography, Chip, Button, CircularProgress } from '@mui/material';
import { CheckCircle as CheckIcon, Launch as LaunchIcon, Schedule as ScheduleIcon, Error as ErrorIcon } from '@mui/icons-material';
export interface PlatformCardProps {
id: string;
name: string;
description: string;
icon: React.ReactNode;
status: 'available' | 'connected' | 'coming_soon' | 'disabled';
features: string[];
isEnabled: boolean;
isLoading: boolean;
onConnect: (platformId: string) => void;
}
const getStatusColor = (status: string) => {
switch (status) {
case 'connected': return 'success';
case 'available': return 'primary';
case 'coming_soon': return 'warning';
case 'disabled': return 'default';
default: return 'default';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <CheckIcon />;
case 'available': return <LaunchIcon />;
case 'coming_soon': return <ScheduleIcon />;
case 'disabled': return <ErrorIcon />;
default: return <LaunchIcon />;
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'connected': return 'Connected';
case 'available': return 'Connect';
case 'coming_soon': return 'Coming Soon';
case 'disabled': return 'Disabled';
default: return 'Unknown';
}
};
const PlatformCard: React.FC<PlatformCardProps> = ({ id, name, description, icon, status, features, isEnabled, isLoading, onConnect }) => {
return (
<Card
sx={{
height: '100%',
border: status === 'connected' ? '2px solid #10b981' : '1px solid #e2e8f0',
backgroundColor: status === 'connected' ? '#f0fdf4' : '#ffffff',
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: isEnabled ? '0 4px 12px rgba(0, 0, 0, 0.1)' : 'none',
transform: isEnabled ? 'translateY(-2px)' : 'none'
}
}}
>
<CardContent sx={{ p: 3 }}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<Box sx={{ color: status === 'connected' ? '#10b981' : '#64748b' }}>
{icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b' }}>
{name}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b' }}>
{description}
</Typography>
</Box>
<Chip
icon={getStatusIcon(status)}
label={getStatusText(status)}
color={getStatusColor(status) as any}
size="small"
/>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1} sx={{ mb: 2 }}>
{features.map((feature, index) => (
<Chip
key={index}
label={feature}
size="small"
icon={<CheckIcon sx={{ fontSize: 14, color: '#10b981' }} />}
sx={{
backgroundColor: '#f0fdf4',
color: '#0c4a6e',
border: '1px solid #10b981',
fontSize: '0.75rem',
height: 24,
'&:hover': {
backgroundColor: '#dcfce7',
}
}}
/>
))}
</Stack>
<Button
variant={status === 'connected' ? 'outlined' : 'contained'}
size="medium"
fullWidth
disabled={!isEnabled || isLoading}
onClick={() => isEnabled && onConnect(id)}
startIcon={isLoading ? <CircularProgress size={16} /> : getStatusIcon(status)}
sx={{
textTransform: 'none',
fontWeight: 600,
...(status === 'connected' && {
borderColor: '#10b981',
color: '#10b981',
'&:hover': {
backgroundColor: '#10b981',
color: 'white'
}
})
}}
>
{status === 'connected' ? 'Connected' : status === 'coming_soon' ? 'Coming Soon' : 'Connect'}
</Button>
</CardContent>
</Card>
);
};
export default PlatformCard;

View File

@@ -0,0 +1,209 @@
import React from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
Stack,
Chip,
Button
} from '@mui/material';
import {
CheckCircle as CheckIcon,
Error as ErrorIcon,
Info as InfoIcon,
Launch as LaunchIcon,
Schedule as ScheduleIcon
} from '@mui/icons-material';
import PlatformCard from './PlatformCard';
import GSCPlatformCard from './GSCPlatformCard';
import WordPressOAuthPlatformCard from './WordPressOAuthPlatformCard';
import WixPlatformCard from './WixPlatformCard';
import { type GSCSite } from '../../../api/gsc';
interface Platform {
id: string;
name: string;
description: string;
icon: React.ReactNode;
category: 'website' | 'social' | 'analytics';
status: 'available' | 'connected' | 'coming_soon' | 'disabled';
features: string[];
benefits: string[];
oauthUrl?: string;
isEnabled: boolean;
}
interface PlatformSectionProps {
title: string;
description: string;
platforms: Platform[];
connectedPlatforms: string[];
gscSites: GSCSite[] | null;
isLoading: boolean;
onConnect: (platformId: string) => void;
onDisconnect?: (platformId: string) => void;
setConnectedPlatforms?: (platforms: string[]) => void;
fadeTimeout?: number;
}
const PlatformSection: React.FC<PlatformSectionProps> = ({
title,
description,
platforms,
connectedPlatforms,
gscSites,
isLoading,
onConnect,
onDisconnect,
setConnectedPlatforms,
fadeTimeout = 800
}) => {
const getStatusColor = (status: string) => {
switch (status) {
case 'connected': return 'success';
case 'available': return 'primary';
case 'coming_soon': return 'warning';
case 'disabled': return 'default';
default: return 'default';
}
};
const getStatusIcon = (status: string): React.ReactElement => {
switch (status) {
case 'connected': return <CheckIcon />;
case 'available': return <LaunchIcon />;
case 'coming_soon': return <ScheduleIcon />;
case 'disabled': return <ErrorIcon />;
default: return <InfoIcon />;
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'connected': return 'Connected';
case 'available': return 'Connect';
case 'coming_soon': return 'Coming Soon';
case 'disabled': return 'Disabled';
default: return 'Unknown';
}
};
const platformsWithStatus = platforms.map(platform => ({
...platform,
status: connectedPlatforms.includes(platform.id) ? 'connected' : platform.status
}));
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', mb: 2 }}>
{title}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', mb: 3 }}>
{description}
</Typography>
<Grid container spacing={2}>
{platformsWithStatus.map((platform) => (
<Grid item xs={12} md={platform.category === 'social' ? 4 : 6} key={platform.id}>
{platform.id === 'gsc' ? (
<GSCPlatformCard
platform={platform}
gscSites={gscSites}
isLoading={isLoading}
onConnect={onConnect}
getStatusIcon={getStatusIcon}
getStatusText={getStatusText}
getStatusColor={getStatusColor}
onRefresh={() => {
// Trigger a refresh of GSC status
console.log('Refreshing GSC status...');
}}
/>
) : platform.id === 'wordpress' ? (
<WordPressOAuthPlatformCard
onConnect={onConnect}
onDisconnect={onDisconnect}
connectedPlatforms={connectedPlatforms}
setConnectedPlatforms={setConnectedPlatforms || (() => {})}
/>
) : platform.id === 'wix' ? (
<WixPlatformCard
onConnect={onConnect}
onDisconnect={onDisconnect}
connectedPlatforms={connectedPlatforms}
setConnectedPlatforms={setConnectedPlatforms || (() => {})}
/>
) : platform.category === 'social' ? (
<Card
sx={{
height: '100%',
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
transition: 'all 0.2s ease',
opacity: platform.isEnabled ? 1 : 0.6,
'&:hover': {
boxShadow: platform.isEnabled ? '0 4px 12px rgba(0, 0, 0, 0.1)' : 'none',
transform: platform.isEnabled ? 'translateY(-2px)' : 'none'
}
}}
>
<CardContent sx={{ p: 2.5 }}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<Box sx={{ color: '#64748b' }}>
{platform.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
{platform.name}
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', fontSize: '0.875rem' }}>
{platform.description}
</Typography>
</Box>
<Chip
icon={getStatusIcon(platform.status)}
label={getStatusText(platform.status)}
color={getStatusColor(platform.status) as any}
size="small"
/>
</Stack>
<Button
variant="outlined"
size="small"
fullWidth
disabled={!platform.isEnabled}
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: '#e2e8f0',
color: '#64748b'
}}
>
Coming Soon
</Button>
</CardContent>
</Card>
) : (
<PlatformCard
id={platform.id}
name={platform.name}
description={platform.description}
icon={platform.icon}
status={platform.status}
features={platform.features}
isEnabled={platform.isEnabled}
isLoading={isLoading}
onConnect={onConnect}
/>
)}
</Grid>
))}
</Grid>
</Box>
);
};
export default PlatformSection;

View File

@@ -0,0 +1,235 @@
/**
* Wix Platform Card Component
* Handles Wix connection using the same pattern as GSC/WordPress
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
Chip,
CircularProgress,
IconButton,
Tooltip
} from '@mui/material';
import {
Web as WixIcon,
Add as AddIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
import { useWixConnection } from '../../../hooks/useWixConnection';
import { usePlatformConnections } from './usePlatformConnections';
interface WixPlatformCardProps {
onConnect?: (platform: string) => void;
onDisconnect?: (platform: string) => void;
connectedPlatforms: string[];
setConnectedPlatforms: (platforms: string[]) => void;
}
const WixPlatformCard: React.FC<WixPlatformCardProps> = ({
onConnect,
onDisconnect,
connectedPlatforms,
setConnectedPlatforms
}) => {
const { connected, sites, totalSites, isLoading, checkStatus } = useWixConnection();
const { handleConnect } = usePlatformConnections();
const [isConnecting, setIsConnecting] = useState(false);
// Update connected platforms when Wix connection changes
useEffect(() => {
if (connected && totalSites > 0) {
if (!connectedPlatforms.includes('wix')) {
setConnectedPlatforms([...connectedPlatforms, 'wix']);
}
} else {
if (connectedPlatforms.includes('wix')) {
setConnectedPlatforms(connectedPlatforms.filter(p => p !== 'wix'));
}
}
}, [connected, totalSites, connectedPlatforms, setConnectedPlatforms]);
const handleWixConnect = async () => {
try {
setIsConnecting(true);
await handleConnect('wix');
} catch (error) {
console.error('Error connecting to Wix:', error);
} finally {
setIsConnecting(false);
}
};
const getStatusIcon = () => {
if (isLoading || isConnecting) return <CircularProgress size={20} />;
if (connected && totalSites > 0) return <CheckCircleIcon color="success" />;
return <ErrorIcon color="error" />;
};
const getStatusColor = () => {
if (connected && totalSites > 0) return 'success';
return 'default';
};
const getStatusText = () => {
if (isLoading || isConnecting) return 'Connecting...';
if (connected && totalSites > 0) return `Connected (${totalSites} site${totalSites > 1 ? 's' : ''})`;
return 'Not Connected';
};
return (
<Card
sx={{
height: '100%',
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
transform: 'translateY(-2px)'
}
}}
>
<CardContent sx={{ p: 2.5 }}>
{/* Header */}
<Box display="flex" alignItems="center" mb={2}>
<Box sx={{ color: '#ff6b6b', mr: 1 }}>
<WixIcon />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
Wix
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', fontSize: '0.875rem' }}>
Connect your Wix website for automated content publishing and analytics
</Typography>
</Box>
<Chip
icon={getStatusIcon()}
label={getStatusText()}
color={getStatusColor() as any}
size="small"
/>
</Box>
{/* Connected Sites Display */}
{connected && totalSites > 0 && (
<Box mb={2}>
<Typography variant="body2" sx={{ fontWeight: 500, color: '#1e293b', mb: 1 }}>
Connected Sites:
</Typography>
<Box
sx={{
p: 1.5,
border: '1px solid #e2e8f0',
borderRadius: 1,
backgroundColor: '#f8fafc',
fontSize: '0.875rem',
color: '#475569',
fontFamily: 'monospace'
}}
>
{sites.length > 0 ? sites[0].blog_url : 'Connected Wix Site'}
</Box>
</Box>
)}
{/* Features as Chips */}
<Box mb={2} sx={{ minHeight: '32px' }}>
<Box display="flex" flexWrap="wrap" gap={0.5}>
<Chip
label="Auto-publish content"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Analytics tracking"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="SEO optimization"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Box>
</Box>
{/* Actions */}
<Box display="flex" gap={1}>
{connected && totalSites > 0 ? (
<>
<Button
variant="outlined"
size="small"
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: '#e2e8f0',
color: '#64748b',
flex: 1
}}
disabled
>
Connected ({totalSites})
</Button>
<Tooltip title="Refresh status">
<IconButton
onClick={checkStatus}
disabled={isLoading}
size="small"
sx={{ color: '#64748b' }}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</>
) : (
<Button
variant="contained"
size="small"
onClick={handleWixConnect}
disabled={isLoading || isConnecting}
sx={{
textTransform: 'none',
fontWeight: 600,
flex: 1
}}
>
{isConnecting ? 'Connecting...' : 'Connect Wix'}
</Button>
)}
</Box>
</CardContent>
</Card>
);
};
export default WixPlatformCard;

View File

@@ -0,0 +1,241 @@
import React from 'react';
import {
Box,
Typography,
LinearProgress,
Stepper,
Step,
StepLabel,
IconButton,
Tooltip,
Fade
} from '@mui/material';
import {
HelpOutline,
Close
} from '@mui/icons-material';
import UserBadge from '../../shared/UserBadge';
interface WizardHeaderProps {
activeStep: number;
progress: number;
stepHeaderContent: {
title: string;
description: string;
};
showProgressMessage: boolean;
progressMessage: string;
showHelp: boolean;
isMobile: boolean;
steps: Array<{
label: string;
description: string;
icon: string;
}>;
onStepClick: (stepIndex: number) => void;
onHelpToggle: () => void;
}
export const WizardHeader: React.FC<WizardHeaderProps> = ({
activeStep,
progress,
stepHeaderContent,
showProgressMessage,
progressMessage,
showHelp,
isMobile,
steps,
onStepClick,
onHelpToggle
}) => {
return (
<Box
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
p: { xs: 3, md: 4 },
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
{/* Progress Message */}
{showProgressMessage && (
<Fade in={showProgressMessage}>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
background: 'rgba(16, 185, 129, 0.9)',
color: 'white',
p: 2,
textAlign: 'center',
zIndex: 10,
backdropFilter: 'blur(10px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{progressMessage}
</Typography>
</Box>
</Fade>
)}
{/* Top Row - Title and Actions */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ flex: 1 }}>
<UserBadge colorMode="dark" />
</Box>
<Box sx={{ flex: 2, textAlign: 'center' }}>
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
{stepHeaderContent.title}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, flex: 1, justifyContent: 'flex-end' }}>
<Tooltip title="Get Help" arrow>
<IconButton
onClick={onHelpToggle}
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<HelpOutline />
</IconButton>
</Tooltip>
<Tooltip title="Skip for now" arrow>
<IconButton
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<Close />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Progress Bar */}
<Box sx={{ mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 500 }}>
Setup Progress
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 600 }}>
{Math.round(progress)}% Complete
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.2)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}
}}
/>
</Box>
{/* Stepper in Header */}
<Box sx={{ position: 'relative', zIndex: 1 }}>
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
sx={{
'& .MuiStepLabel-root': {
cursor: 'pointer',
},
'& .MuiStepLabel-label': {
fontSize: '0.875rem',
fontWeight: 600,
color: 'white',
},
'& .MuiStepLabel-labelContainer': {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
'& .MuiStepLabel-label.Mui-completed': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiStepLabel-label.Mui-active': {
color: 'white',
},
'& .MuiStepLabel-label.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
}}
>
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel
onClick={() => onStepClick(index)}
sx={{
cursor: index <= activeStep ? 'pointer' : 'default',
'& .MuiStepLabel-iconContainer': {
background: index <= activeStep
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50%',
width: 40,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: index <= activeStep ? 'white' : 'rgba(255, 255, 255, 0.6)',
fontSize: '1.2rem',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: index <= activeStep
? '0 4px 12px rgba(255, 255, 255, 0.2)'
: 'none',
'&:hover': {
transform: index <= activeStep ? 'scale(1.05)' : 'none',
boxShadow: index <= activeStep
? '0 6px 16px rgba(255, 255, 255, 0.3)'
: 'none',
}
},
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="h6" sx={{ mb: 0.5 }}>
{step.icon}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'center' }}>
{step.label}
</Typography>
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,61 @@
import React from 'react';
import {
Box,
Typography,
Paper,
LinearProgress,
Fade
} from '@mui/material';
interface WizardLoadingStateProps {
loading: boolean;
}
export const WizardLoadingState: React.FC<WizardLoadingStateProps> = ({ loading }) => {
if (!loading) {
return null;
}
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<Fade in={true}>
<Paper
elevation={24}
sx={{
p: 4,
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
maxWidth: 400,
width: '100%',
}}
>
<Typography variant="h5" align="center" gutterBottom sx={{ fontWeight: 600 }}>
Setting up your workspace...
</Typography>
<LinearProgress
sx={{
mt: 3,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
}}
/>
</Paper>
</Fade>
</Box>
);
};

View File

@@ -0,0 +1,111 @@
import React from 'react';
import {
Box,
Button,
Typography,
Tooltip
} from '@mui/material';
import {
ArrowBack,
ArrowForward,
CheckCircle
} from '@mui/icons-material';
interface WizardNavigationProps {
activeStep: number;
totalSteps: number;
onBack: () => void;
onNext: () => void;
isLastStep: boolean;
isCurrentStepValid?: boolean;
}
export const WizardNavigation: React.FC<WizardNavigationProps> = ({
activeStep,
totalSteps,
onBack,
onNext,
isLastStep,
isCurrentStepValid = true
}) => {
return (
<Box
sx={{
p: { xs: 2, md: 3 },
pt: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderTop: '1px solid rgba(0,0,0,0.08)',
background: 'rgba(0,0,0,0.02)',
}}
>
<Button
variant="outlined"
onClick={onBack}
disabled={activeStep === 0}
startIcon={<ArrowBack />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
}}
>
Back
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.7, fontWeight: 500 }}>
Step {activeStep + 1} of {totalSteps}
</Typography>
{isLastStep && (
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
)}
</Box>
<Tooltip
title={!isCurrentStepValid ? "Complete the current step requirements to continue" : ""}
placement="top"
>
<span>
<Button
variant="contained"
onClick={onNext}
disabled={isLastStep || !isCurrentStepValid}
endIcon={isLastStep ? <CheckCircle /> : <ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
{isLastStep ? 'Complete Setup' : 'Continue'}
</Button>
</span>
</Tooltip>
</Box>
);
};

View File

@@ -0,0 +1,332 @@
/**
* WordPress OAuth Platform Card Component
* Simplified WordPress connection using OAuth2 flow.
*/
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Alert,
CircularProgress,
IconButton,
Tooltip,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Divider
} from '@mui/material';
import {
Web as WordPressIcon,
Add as AddIcon,
Delete as DeleteIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
Launch as LaunchIcon
} from '@mui/icons-material';
import { useWordPressOAuth } from '../../../hooks/useWordPressOAuth';
interface WordPressOAuthPlatformCardProps {
onConnect?: (platform: string) => void;
onDisconnect?: (platform: string) => void;
connectedPlatforms: string[];
setConnectedPlatforms: (platforms: string[]) => void;
}
const WordPressOAuthPlatformCard: React.FC<WordPressOAuthPlatformCardProps> = ({
onConnect,
onDisconnect,
connectedPlatforms,
setConnectedPlatforms
}) => {
const {
connected,
sites,
totalSites,
isLoading,
startOAuthFlow,
disconnectSite,
refreshStatus
} = useWordPressOAuth();
const [showSitesDialog, setShowSitesDialog] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const isConnected = connected && totalSites > 0;
const handleConnect = async () => {
try {
setIsConnecting(true);
await startOAuthFlow();
// OAuth flow will handle the connection
} catch (error: any) {
console.error('Error connecting to WordPress:', error);
// Show user-friendly error message for configuration issues
if (error.response?.status === 500 && error.response?.data?.detail?.includes('not configured')) {
alert('WordPress OAuth is not properly configured. Please contact support or check that WordPress.com application credentials are set up correctly.');
} else {
alert('Failed to connect to WordPress. Please try again or contact support if the problem persists.');
}
} finally {
setIsConnecting(false);
}
};
const handleDisconnectSite = async (tokenId: number) => {
try {
const success = await disconnectSite(tokenId);
if (success) {
// Check if we still have connected sites
const remainingSites = sites.filter(site => site.id !== tokenId);
if (remainingSites.length === 0) {
setConnectedPlatforms(connectedPlatforms.filter(p => p !== 'wordpress'));
onDisconnect?.('wordpress');
}
setShowSitesDialog(false);
}
} catch (error) {
console.error('Error disconnecting WordPress site:', error);
}
};
const getStatusIcon = () => {
if (isLoading || isConnecting) return <CircularProgress size={20} />;
if (isConnected) return <CheckCircleIcon color="success" />;
return <ErrorIcon color="error" />;
};
const getStatusColor = () => {
if (isConnected) return 'success';
return 'default';
};
const getStatusText = () => {
if (isLoading || isConnecting) return 'Connecting...';
if (isConnected) return `Connected (${totalSites} site${totalSites > 1 ? 's' : ''})`;
return 'Not Connected';
};
return (
<>
<Card
sx={{
height: '100%',
border: '1px solid #e2e8f0',
backgroundColor: '#ffffff',
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
transform: 'translateY(-2px)'
}
}}
>
<CardContent sx={{ p: 2.5 }}>
{/* Header */}
<Box display="flex" alignItems="center" mb={2}>
<Box sx={{ color: '#21759b', mr: 1 }}>
<WordPressIcon />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: '#1e293b' }}>
WordPress
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', fontSize: '0.875rem' }}>
Connect your WordPress.com sites with secure OAuth authentication
</Typography>
</Box>
<Chip
icon={getStatusIcon()}
label={getStatusText()}
color={getStatusColor() as any}
size="small"
/>
</Box>
{/* Connected Sites Display */}
{isConnected && totalSites > 0 && (
<Box mb={2}>
<Typography variant="body2" sx={{ fontWeight: 500, color: '#1e293b', mb: 1 }}>
Connected Sites:
</Typography>
<Box
sx={{
p: 1.5,
border: '1px solid #e2e8f0',
borderRadius: 1,
backgroundColor: '#f8fafc',
fontSize: '0.875rem',
color: '#475569',
fontFamily: 'monospace'
}}
>
{sites.length > 0 ? sites[0].blog_url : 'Connected WordPress Site'}
</Box>
</Box>
)}
{/* Features as Chips */}
<Box mb={2} sx={{ minHeight: '32px' }}>
<Box display="flex" flexWrap="wrap" gap={0.5}>
<Chip
label="OAuth connection"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Direct publishing"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Media integration"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
<Chip
label="Category & tags"
size="small"
variant="outlined"
sx={{
color: '#475569',
borderColor: '#e2e8f0',
'&:hover': {
backgroundColor: '#f8fafc'
}
}}
/>
</Box>
</Box>
{/* Actions */}
<Box display="flex" gap={1}>
{isConnected ? (
<>
<Button
variant="outlined"
size="small"
onClick={() => setShowSitesDialog(true)}
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: '#e2e8f0',
color: '#64748b',
flex: 1
}}
>
Manage Sites ({totalSites})
</Button>
<Tooltip title="Refresh status">
<IconButton
onClick={refreshStatus}
disabled={isLoading}
size="small"
sx={{ color: '#64748b' }}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</>
) : (
<Button
variant="contained"
size="small"
onClick={handleConnect}
disabled={isLoading || isConnecting}
sx={{
textTransform: 'none',
fontWeight: 600,
flex: 1
}}
>
{isConnecting ? 'Connecting...' : 'Connect WordPress'}
</Button>
)}
</Box>
</CardContent>
</Card>
{/* Manage Sites Dialog */}
<Dialog open={showSitesDialog} onClose={() => setShowSitesDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>Manage WordPress Sites</DialogTitle>
<DialogContent>
<List>
{sites.map((site, index) => (
<React.Fragment key={site.id}>
<ListItem>
<ListItemText
primary={site.blog_url}
secondary={
<Box>
<Typography variant="body2" component="span" color="text.secondary" display="block">
Blog ID: {site.blog_id}
</Typography>
<Typography variant="caption" component="span" color="text.secondary" display="block">
Connected: {new Date(site.created_at).toLocaleDateString()}
</Typography>
<Typography variant="caption" component="span" color="text.secondary" display="block">
Scope: {site.scope}
</Typography>
</Box>
}
/>
<ListItemSecondaryAction>
<Tooltip title="Disconnect site">
<IconButton
edge="end"
onClick={() => handleDisconnectSite(site.id)}
color="error"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
{index < sites.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowSitesDialog(false)}>Close</Button>
<Button onClick={() => { setShowSitesDialog(false); handleConnect(); }} variant="contained">
Add New Site
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default WordPressOAuthPlatformCard;

View File

@@ -0,0 +1,397 @@
/**
* WordPress Platform Card Component
* Handles WordPress site connection and management.
*/
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Alert,
CircularProgress,
IconButton,
Tooltip,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Divider
} from '@mui/material';
import {
Web as WordPressIcon,
Add as AddIcon,
Delete as DeleteIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
Settings as SettingsIcon
} from '@mui/icons-material';
import { useWordPressConnection } from '../../../hooks/useWordPressConnection';
interface WordPressPlatformCardProps {
onConnect?: (platform: string) => void;
onDisconnect?: (platform: string) => void;
connectedPlatforms: string[];
setConnectedPlatforms: (platforms: string[]) => void;
}
const WordPressPlatformCard: React.FC<WordPressPlatformCardProps> = ({
onConnect,
onDisconnect,
connectedPlatforms,
setConnectedPlatforms
}) => {
const {
connected,
sites,
totalSites,
isLoading,
addSite,
disconnectSite,
testConnection,
validateSiteUrl,
formatSiteUrl,
refreshStatus
} = useWordPressConnection();
const [showAddDialog, setShowAddDialog] = useState(false);
const [showSitesDialog, setShowSitesDialog] = useState(false);
const [formData, setFormData] = useState({
site_url: '',
site_name: '',
username: '',
app_password: ''
});
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const isConnected = connected && totalSites > 0;
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (formErrors[field]) {
setFormErrors(prev => ({ ...prev, [field]: '' }));
}
};
const validateForm = (): boolean => {
const errors: Record<string, string> = {};
if (!formData.site_url.trim()) {
errors.site_url = 'Site URL is required';
} else if (!validateSiteUrl(formData.site_url)) {
errors.site_url = 'Please enter a valid site URL';
}
if (!formData.site_name.trim()) {
errors.site_name = 'Site name is required';
}
if (!formData.username.trim()) {
errors.username = 'Username is required';
}
if (!formData.app_password.trim()) {
errors.app_password = 'Application password is required';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const handleTestConnection = async () => {
if (!validateForm()) return;
try {
setIsSubmitting(true);
setTestResult(null);
const success = await testConnection(formData);
setTestResult({
success,
message: success ? 'Connection successful!' : 'Connection failed. Please check your credentials.'
});
} catch (error) {
setTestResult({
success: false,
message: 'Connection test failed. Please try again.'
});
} finally {
setIsSubmitting(false);
}
};
const handleAddSite = async () => {
if (!validateForm()) return;
try {
setIsSubmitting(true);
setTestResult(null);
const success = await addSite(formData);
if (success) {
setShowAddDialog(false);
setFormData({ site_url: '', site_name: '', username: '', app_password: '' });
setConnectedPlatforms([...connectedPlatforms, 'wordpress']);
onConnect?.('wordpress');
} else {
setTestResult({
success: false,
message: 'Failed to add WordPress site. Please try again.'
});
}
} catch (error) {
setTestResult({
success: false,
message: 'Failed to add WordPress site. Please try again.'
});
} finally {
setIsSubmitting(false);
}
};
const handleDisconnectSite = async (siteId: number) => {
try {
const success = await disconnectSite(siteId);
if (success) {
// Check if we still have connected sites
const remainingSites = sites.filter(site => site.id !== siteId);
if (remainingSites.length === 0) {
setConnectedPlatforms(connectedPlatforms.filter(p => p !== 'wordpress'));
onDisconnect?.('wordpress');
}
setShowSitesDialog(false);
}
} catch (error) {
console.error('Error disconnecting WordPress site:', error);
}
};
const getStatusIcon = () => {
if (isLoading) return <CircularProgress size={20} />;
if (isConnected) return <CheckCircleIcon color="success" />;
return <ErrorIcon color="error" />;
};
const getStatusColor = () => {
if (isConnected) return 'success';
return 'default';
};
const getStatusText = () => {
if (isLoading) return 'Checking...';
if (isConnected) return `Connected (${totalSites} site${totalSites > 1 ? 's' : ''})`;
return 'Not Connected';
};
return (
<>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<Box display="flex" alignItems="center" mb={2}>
<WordPressIcon sx={{ mr: 1, color: '#21759b' }} />
<Typography variant="h6" component="h3">
WordPress
</Typography>
<Box ml="auto">
<Chip
icon={getStatusIcon()}
label={getStatusText()}
color={getStatusColor() as any}
size="small"
/>
</Box>
</Box>
{/* Description */}
<Typography variant="body2" color="text.secondary" mb={3}>
Connect your WordPress sites for seamless content publishing and management.
</Typography>
{/* Features */}
<Box mb={3}>
<Typography variant="body2" fontWeight="medium" mb={1}>
Features:
</Typography>
<Typography variant="body2" color="text.secondary">
Direct publishing to WordPress<br />
Media library integration<br />
Category and tag management<br />
SEO optimization
</Typography>
</Box>
{/* Actions */}
<Box mt="auto">
{isConnected ? (
<Box display="flex" gap={1}>
<Button
variant="outlined"
startIcon={<SettingsIcon />}
onClick={() => setShowSitesDialog(true)}
fullWidth
>
Manage Sites ({totalSites})
</Button>
<Tooltip title="Refresh status">
<IconButton onClick={refreshStatus} disabled={isLoading}>
<RefreshIcon />
</IconButton>
</Tooltip>
</Box>
) : (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setShowAddDialog(true)}
fullWidth
disabled={isLoading}
>
Connect WordPress
</Button>
)}
</Box>
</CardContent>
</Card>
{/* Add Site Dialog */}
<Dialog open={showAddDialog} onClose={() => setShowAddDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>Connect WordPress Site</DialogTitle>
<DialogContent>
<Box pt={1}>
<TextField
fullWidth
label="Site URL"
value={formData.site_url}
onChange={(e) => handleInputChange('site_url', e.target.value)}
error={!!formErrors.site_url}
helperText={formErrors.site_url || 'e.g., mysite.com or https://mysite.com'}
margin="normal"
/>
<TextField
fullWidth
label="Site Name"
value={formData.site_name}
onChange={(e) => handleInputChange('site_name', e.target.value)}
error={!!formErrors.site_name}
helperText={formErrors.site_name || 'A friendly name for this site'}
margin="normal"
/>
<TextField
fullWidth
label="Username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
error={!!formErrors.username}
helperText={formErrors.username || 'Your WordPress username'}
margin="normal"
/>
<TextField
fullWidth
label="Application Password"
type="password"
value={formData.app_password}
onChange={(e) => handleInputChange('app_password', e.target.value)}
error={!!formErrors.app_password}
helperText={formErrors.app_password || 'Generate from WordPress Admin → Users → Profile → Application Passwords'}
margin="normal"
/>
{testResult && (
<Alert severity={testResult.success ? 'success' : 'error'} sx={{ mt: 2 }}>
{testResult.message}
</Alert>
)}
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
<strong>How to get Application Password:</strong><br />
1. Go to your WordPress Admin Users Profile<br />
2. Scroll down to "Application Passwords"<br />
3. Enter a name (e.g., "ALwrity") and click "Add New Application Password"<br />
4. Copy the generated password (it won't be shown again)
</Typography>
</Alert>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowAddDialog(false)}>Cancel</Button>
<Button onClick={handleTestConnection} disabled={isSubmitting}>
Test Connection
</Button>
<Button
onClick={handleAddSite}
variant="contained"
disabled={isSubmitting || !testResult?.success}
>
{isSubmitting ? <CircularProgress size={20} /> : 'Connect Site'}
</Button>
</DialogActions>
</Dialog>
{/* Manage Sites Dialog */}
<Dialog open={showSitesDialog} onClose={() => setShowSitesDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>Manage WordPress Sites</DialogTitle>
<DialogContent>
<List>
{sites.map((site, index) => (
<React.Fragment key={site.id}>
<ListItem>
<ListItemText
primary={site.site_name}
secondary={
<Box>
<Typography variant="body2" color="text.secondary">
{site.site_url}
</Typography>
<Typography variant="caption" color="text.secondary">
Connected: {new Date(site.created_at).toLocaleDateString()}
</Typography>
</Box>
}
/>
<ListItemSecondaryAction>
<Tooltip title="Disconnect site">
<IconButton
edge="end"
onClick={() => handleDisconnectSite(site.id)}
color="error"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
{index < sites.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowSitesDialog(false)}>Close</Button>
<Button onClick={() => { setShowSitesDialog(false); setShowAddDialog(true); }} variant="contained">
Add New Site
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default WordPressPlatformCard;

View File

@@ -0,0 +1,141 @@
import { useState, useEffect } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { gscAPI, type GSCSite } from '../../../api/gsc';
export const useGSCConnection = () => {
const { getToken } = useAuth();
const [gscSites, setGscSites] = useState<GSCSite[] | null>(null);
const [connectedPlatforms, setConnectedPlatforms] = useState<string[]>([]);
useEffect(() => {
// Ensure GSC API uses authenticated client
try {
gscAPI.setAuthTokenGetter(async () => {
try {
return await getToken();
} catch {
return null;
}
});
} catch {}
}, [getToken]);
useEffect(() => {
// Check current GSC connection status on load
(async () => {
try {
const status = await gscAPI.getStatus();
if (status.connected) {
setConnectedPlatforms(prev => Array.from(new Set([...prev, 'gsc'])));
if (status.sites && status.sites.length) setGscSites(status.sites);
} else {
setConnectedPlatforms(prev => prev.filter(p => p !== 'gsc'));
setGscSites(null);
}
} catch (error) {
console.log('GSC status check failed');
try {
await gscAPI.clearIncomplete();
} catch {}
setConnectedPlatforms(prev => prev.filter(p => p !== 'gsc'));
setGscSites(null);
}
})();
}, []);
const handleGSCConnect = async () => {
try {
// Clear any incomplete credentials and connection state before starting OAuth
try {
await gscAPI.clearIncomplete();
} catch (e) {
console.log('Clear incomplete failed:', e);
}
// Also try to disconnect completely
try {
await gscAPI.disconnect();
} catch (e) {
console.log('Disconnect failed:', e);
}
// Clear local connection state
setConnectedPlatforms(prev => prev.filter(p => p !== 'gsc'));
setGscSites(null);
const { auth_url } = await gscAPI.getAuthUrl();
const popup = window.open(
auth_url,
'gsc-auth',
'width=600,height=700,scrollbars=yes,resizable=yes'
);
if (!popup) {
// Fallback: navigate directly to OAuth URL if popup is blocked
console.log('Popup blocked, navigating directly to OAuth URL');
window.location.href = auth_url;
return;
}
// Check if popup was redirected immediately (OAuth consent screen issue)
setTimeout(() => {
try {
if (popup.closed) {
console.log('GSC popup closed immediately - possible OAuth consent screen issue');
}
} catch (e) {
// Ignore cross-origin errors
}
}, 2000);
// Prefer message-based completion from callback window to avoid COOP issues
let messageHandled = false;
const messageHandler = (event: MessageEvent) => {
if (messageHandled) return; // Prevent duplicate handling
if (!event?.data || typeof event.data !== 'object') return;
const { type } = event.data as { type?: string };
if (type === 'GSC_AUTH_SUCCESS' || type === 'GSC_AUTH_ERROR') {
messageHandled = true;
try { popup.close(); } catch {}
window.removeEventListener('message', messageHandler);
if (type === 'GSC_AUTH_SUCCESS') {
// Optimistically mark as connected; a later status refresh will confirm
setConnectedPlatforms(prev => Array.from(new Set([...prev, 'gsc'])));
// Refresh sites
(async () => {
try {
const status = await gscAPI.getStatus();
if (status.connected && status.sites) setGscSites(status.sites);
} catch {}
})();
}
setTimeout(() => {
window.location.href = '/onboarding?step=5';
}, 250);
}
};
window.addEventListener('message', messageHandler);
// Fallback: safety timeout in case message doesn't arrive
setTimeout(() => {
try { if (!popup.closed) popup.close(); } catch {}
window.removeEventListener('message', messageHandler);
}, 3 * 60 * 1000);
} catch (error) {
console.error('GSC OAuth error:', error);
throw error;
}
};
return {
gscSites,
connectedPlatforms,
setConnectedPlatforms,
setGscSites,
handleGSCConnect
};
};

View File

@@ -0,0 +1,104 @@
import { useState, useEffect } from 'react';
import { createClient, OAuthStrategy } from '@wix/sdk';
export const usePlatformConnections = () => {
const [connectedPlatforms, setConnectedPlatforms] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
// Handle Wix OAuth popup messages
useEffect(() => {
const handler = (event: MessageEvent) => {
const trusted = [window.location.origin, 'https://littery-sonny-unscrutinisingly.ngrok-free.dev'];
if (!trusted.includes(event.origin)) return;
if (!event.data || typeof event.data !== 'object') return;
if (event.data.type === 'WIX_OAUTH_SUCCESS') {
console.log('Wix OAuth success message received');
setConnectedPlatforms(prev => {
const updated = [...prev.filter(id => id !== 'wix'), 'wix'];
console.log('Updated connected platforms via message:', updated);
return updated;
});
setToastMessage('Wix account connected successfully!');
setShowToast(true);
}
if (event.data.type === 'WIX_OAUTH_ERROR') {
setToastMessage('Wix connection failed. Please try again.');
setShowToast(true);
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, [setConnectedPlatforms, setToastMessage]);
// Fallback: detect wix_connected query param after full-page redirect
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('wix_connected') === 'true') {
console.log('Wix connected via URL param, updating state');
setConnectedPlatforms(prev => {
const updated = [...prev.filter(id => id !== 'wix'), 'wix'];
console.log('Updated connected platforms:', updated);
return updated;
});
setToastMessage('Wix account connected successfully!');
setShowToast(true);
// Clean URL
const clean = window.location.pathname + window.location.hash;
window.history.replaceState({}, document.title, clean || '/');
}
}, [setConnectedPlatforms, setToastMessage]);
const handleWixConnect = async () => {
try {
// Use the working Wix OAuth flow from WixTestPage
const wixClient = createClient({
auth: OAuthStrategy({ clientId: '75d88e36-1c76-4009-b769-15f4654556df' })
});
const NGROK_ORIGIN = 'https://littery-sonny-unscrutinisingly.ngrok-free.dev';
const redirectOrigin = window.location.origin.includes('localhost') ? NGROK_ORIGIN : window.location.origin;
const redirectUri = `${redirectOrigin}/wix/callback`;
const oauthData = await wixClient.auth.generateOAuthData(redirectUri);
// Use sessionStorage to ensure data is scoped to this tab/session (like WixTestPage)
sessionStorage.setItem('wix_oauth_data', JSON.stringify(oauthData));
const { authUrl } = await wixClient.auth.getAuthUrl(oauthData);
window.location.href = authUrl;
} catch (error) {
console.error('Wix connection error:', error);
throw error;
}
};
const handleConnect = async (platformId: string) => {
setIsLoading(true);
try {
if (platformId === 'wix') {
await handleWixConnect();
return;
}
// For other platforms, you can add their connection logic here
console.log(`Connecting to ${platformId}...`);
} catch (error) {
console.error('Connection error:', error);
} finally {
setIsLoading(false);
}
};
return {
connectedPlatforms,
setConnectedPlatforms,
isLoading,
showToast,
setShowToast,
toastMessage,
setToastMessage,
handleConnect
};
};

View File

@@ -1,6 +1,6 @@
/** Google Search Console OAuth Callback Handler Component. */
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import {
Box,
Typography,
@@ -13,16 +13,14 @@ import {
Error as ErrorIcon
} from '@mui/icons-material';
import { gscAPI } from '../../../api/gsc';
import { useAuth } from '@clerk/clerk-react';
const GSCAuthCallback: React.FC = () => {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [message, setMessage] = useState<string>('Processing authentication...');
const { getToken } = useAuth();
useEffect(() => {
handleOAuthCallback();
}, []);
const handleOAuthCallback = async () => {
const handleOAuthCallback = useCallback(async () => {
try {
console.log('GSC Auth Callback: Processing OAuth callback');
@@ -76,7 +74,19 @@ const GSCAuthCallback: React.FC = () => {
}, '*');
}
}
};
}, [message]);
useEffect(() => {
// Ensure API client has an auth token getter in the popup context
gscAPI.setAuthTokenGetter(async () => {
try {
return await getToken();
} catch (e) {
return null;
}
});
handleOAuthCallback();
}, [getToken, handleOAuthCallback]);
const getStatusIcon = () => {
switch (status) {
@@ -91,16 +101,6 @@ const GSCAuthCallback: React.FC = () => {
}
};
const getStatusColor = () => {
switch (status) {
case 'success':
return 'success';
case 'error':
return 'error';
default:
return 'info';
}
};
return (
<Box

View File

@@ -0,0 +1,67 @@
import React, { useRef, useEffect } from 'react';
import { useScramble } from '../hooks/useScramble';
interface ScrambleTextProps {
text: string;
duration?: number;
delay?: number;
restartInterval?: number;
className?: string;
style?: React.CSSProperties;
as?: React.ElementType;
}
export const ScrambleText: React.FC<ScrambleTextProps> = ({
text,
duration = 750,
delay = 0,
restartInterval = 7000,
className = '',
style = {},
as: Component = 'span',
}) => {
const { displayText, start, stop } = useScramble({ text, duration });
const ref = useRef<HTMLElement>(null);
const intervalRef = useRef<number | null>(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
// A flag to ensure our timeouts don't run after unmount
let isMounted = true;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => {
if (!isMounted) return;
start();
if (intervalRef.current) clearInterval(intervalRef.current);
intervalRef.current = window.setInterval(start, restartInterval);
}, delay);
} else {
stop();
if (intervalRef.current) clearInterval(intervalRef.current);
}
},
{ threshold: 0.2, rootMargin: '0px' }
);
observer.observe(element);
return () => {
isMounted = false;
observer.disconnect();
if (intervalRef.current) clearInterval(intervalRef.current);
stop();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, restartInterval, text]); // Re-run effect if text or config changes. start/stop are stable.
return (
<Component ref={ref} className={className} style={style}>
{displayText}
</Component>
);
};

View File

@@ -20,29 +20,35 @@ const WixCallbackPage: React.FC = () => {
return;
}
const oauthData = JSON.parse(saved);
// Optionally validate state matches
if (oauthData?.state && oauthData.state !== state) {
setError('State mismatch. Please restart the connection.');
return;
}
// Use the originally generated state to avoid SDK "Invalid _state" errors
const tokens = await wixClient.auth.getMemberTokens(code, state, oauthData);
wixClient.auth.setTokens(tokens);
// Persist tokens for subsequent API calls on this tab
try { sessionStorage.setItem('wix_tokens', JSON.stringify(tokens)); } catch {}
// Persist tokens for the test page to use
try {
sessionStorage.setItem('wix_tokens', JSON.stringify(tokens));
} catch {}
// optional: ping backend to mark connected
try { await fetch('/api/wix/test/connection/status'); } catch {}
// Cleanup saved oauth data
sessionStorage.removeItem('wix_oauth_data');
localStorage.removeItem('wix_oauth_data');
// Mark frontend session as connected for test UI
// Mark frontend session as connected for onboarding UI
sessionStorage.setItem('wix_connected', 'true');
window.location.replace('/wix-test');
// Notify opener (if opened as popup) and close; otherwise fallback to redirect
try {
const payload = { type: 'WIX_OAUTH_SUCCESS', success: true, tokens } as any;
(window.opener || window.parent)?.postMessage(payload, '*');
if (window.opener) {
window.close();
return;
}
} catch {}
// Fallback redirect for same-tab flow and let onboarding hook mark Wix as connected
window.location.replace('/onboarding?step=5&wix_connected=true');
} catch (e: any) {
setError(e?.message || 'OAuth callback failed');
try {
(window.opener || window.parent)?.postMessage({ type: 'WIX_OAUTH_ERROR', success: false, error: e?.message || 'OAuth callback failed' }, '*');
if (window.opener) window.close();
} catch {}
}
};
run();

View File

@@ -0,0 +1,65 @@
import React, { useEffect, useState } from 'react';
import { Box, CircularProgress, Typography, Alert } from '@mui/material';
const WordPressCallbackPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const run = async () => {
try {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (!code || !state) {
throw new Error('Missing OAuth parameters');
}
try {
// Call backend to complete token exchange
await fetch(`/wp/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`, {
method: 'GET',
credentials: 'include'
});
} catch (e) {
// Continue; backend HTML callback may already be handled in popup
}
// Notify opener and close if this is a popup window
try {
(window.opener || window.parent)?.postMessage({ type: 'WPCOM_OAUTH_SUCCESS', success: true }, '*');
if (window.opener) {
window.close();
return;
}
} catch {}
// Fallback: redirect back to onboarding
window.location.replace('/onboarding?step=5');
} catch (e: any) {
setError(e?.message || 'OAuth callback failed');
try {
(window.opener || window.parent)?.postMessage({ type: 'WPCOM_OAUTH_ERROR', success: false, error: e?.message || 'OAuth callback failed' }, '*');
if (window.opener) window.close();
} catch {}
}
};
run();
}, []);
return (
<Box sx={{ p: 4, maxWidth: 680, mx: 'auto' }}>
{!error ? (
<Box display="flex" alignItems="center" gap={2}>
<CircularProgress size={22} />
<Typography>Completing WordPress signin</Typography>
</Box>
) : (
<Alert severity="error">{error}</Alert>
)}
</Box>
);
};
export default WordPressCallbackPage;