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