- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField - Fix blog title not truncating: add min-w-0 for flex item overflow - Fix outline generation 500: escape curly braces in f-string prompt template - Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager - Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient - Fix hallucination detector 404: explicitly include router in main.py and app.py - Fix missing error_data in task failure responses - Hide CopilotKit web inspector button - Remove hardcoded fallback suggestions from SmartTypingAssist - Fix stale closure refs in SmartTypingAssist handleTypingChange - Add two-column editor layout, stats bar, section hover menu - Various subscription, billing, and research module improvements
646 lines
21 KiB
TypeScript
646 lines
21 KiB
TypeScript
import React, { useRef, useCallback, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import Dialog from '@mui/material/Dialog';
|
|
import DialogTitle from '@mui/material/DialogTitle';
|
|
import DialogContent from '@mui/material/DialogContent';
|
|
import DialogContentText from '@mui/material/DialogContentText';
|
|
import DialogActions from '@mui/material/DialogActions';
|
|
import Button from '@mui/material/Button';
|
|
import { debug } from '../../utils/debug';
|
|
import WriterCopilotSidebar from './BlogWriterUtils/WriterCopilotSidebar';
|
|
import { blogWriterApi } from '../../services/blogWriterApi';
|
|
import { useClaimFixer } from '../../hooks/useClaimFixer';
|
|
import { useMarkdownProcessor } from '../../hooks/useMarkdownProcessor';
|
|
import { useBlogWriterState } from '../../hooks/useBlogWriterState';
|
|
import HallucinationChecker from './HallucinationChecker';
|
|
import Publisher from './Publisher';
|
|
import OutlineGenerator from './OutlineGenerator';
|
|
import OutlineRefiner from './OutlineRefiner';
|
|
import { SEOProcessor } from './SEO';
|
|
import TaskProgressModals from './BlogWriterUtils/TaskProgressModals';
|
|
import { SEOAnalysisModal } from './SEOAnalysisModal';
|
|
import { SEOMetadataModal } from './SEOMetadataModal';
|
|
import { usePhaseNavigation } from '../../hooks/usePhaseNavigation';
|
|
import HeaderBar from './BlogWriterUtils/HeaderBar';
|
|
import PhaseContent from './BlogWriterUtils/PhaseContent';
|
|
import useBlogWriterCopilotActions from './BlogWriterUtils/useBlogWriterCopilotActions';
|
|
import { useCopilotKitHealth } from '../../hooks/useCopilotKitHealth';
|
|
import { useSEOManager } from './BlogWriterUtils/useSEOManager';
|
|
import { usePhaseActionHandlers } from './BlogWriterUtils/usePhaseActionHandlers';
|
|
import { useBlogWriterPolling } from './BlogWriterUtils/useBlogWriterPolling';
|
|
import { useCopilotSuggestions } from './BlogWriterUtils/useCopilotSuggestions';
|
|
import { usePhaseRestoration } from './BlogWriterUtils/usePhaseRestoration';
|
|
import { useModalVisibility } from './BlogWriterUtils/useModalVisibility';
|
|
import { useBlogWriterRefs } from './BlogWriterUtils/useBlogWriterRefs';
|
|
import { BlogWriterLandingSection } from './BlogWriterUtils/BlogWriterLandingSection';
|
|
import { CopilotKitComponents } from './BlogWriterUtils/CopilotKitComponents';
|
|
|
|
const BlogWriter: React.FC = () => {
|
|
// Add light theme class to body/html on mount, remove on unmount
|
|
React.useEffect(() => {
|
|
document.body.classList.add('blog-writer-page');
|
|
document.documentElement.classList.add('blog-writer-page');
|
|
return () => {
|
|
document.body.classList.remove('blog-writer-page');
|
|
document.documentElement.classList.remove('blog-writer-page');
|
|
};
|
|
}, []);
|
|
|
|
// Check CopilotKit health status
|
|
const { isAvailable: copilotKitAvailable } = useCopilotKitHealth({
|
|
enabled: true, // Enable health checking
|
|
});
|
|
|
|
const navigate = useNavigate();
|
|
|
|
// Use custom hook for all state management
|
|
const {
|
|
research,
|
|
outline,
|
|
titleOptions,
|
|
selectedTitle,
|
|
sections,
|
|
seoAnalysis,
|
|
genMode,
|
|
seoMetadata,
|
|
continuityRefresh,
|
|
outlineTaskId,
|
|
sourceMappingStats,
|
|
groundingInsights,
|
|
optimizationResults,
|
|
researchCoverage,
|
|
researchTitles,
|
|
aiGeneratedTitles,
|
|
outlineConfirmed,
|
|
contentConfirmed,
|
|
flowAnalysisCompleted,
|
|
flowAnalysisResults,
|
|
sectionImages,
|
|
setResearch,
|
|
setOutline,
|
|
setTitleOptions,
|
|
setSelectedTitle,
|
|
setSections,
|
|
setSeoAnalysis,
|
|
setGenMode,
|
|
setSeoMetadata,
|
|
setContinuityRefresh,
|
|
setOutlineTaskId,
|
|
setContentConfirmed,
|
|
setOutlineConfirmed,
|
|
setFlowAnalysisCompleted,
|
|
setFlowAnalysisResults,
|
|
setSectionImages,
|
|
handleResearchComplete,
|
|
handleOutlineComplete,
|
|
handleOutlineError,
|
|
handleTitleSelect,
|
|
handleCustomTitle,
|
|
handleOutlineConfirmed,
|
|
handleOutlineRefined,
|
|
handleContentUpdate,
|
|
handleContentSave
|
|
} = useBlogWriterState();
|
|
|
|
// SEO Manager - handles all SEO-related logic
|
|
// Initialize phase navigation with temporary false value for seoRecommendationsApplied
|
|
const [tempSeoRecommendationsApplied] = React.useState(false);
|
|
const {
|
|
phases: tempPhases,
|
|
currentPhase: tempCurrentPhase,
|
|
navigateToPhase: tempNavigateToPhase,
|
|
setCurrentPhase: tempSetCurrentPhase,
|
|
resetUserSelection
|
|
} = usePhaseNavigation(
|
|
research,
|
|
outline,
|
|
outlineConfirmed,
|
|
Object.keys(sections).length > 0,
|
|
contentConfirmed,
|
|
seoAnalysis,
|
|
seoMetadata,
|
|
tempSeoRecommendationsApplied
|
|
);
|
|
|
|
const {
|
|
isSEOAnalysisModalOpen,
|
|
setIsSEOAnalysisModalOpen,
|
|
isSEOMetadataModalOpen,
|
|
setIsSEOMetadataModalOpen,
|
|
seoRecommendationsApplied,
|
|
setSeoRecommendationsApplied,
|
|
lastSEOModalOpenRef,
|
|
runSEOAnalysisDirect,
|
|
handleApplySeoRecommendations,
|
|
handleSEOAnalysisComplete,
|
|
handleSEOModalClose,
|
|
confirmBlogContent,
|
|
} = useSEOManager({
|
|
sections,
|
|
research,
|
|
outline,
|
|
selectedTitle,
|
|
contentConfirmed,
|
|
seoAnalysis,
|
|
currentPhase: tempCurrentPhase,
|
|
navigateToPhase: tempNavigateToPhase,
|
|
setContentConfirmed,
|
|
setSeoAnalysis,
|
|
setSeoMetadata,
|
|
setSections,
|
|
setSelectedTitle: setSelectedTitle as (title: string | null) => void,
|
|
setContinuityRefresh,
|
|
setFlowAnalysisCompleted,
|
|
setFlowAnalysisResults,
|
|
});
|
|
|
|
// Phase navigation hook with correct seoRecommendationsApplied
|
|
const {
|
|
phases,
|
|
currentPhase,
|
|
navigateToPhase,
|
|
setCurrentPhase,
|
|
} = usePhaseNavigation(
|
|
research,
|
|
outline,
|
|
outlineConfirmed,
|
|
Object.keys(sections).length > 0,
|
|
contentConfirmed,
|
|
seoAnalysis,
|
|
seoMetadata,
|
|
seoRecommendationsApplied
|
|
);
|
|
|
|
// Update ref when navigateToPhase changes
|
|
React.useEffect(() => {
|
|
navigateToPhaseRef.current = navigateToPhase;
|
|
}, [navigateToPhase]);
|
|
|
|
// Phase restoration logic
|
|
usePhaseRestoration({
|
|
copilotKitAvailable,
|
|
research,
|
|
phases,
|
|
currentPhase,
|
|
navigateToPhase,
|
|
setCurrentPhase,
|
|
});
|
|
|
|
// All SEO management logic is now in useSEOManager hook above
|
|
|
|
// Custom hooks for complex functionality
|
|
const { buildFullMarkdown, buildUpdatedMarkdownForClaim, applyClaimFix } = useClaimFixer(
|
|
outline,
|
|
sections,
|
|
setSections
|
|
);
|
|
|
|
const { convertMarkdownToHTML } = useMarkdownProcessor(
|
|
outline,
|
|
sections
|
|
);
|
|
|
|
// Store navigateToPhase in a ref for use in polling callbacks
|
|
const navigateToPhaseRef = React.useRef<((phase: string) => void) | null>(null);
|
|
|
|
// Polling hooks - extracted to useBlogWriterPolling
|
|
const {
|
|
researchPolling,
|
|
outlinePolling,
|
|
mediumPolling,
|
|
rewritePolling,
|
|
researchPollingState,
|
|
outlinePollingState,
|
|
mediumPollingState,
|
|
} = useBlogWriterPolling({
|
|
onResearchComplete: handleResearchComplete,
|
|
onOutlineComplete: handleOutlineComplete,
|
|
onOutlineError: handleOutlineError,
|
|
onSectionsUpdate: setSections,
|
|
onContentConfirmed: () => {
|
|
debug.log('[BlogWriter] Content generation completed - auto-confirming content');
|
|
setContentConfirmed(true);
|
|
},
|
|
navigateToPhase: (phase) => {
|
|
debug.log('[BlogWriter] Navigating to phase after content generation', { phase });
|
|
// Use ref to access navigateToPhase (defined later in component)
|
|
if (navigateToPhaseRef.current) {
|
|
setTimeout(() => {
|
|
navigateToPhaseRef.current?.(phase);
|
|
}, 0);
|
|
}
|
|
},
|
|
});
|
|
|
|
// Modal visibility management - extracted to useModalVisibility
|
|
const {
|
|
showModal,
|
|
showOutlineModal,
|
|
setShowOutlineModal,
|
|
isMediumGenerationStarting,
|
|
setIsMediumGenerationStarting,
|
|
} = useModalVisibility({
|
|
mediumPolling,
|
|
rewritePolling,
|
|
outlinePolling,
|
|
});
|
|
|
|
// CopilotKit suggestions management - extracted to useCopilotSuggestions
|
|
// Check if sections exist AND have actual content (not just empty strings)
|
|
const hasContent = React.useMemo(() => {
|
|
const sectionKeys = Object.keys(sections);
|
|
if (sectionKeys.length === 0) return false;
|
|
// Check if at least one section has actual content
|
|
const sectionsWithContent = Object.values(sections).filter(c => c && c.trim().length > 0);
|
|
return sectionsWithContent.length > 0;
|
|
}, [sections]);
|
|
const {
|
|
suggestions,
|
|
setSuggestionsRef,
|
|
} = useCopilotSuggestions({
|
|
research,
|
|
outline,
|
|
outlineConfirmed,
|
|
researchPollingState,
|
|
outlinePollingState,
|
|
mediumPollingState,
|
|
hasContent,
|
|
flowAnalysisCompleted,
|
|
contentConfirmed,
|
|
seoAnalysis,
|
|
seoMetadata,
|
|
seoRecommendationsApplied,
|
|
});
|
|
|
|
// Refs and tracking logic - extracted to useBlogWriterRefs
|
|
useBlogWriterRefs({
|
|
research,
|
|
outline,
|
|
outlineConfirmed,
|
|
contentConfirmed,
|
|
sections,
|
|
currentPhase,
|
|
isSEOAnalysisModalOpen,
|
|
resetUserSelection,
|
|
});
|
|
|
|
const handlePhaseClick = useCallback((phaseId: string) => {
|
|
navigateToPhase(phaseId);
|
|
// When clicking Research phase, ensure we navigate to research phase (this will trigger research form to show)
|
|
if (phaseId === 'research' && !research) {
|
|
debug.log('[BlogWriter] Research phase clicked - navigating to research phase to show form');
|
|
// navigateToPhase already called above, which will set currentPhase to 'research'
|
|
// BlogWriterLandingSection will detect currentPhase === 'research' and show ManualResearchForm
|
|
}
|
|
if (phaseId === 'seo') {
|
|
if (seoAnalysis) {
|
|
setIsSEOAnalysisModalOpen(true);
|
|
debug.log('[BlogWriter] SEO modal opened (phase navigation)');
|
|
} else {
|
|
runSEOAnalysisDirect();
|
|
}
|
|
}
|
|
}, [navigateToPhase, seoAnalysis, research, runSEOAnalysisDirect, setIsSEOAnalysisModalOpen]);
|
|
|
|
const handleNewBlog = useCallback(() => {
|
|
setResearch(null);
|
|
setOutline([]);
|
|
setSections({});
|
|
setSeoAnalysis(null);
|
|
setSeoMetadata(null);
|
|
setContentConfirmed(false);
|
|
setOutlineConfirmed(false);
|
|
setSelectedTitle('');
|
|
setTitleOptions([]);
|
|
setCurrentPhase('');
|
|
try {
|
|
localStorage.removeItem('blog_outline');
|
|
localStorage.removeItem('blog_title_options');
|
|
localStorage.removeItem('blog_selected_title');
|
|
localStorage.removeItem('blogwriter_current_phase');
|
|
localStorage.removeItem('blogwriter_user_selected_phase');
|
|
localStorage.removeItem('blog_content_confirmed');
|
|
localStorage.removeItem('blog_seo_recommendations_applied');
|
|
} catch {
|
|
// ignore localStorage errors
|
|
}
|
|
}, [setResearch, setOutline, setSections, setSeoAnalysis, setSeoMetadata,
|
|
setContentConfirmed, setOutlineConfirmed, setSelectedTitle, setTitleOptions,
|
|
setCurrentPhase]);
|
|
|
|
const handleMyBlogs = useCallback(() => {
|
|
navigate('/asset-library?source_module=blog_writer&asset_type=text');
|
|
}, [navigate]);
|
|
|
|
const [newBlogDialogOpen, setNewBlogDialogOpen] = useState(false);
|
|
|
|
const hasExistingWork = !!(research || outline.length > 0 || Object.keys(sections).length > 0);
|
|
|
|
const confirmNewBlog = useCallback(() => {
|
|
if (hasExistingWork) {
|
|
setNewBlogDialogOpen(true);
|
|
} else {
|
|
handleNewBlog();
|
|
}
|
|
}, [hasExistingWork, handleNewBlog]);
|
|
|
|
const outlineGenRef = useRef<any>(null);
|
|
|
|
// Callback to handle cached outline completion
|
|
const handleCachedOutlineComplete = useCallback((result: { outline: any[], title_options?: string[] }) => {
|
|
if (result.outline && Array.isArray(result.outline)) {
|
|
handleOutlineComplete(result);
|
|
}
|
|
}, [handleOutlineComplete]);
|
|
|
|
// Callback to handle cached content completion
|
|
const handleCachedContentComplete = useCallback((cachedSections: Record<string, string>) => {
|
|
if (cachedSections && Object.keys(cachedSections).length > 0) {
|
|
setSections(cachedSections);
|
|
debug.log('[BlogWriter] Cached content loaded into state', { sections: Object.keys(cachedSections).length });
|
|
}
|
|
}, [setSections]);
|
|
|
|
// Phase action handlers for when CopilotKit is unavailable - extracted to usePhaseActionHandlers
|
|
const {
|
|
handleResearchAction,
|
|
handleOutlineAction,
|
|
handleContentAction,
|
|
handleSEOAction,
|
|
handleApplySEORecommendations,
|
|
handlePublishAction,
|
|
} = usePhaseActionHandlers({
|
|
research,
|
|
outline,
|
|
selectedTitle,
|
|
contentConfirmed,
|
|
sections,
|
|
navigateToPhase,
|
|
handleOutlineConfirmed,
|
|
setIsMediumGenerationStarting,
|
|
mediumPolling,
|
|
outlineGenRef,
|
|
setOutline,
|
|
setContentConfirmed,
|
|
setIsSEOAnalysisModalOpen,
|
|
setIsSEOMetadataModalOpen,
|
|
runSEOAnalysisDirect,
|
|
onResearchComplete: handleResearchComplete,
|
|
onOutlineComplete: handleCachedOutlineComplete,
|
|
onContentComplete: handleCachedContentComplete,
|
|
});
|
|
|
|
// Handle medium generation start from OutlineFeedbackForm
|
|
const handleMediumGenerationStarted = (taskId: string) => {
|
|
console.log('Starting medium generation polling for task:', taskId);
|
|
setIsMediumGenerationStarting(false); // Clear the starting state
|
|
mediumPolling.startPolling(taskId);
|
|
};
|
|
|
|
// Show modal immediately when copilot action is triggered
|
|
const handleMediumGenerationTriggered = () => {
|
|
console.log('Medium generation triggered - showing modal immediately');
|
|
setIsMediumGenerationStarting(true);
|
|
};
|
|
|
|
useBlogWriterCopilotActions({
|
|
isSEOAnalysisModalOpen,
|
|
lastSEOModalOpenRef,
|
|
runSEOAnalysisDirect,
|
|
confirmBlogContent,
|
|
sections,
|
|
research,
|
|
openSEOMetadata: () => setIsSEOMetadataModalOpen(true),
|
|
navigateToPhase,
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
<div style={{
|
|
height: '100vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
backgroundColor: '#ffffff',
|
|
color: '#1a1a1a',
|
|
overflow: 'auto'
|
|
}} className="blog-writer-container">
|
|
{/* CopilotKit-dependent components - extracted to CopilotKitComponents */}
|
|
{copilotKitAvailable && (
|
|
<CopilotKitComponents
|
|
research={research}
|
|
outline={outline}
|
|
outlineConfirmed={outlineConfirmed}
|
|
sections={sections}
|
|
selectedTitle={selectedTitle}
|
|
onResearchComplete={handleResearchComplete}
|
|
onOutlineCreated={setOutline}
|
|
onOutlineUpdated={setOutline}
|
|
onTitleOptionsSet={setTitleOptions}
|
|
onOutlineConfirmed={handleOutlineConfirmed}
|
|
onOutlineRefined={(feedback?: string) => handleOutlineRefined(feedback || '')}
|
|
onMediumGenerationStarted={handleMediumGenerationStarted}
|
|
onMediumGenerationTriggered={handleMediumGenerationTriggered}
|
|
onRewriteStarted={(taskId) => {
|
|
console.log('Starting rewrite polling for task:', taskId);
|
|
rewritePolling.startPolling(taskId);
|
|
}}
|
|
onRewriteTriggered={() => {
|
|
console.log('Rewrite triggered - showing modal immediately');
|
|
setIsMediumGenerationStarting(true);
|
|
}}
|
|
setFlowAnalysisCompleted={setFlowAnalysisCompleted}
|
|
setFlowAnalysisResults={setFlowAnalysisResults}
|
|
setContinuityRefresh={setContinuityRefresh}
|
|
researchPolling={researchPolling}
|
|
navigateToPhase={navigateToPhase}
|
|
/>
|
|
)}
|
|
|
|
{/* New extracted functionality components */}
|
|
<OutlineGenerator
|
|
ref={outlineGenRef}
|
|
research={research}
|
|
onTaskStart={(taskId) => setOutlineTaskId(taskId)}
|
|
onPollingStart={(taskId) => outlinePolling.startPolling(taskId)}
|
|
onModalShow={() => setShowOutlineModal(true)}
|
|
navigateToPhase={navigateToPhase}
|
|
onOutlineCreated={(outline, titleOptions) => {
|
|
// Handle cached outline from CopilotKit action (same as header button)
|
|
setOutline(outline);
|
|
if (titleOptions) {
|
|
setTitleOptions(titleOptions);
|
|
}
|
|
}}
|
|
/>
|
|
<OutlineRefiner
|
|
outline={outline}
|
|
onOutlineUpdated={setOutline}
|
|
/>
|
|
<SEOProcessor
|
|
buildFullMarkdown={buildFullMarkdown}
|
|
seoMetadata={seoMetadata}
|
|
onSEOAnalysis={setSeoAnalysis}
|
|
onSEOMetadata={setSeoMetadata}
|
|
/>
|
|
<HallucinationChecker
|
|
buildFullMarkdown={buildFullMarkdown}
|
|
buildUpdatedMarkdownForClaim={buildUpdatedMarkdownForClaim}
|
|
applyClaimFix={applyClaimFix}
|
|
/>
|
|
<Publisher
|
|
buildFullMarkdown={buildFullMarkdown}
|
|
convertMarkdownToHTML={convertMarkdownToHTML}
|
|
seoMetadata={seoMetadata}
|
|
/>
|
|
|
|
{/* Phase navigation header - always visible as default interface */}
|
|
<div style={{ flexShrink: 0 }}>
|
|
<HeaderBar
|
|
phases={phases}
|
|
currentPhase={currentPhase}
|
|
onPhaseClick={handlePhaseClick}
|
|
copilotKitAvailable={copilotKitAvailable}
|
|
actionHandlers={{
|
|
onResearchAction: handleResearchAction,
|
|
onOutlineAction: handleOutlineAction,
|
|
onContentAction: handleContentAction,
|
|
onSEOAction: handleSEOAction,
|
|
onApplySEORecommendations: handleApplySEORecommendations,
|
|
onPublishAction: handlePublishAction,
|
|
}}
|
|
hasResearch={!!research}
|
|
hasOutline={outline.length > 0}
|
|
outlineConfirmed={outlineConfirmed}
|
|
hasContent={Object.keys(sections).length > 0}
|
|
contentConfirmed={contentConfirmed}
|
|
hasSEOAnalysis={!!seoAnalysis}
|
|
seoRecommendationsApplied={seoRecommendationsApplied}
|
|
hasSEOMetadata={!!seoMetadata}
|
|
onNewBlog={confirmNewBlog}
|
|
onMyBlogs={handleMyBlogs}
|
|
onHelp={() => window.open('/docs', '_blank')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Landing section - extracted to BlogWriterLandingSection */}
|
|
<BlogWriterLandingSection
|
|
research={research}
|
|
copilotKitAvailable={copilotKitAvailable}
|
|
currentPhase={currentPhase}
|
|
navigateToPhase={navigateToPhase}
|
|
onResearchComplete={handleResearchComplete}
|
|
/>
|
|
|
|
{research && (
|
|
<>
|
|
<PhaseContent
|
|
currentPhase={currentPhase}
|
|
research={research}
|
|
outline={outline}
|
|
outlineConfirmed={outlineConfirmed}
|
|
titleOptions={titleOptions}
|
|
selectedTitle={selectedTitle}
|
|
researchTitles={researchTitles}
|
|
aiGeneratedTitles={aiGeneratedTitles}
|
|
sourceMappingStats={sourceMappingStats}
|
|
groundingInsights={groundingInsights}
|
|
optimizationResults={optimizationResults}
|
|
researchCoverage={researchCoverage}
|
|
setOutline={setOutline}
|
|
sections={sections}
|
|
handleContentUpdate={handleContentUpdate}
|
|
handleContentSave={handleContentSave}
|
|
continuityRefresh={continuityRefresh}
|
|
flowAnalysisResults={flowAnalysisResults}
|
|
outlineGenRef={outlineGenRef}
|
|
blogWriterApi={blogWriterApi}
|
|
sectionImages={sectionImages}
|
|
setSectionImages={setSectionImages}
|
|
contentConfirmed={contentConfirmed}
|
|
seoAnalysis={seoAnalysis}
|
|
seoMetadata={seoMetadata}
|
|
onTitleSelect={handleTitleSelect}
|
|
onCustomTitle={handleCustomTitle}
|
|
copilotKitAvailable={copilotKitAvailable}
|
|
onResearchComplete={handleResearchComplete}
|
|
onOutlineGenerationStart={(taskId) => {
|
|
setOutlineTaskId(taskId);
|
|
outlinePolling.startPolling(taskId);
|
|
setShowOutlineModal(true);
|
|
}}
|
|
onContentGenerationStart={handleMediumGenerationStarted}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<WriterCopilotSidebar
|
|
suggestions={suggestions}
|
|
research={research}
|
|
outline={outline}
|
|
outlineConfirmed={outlineConfirmed}
|
|
/>
|
|
|
|
<TaskProgressModals
|
|
showOutlineModal={showOutlineModal}
|
|
outlinePolling={outlinePolling}
|
|
showModal={showModal}
|
|
rewritePolling={rewritePolling}
|
|
mediumPolling={mediumPolling}
|
|
/>
|
|
|
|
{/* SEO Analysis Modal */}
|
|
<SEOAnalysisModal
|
|
isOpen={isSEOAnalysisModalOpen}
|
|
onClose={handleSEOModalClose}
|
|
blogContent={buildFullMarkdown()}
|
|
blogTitle={selectedTitle}
|
|
researchData={research}
|
|
onApplyRecommendations={handleApplySeoRecommendations}
|
|
onAnalysisComplete={handleSEOAnalysisComplete}
|
|
/>
|
|
|
|
{/* SEO Metadata Modal */}
|
|
<SEOMetadataModal
|
|
isOpen={isSEOMetadataModalOpen}
|
|
onClose={() => setIsSEOMetadataModalOpen(false)}
|
|
blogContent={buildFullMarkdown()}
|
|
blogTitle={selectedTitle}
|
|
researchData={research}
|
|
outline={outline}
|
|
seoAnalysis={seoAnalysis}
|
|
onMetadataGenerated={(metadata) => {
|
|
console.log('SEO metadata generated:', metadata);
|
|
setSeoMetadata(metadata);
|
|
// Metadata is now saved and will be used when publishing to WordPress/Wix
|
|
// The metadata includes all SEO fields (title, description, tags, Open Graph, etc.)
|
|
// Publisher component will use this metadata when calling publish API
|
|
}}
|
|
/>
|
|
|
|
{/* New Blog confirmation dialog */}
|
|
<Dialog
|
|
open={newBlogDialogOpen}
|
|
onClose={() => setNewBlogDialogOpen(false)}
|
|
aria-labelledby="new-blog-dialog-title"
|
|
>
|
|
<DialogTitle id="new-blog-dialog-title">Start New Blog?</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText>
|
|
This will clear all your current work and start a new blog. This action cannot be undone.
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setNewBlogDialogOpen(false)}>Cancel</Button>
|
|
<Button onClick={() => { handleNewBlog(); setNewBlogDialogOpen(false); }} color="primary" variant="contained">
|
|
Start New
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BlogWriter; |