237 lines
8.8 KiB
TypeScript
237 lines
8.8 KiB
TypeScript
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
import { BlogResearchResponse, BlogOutlineSection } from '../services/blogWriterApi';
|
|
|
|
export interface Phase {
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
description: string;
|
|
completed: boolean;
|
|
current: boolean;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export const usePhaseNavigation = (
|
|
research: BlogResearchResponse | null,
|
|
outline: BlogOutlineSection[],
|
|
outlineConfirmed: boolean,
|
|
hasContent: boolean,
|
|
contentConfirmed: boolean,
|
|
seoAnalysis: any,
|
|
seoMetadata: any,
|
|
seoRecommendationsApplied?: boolean
|
|
) => {
|
|
// Initialize from localStorage if available
|
|
// If no research exists, default to empty string to show landing page
|
|
// Only default to 'research' if research already exists (resuming a session)
|
|
const getInitialPhase = (): string => {
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
const stored = window.localStorage.getItem('blogwriter_current_phase');
|
|
if (stored) {
|
|
// If stored phase is 'research' but no research exists, show landing page instead
|
|
if (stored === 'research' && !research) {
|
|
return ''; // Return empty to show landing page
|
|
}
|
|
// For other phases, use stored value (user might be in middle of outline/content/seo/publish)
|
|
// Even if research doesn't exist, allow other phases to be restored (edge case)
|
|
return stored;
|
|
}
|
|
}
|
|
} catch {}
|
|
// Default to empty string to show landing page when no research exists
|
|
// Will be set to 'research' when user clicks "Start Research"
|
|
return research ? 'research' : '';
|
|
};
|
|
|
|
const [currentPhase, setCurrentPhase] = useState<string>(getInitialPhase());
|
|
const [userSelectedPhase, setUserSelectedPhase] = useState<boolean>(() => {
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
const stored = window.localStorage.getItem('blogwriter_user_selected_phase');
|
|
return stored === 'true';
|
|
}
|
|
} catch {}
|
|
return false;
|
|
});
|
|
const lastClickAtRef = useRef<number>(0);
|
|
|
|
// Determine phase states based on current data
|
|
const phases = useMemo((): Phase[] => {
|
|
const researchCompleted = !!research;
|
|
const outlineCompleted = outline.length > 0;
|
|
const contentCompleted = hasContent && contentConfirmed;
|
|
// SEO is complete when analysis exists AND recommendations are applied
|
|
const seoCompleted = !!seoAnalysis && (seoRecommendationsApplied === true || !!seoMetadata);
|
|
|
|
return [
|
|
{
|
|
id: 'research',
|
|
name: 'Research',
|
|
icon: '🔍',
|
|
description: 'Research your topic and gather data',
|
|
completed: researchCompleted,
|
|
current: currentPhase === 'research',
|
|
disabled: false // Research is always accessible
|
|
},
|
|
{
|
|
id: 'outline',
|
|
name: 'Outline',
|
|
icon: '📝',
|
|
description: 'Create and refine your blog outline',
|
|
completed: outlineCompleted,
|
|
current: currentPhase === 'outline',
|
|
disabled: !researchCompleted // Disabled only if research not completed (can always go back if completed)
|
|
},
|
|
{
|
|
id: 'content',
|
|
name: 'Content',
|
|
icon: '✍️',
|
|
description: 'Generate and edit your blog content',
|
|
completed: contentCompleted,
|
|
current: currentPhase === 'content',
|
|
disabled: !outlineCompleted // Disabled only if outline not completed (can always go back if completed)
|
|
},
|
|
{
|
|
id: 'seo',
|
|
name: 'SEO',
|
|
icon: '📈',
|
|
description: 'Optimize for search engines',
|
|
completed: seoCompleted,
|
|
current: currentPhase === 'seo',
|
|
disabled: !contentCompleted // Disabled only if content not completed (can always go back if completed)
|
|
},
|
|
{
|
|
id: 'publish',
|
|
name: 'Publish',
|
|
icon: '🚀',
|
|
description: 'Publish your blog post',
|
|
completed: false, // This would be set when actually published
|
|
current: currentPhase === 'publish',
|
|
disabled: !seoCompleted // Can access if SEO done
|
|
}
|
|
];
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [research, outline, outlineConfirmed, hasContent, contentConfirmed, seoAnalysis, seoMetadata, seoRecommendationsApplied, currentPhase]);
|
|
|
|
// Persist current phase and user selection
|
|
useEffect(() => {
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
window.localStorage.setItem('blogwriter_current_phase', currentPhase);
|
|
window.localStorage.setItem('blogwriter_user_selected_phase', String(userSelectedPhase));
|
|
}
|
|
} catch {}
|
|
}, [currentPhase, userSelectedPhase]);
|
|
|
|
// Validate stored phase against current availability (quiet)
|
|
useEffect(() => {
|
|
// Allow empty string as a valid phase (landing page state)
|
|
if (currentPhase === '') {
|
|
return; // Don't validate empty phase - it's intentional for landing page
|
|
}
|
|
|
|
const current = phases.find(p => p.id === currentPhase);
|
|
if (!current) {
|
|
// If phase not found and no research exists, go to landing (empty string)
|
|
// Otherwise, default to research
|
|
setCurrentPhase(research ? 'research' : '');
|
|
return;
|
|
}
|
|
if (current.disabled) {
|
|
// Find the first non-disabled phase in order of progression the user qualifies for
|
|
// If no research exists, default to landing (empty string) instead of research
|
|
const fallback = phases.find(p => !p.disabled) || ({ id: research ? 'research' : '' } as Phase);
|
|
if (fallback.id !== currentPhase) {
|
|
setCurrentPhase(fallback.id);
|
|
}
|
|
}
|
|
}, [phases, currentPhase, research]);
|
|
|
|
// Auto-update current phase based on completion status (only if user hasn't manually selected a phase)
|
|
useEffect(() => {
|
|
if (userSelectedPhase) {
|
|
return; // Don't auto-update if user has manually selected a phase
|
|
}
|
|
|
|
// If no research exists and phase is empty/landing, stay on landing
|
|
if (!research && currentPhase === '') {
|
|
return; // Keep showing landing page
|
|
}
|
|
|
|
// Auto-progress to the next available phase when conditions are met
|
|
if (research && outline.length === 0) {
|
|
// Research completed, but no outline yet - stay on research
|
|
if (currentPhase !== 'research') {
|
|
setCurrentPhase('research');
|
|
}
|
|
} else if (research && outline.length > 0 && !outlineConfirmed) {
|
|
// Outline created but not confirmed - move to outline phase
|
|
if (currentPhase !== 'outline') {
|
|
setCurrentPhase('outline');
|
|
}
|
|
} else if (outlineConfirmed && hasContent && !contentConfirmed) {
|
|
// Content generated but not confirmed - move to content phase
|
|
if (currentPhase !== 'content') {
|
|
setCurrentPhase('content');
|
|
}
|
|
} else if (contentConfirmed && !seoAnalysis) {
|
|
// Content confirmed but no SEO analysis yet - move to SEO phase
|
|
if (currentPhase !== 'seo') {
|
|
setCurrentPhase('seo');
|
|
}
|
|
} else if (seoAnalysis && !seoRecommendationsApplied && !seoMetadata) {
|
|
// SEO analysis done but recommendations not applied - stay on SEO phase
|
|
if (currentPhase !== 'seo') {
|
|
setCurrentPhase('seo');
|
|
}
|
|
} else if (seoAnalysis && (seoRecommendationsApplied || seoMetadata)) {
|
|
// SEO recommendations applied or metadata generated
|
|
if (currentPhase === 'seo') {
|
|
// CRITICAL: Stay in SEO phase so user can review updated content - don't auto-progress
|
|
// User will manually navigate to publish when ready
|
|
// This prevents blank screen by keeping user in SEO phase where BlogEditor is visible
|
|
// No action needed - already in SEO phase, stay here
|
|
} else {
|
|
// User is NOT in SEO phase - can progress to publish
|
|
// This handles cases where user navigates away and comes back
|
|
// Only auto-progress if user is already in a different phase (not actively in SEO)
|
|
if (currentPhase !== 'publish') {
|
|
setCurrentPhase('publish');
|
|
}
|
|
}
|
|
}
|
|
}, [research, outline, outlineConfirmed, hasContent, contentConfirmed, seoAnalysis, seoMetadata, seoRecommendationsApplied, currentPhase, userSelectedPhase]);
|
|
|
|
const navigateToPhase = useCallback((phaseId: string) => {
|
|
// Minimal debounce (200ms) to avoid race conditions on rapid clicks
|
|
const now = Date.now();
|
|
if (now - lastClickAtRef.current < 200) { return; }
|
|
lastClickAtRef.current = now;
|
|
|
|
const phase = phases.find(p => p.id === phaseId);
|
|
|
|
if (phase && !phase.disabled) {
|
|
setCurrentPhase(phaseId);
|
|
setUserSelectedPhase(true); // Mark that user has manually selected a phase
|
|
} else {
|
|
// Quietly ignore blocked navigation
|
|
}
|
|
}, [phases, currentPhase]);
|
|
|
|
// Reset user selection when a new phase is completed (to allow auto-progression)
|
|
const resetUserSelection = () => {
|
|
setUserSelectedPhase(false);
|
|
};
|
|
|
|
return {
|
|
phases,
|
|
currentPhase,
|
|
navigateToPhase,
|
|
setCurrentPhase,
|
|
resetUserSelection
|
|
};
|
|
};
|
|
|
|
export default usePhaseNavigation;
|