ALwrity + Wordpress + Wix + GSC integration
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Persona Step Sections Index
|
||||
* Export all persona display sections
|
||||
*/
|
||||
|
||||
export { CorePersonaDisplay } from './CorePersonaDisplay';
|
||||
export { PlatformPersonaDisplay } from './PlatformPersonaDisplay';
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user