Updated SEO Analysis Modal

This commit is contained in:
ajaysi
2025-09-22 21:02:32 +05:30
parent f98d49cea7
commit 12119d418b
38 changed files with 5742 additions and 2337 deletions

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import { CopilotSidebar } from '@copilotkit/react-ui';
import { useCopilotAction } from '@copilotkit/react-core';
import '@copilotkit/react-ui/styles.css';
import { blogWriterApi } from '../../services/blogWriterApi';
import { useOutlinePolling, useMediumGenerationPolling, useResearchPolling } from '../../hooks/usePolling';
import { useOutlinePolling, useMediumGenerationPolling, useResearchPolling, useRewritePolling } from '../../hooks/usePolling';
import { useClaimFixer } from '../../hooks/useClaimFixer';
import { useMarkdownProcessor } from '../../hooks/useMarkdownProcessor';
import { useBlogWriterState } from '../../hooks/useBlogWriterState';
@@ -18,14 +19,19 @@ import { CustomOutlineForm } from './CustomOutlineForm';
import { ResearchDataActions } from './ResearchDataActions';
import { EnhancedOutlineActions } from './EnhancedOutlineActions';
import HallucinationChecker from './HallucinationChecker';
import { RewriteFeedbackForm } from './RewriteFeedbackForm';
import Publisher from './Publisher';
import OutlineGenerator from './OutlineGenerator';
import OutlineRefiner from './OutlineRefiner';
import SEOProcessor from './SEOProcessor';
import { SEOProcessor } from './SEO';
import BlogWriterLanding from './BlogWriterLanding';
import { OutlineProgressModal } from './OutlineProgressModal';
import OutlineFeedbackForm from './OutlineFeedbackForm';
import { BlogEditor } from './WYSIWYG';
import { SEOAnalysisModal } from './SEOAnalysisModal';
// Type assertion for CopilotKit action
const useCopilotActionTyped = useCopilotAction as any;
export const BlogWriter: React.FC = () => {
// Use custom hook for all state management
@@ -47,13 +53,20 @@ export const BlogWriter: React.FC = () => {
researchTitles,
aiGeneratedTitles,
outlineConfirmed,
contentConfirmed,
flowAnalysisCompleted,
flowAnalysisResults,
setOutline,
setTitleOptions,
setSections,
setSeoAnalysis,
setGenMode,
setSeoMetadata,
setContinuityRefresh,
setOutlineTaskId,
setContentConfirmed,
setFlowAnalysisCompleted,
setFlowAnalysisResults,
handleResearchComplete,
handleOutlineComplete,
handleOutlineError,
@@ -107,6 +120,24 @@ export const BlogWriter: React.FC = () => {
onError: (err) => console.error('Medium generation failed:', err)
});
// Rewrite polling hook (used for blog rewrite operations)
const rewritePolling = useRewritePolling({
onComplete: (result: any) => {
try {
if (result && result.sections) {
const newSections: Record<string, string> = {};
result.sections.forEach((s: any) => {
newSections[String(s.id)] = s.content || '';
});
setSections(newSections);
}
} catch (e) {
console.error('Failed to apply rewrite result:', e);
}
},
onError: (err) => console.error('Rewrite failed:', err)
});
// Get context-aware suggestions based on current task status
const suggestions = useSuggestions(
research,
@@ -114,19 +145,26 @@ export const BlogWriter: React.FC = () => {
outlineConfirmed,
{ isPolling: researchPolling.isPolling, currentStatus: researchPolling.currentStatus },
{ isPolling: outlinePolling.isPolling, currentStatus: outlinePolling.currentStatus },
{ isPolling: mediumPolling.isPolling, currentStatus: mediumPolling.currentStatus }
{ isPolling: mediumPolling.isPolling, currentStatus: mediumPolling.currentStatus },
Object.keys(sections).length > 0, // hasContent
flowAnalysisCompleted, // flowAnalysisCompleted state
contentConfirmed // contentConfirmed state
);
// Add minimum display time for modal
const [showModal, setShowModal] = useState(false);
const [modalStartTime, setModalStartTime] = useState<number | null>(null);
const [isMediumGenerationStarting, setIsMediumGenerationStarting] = useState(false);
const [showOutlineModal, setShowOutlineModal] = useState(false);
// SEO Analysis Modal state
const [isSEOAnalysisModalOpen, setIsSEOAnalysisModalOpen] = useState(false);
useEffect(() => {
if ((mediumPolling.isPolling || isMediumGenerationStarting) && !showModal) {
if ((mediumPolling.isPolling || rewritePolling.isPolling || isMediumGenerationStarting) && !showModal) {
setShowModal(true);
setModalStartTime(Date.now());
} else if (!mediumPolling.isPolling && !isMediumGenerationStarting && showModal) {
} else if (!mediumPolling.isPolling && !rewritePolling.isPolling && !isMediumGenerationStarting && showModal) {
const elapsed = Date.now() - (modalStartTime || 0);
const minDisplayTime = 2000; // 2 seconds minimum
@@ -140,7 +178,19 @@ export const BlogWriter: React.FC = () => {
setModalStartTime(null);
}
}
}, [mediumPolling.isPolling, isMediumGenerationStarting, showModal, modalStartTime]);
}, [mediumPolling.isPolling, rewritePolling.isPolling, isMediumGenerationStarting, showModal, modalStartTime]);
// Handle outline modal visibility
useEffect(() => {
if (outlinePolling.isPolling && !showOutlineModal) {
setShowOutlineModal(true);
} else if (!outlinePolling.isPolling && showOutlineModal) {
// Add a small delay to ensure user sees completion message
setTimeout(() => {
setShowOutlineModal(false);
}, 1000);
}
}, [outlinePolling.isPolling, showOutlineModal]);
// Handle medium generation start from OutlineFeedbackForm
const handleMediumGenerationStarted = (taskId: string) => {
@@ -162,6 +212,62 @@ export const BlogWriter: React.FC = () => {
progressCount: mediumPolling.progressMessages.length
});
// Debug SEO modal state
console.log('🔍 SEO Analysis Modal state:', {
isSEOAnalysisModalOpen,
hasResearch: !!research,
hasContent: !!sections && Object.keys(sections).length > 0,
researchKeys: research ? Object.keys(research) : [],
sectionsKeys: sections ? Object.keys(sections) : []
});
// Debug action registration
console.log('📋 CopilotKit Actions Registered:', ['confirmBlogContent', 'analyzeSEO']);
// Copilot action for confirming blog content
useCopilotActionTyped({
name: "confirmBlogContent",
description: "Confirm that the blog content is ready and move to the next stage (SEO analysis)",
parameters: [],
handler: async () => {
console.log('Blog content confirmed by user');
setContentConfirmed(true);
return "Blog content has been confirmed! You can now proceed with SEO analysis and publishing.";
}
});
// Copilot action for running SEO analysis
useCopilotActionTyped({
name: "analyzeSEO",
description: "Analyze the blog content for SEO optimization and provide detailed recommendations",
parameters: [],
handler: async () => {
console.log('🚀 SEO Analysis Action Triggered!');
console.log('Current modal state before:', isSEOAnalysisModalOpen);
console.log('Sections available:', !!sections && Object.keys(sections).length > 0);
console.log('Research data available:', !!research && !!research.keyword_analysis);
// Check if we have content to analyze
if (!sections || Object.keys(sections).length === 0) {
console.log('❌ No content available for SEO analysis');
return "No blog content available for SEO analysis. Please generate content first.";
}
// Check if we have research data
if (!research || !research.keyword_analysis) {
console.log('❌ No research data available for SEO analysis');
return "Research data is required for SEO analysis. Please run research first.";
}
// Open SEO analysis modal
console.log('✅ All checks passed, opening SEO analysis modal');
setIsSEOAnalysisModalOpen(true);
console.log('Modal state set to true');
return "Running SEO analysis of your blog content. This will analyze content structure, keyword optimization, readability, and provide actionable recommendations.";
}
});
@@ -191,13 +297,41 @@ export const BlogWriter: React.FC = () => {
onOutlineRefined={handleOutlineRefined}
onMediumGenerationStarted={handleMediumGenerationStarted}
onMediumGenerationTriggered={handleMediumGenerationTriggered}
sections={sections}
blogTitle={selectedTitle}
onFlowAnalysisComplete={(analysis) => {
console.log('Flow analysis completed:', analysis);
setFlowAnalysisCompleted(true);
setFlowAnalysisResults(analysis);
// Trigger a refresh of continuity badges
setContinuityRefresh((prev: number) => (prev || 0) + 1);
}}
/>
{/* Rewrite Feedback Form - Only show when content exists */}
{Object.keys(sections).length > 0 && (
<RewriteFeedbackForm
research={research!}
outline={outline}
sections={sections}
blogTitle={selectedTitle}
onRewriteStarted={(taskId) => {
console.log('Starting rewrite polling for task:', taskId);
rewritePolling.startPolling(taskId);
}}
onRewriteTriggered={() => {
console.log('Rewrite triggered - showing modal immediately');
setIsMediumGenerationStarting(true);
}}
/>
)}
{/* New extracted functionality components */}
<OutlineGenerator
research={research}
onTaskStart={(taskId) => setOutlineTaskId(taskId)}
onPollingStart={(taskId) => outlinePolling.startPolling(taskId)}
onModalShow={() => setShowOutlineModal(true)}
/>
<OutlineRefiner
outline={outline}
@@ -239,17 +373,19 @@ export const BlogWriter: React.FC = () => {
<div>
{outlineConfirmed ? (
/* WYSIWYG Editor - Show when outline is confirmed */
<BlogEditor
outline={outline}
research={research}
initialTitle={selectedTitle}
titleOptions={titleOptions}
researchTitles={researchTitles}
aiGeneratedTitles={aiGeneratedTitles}
sections={sections}
onContentUpdate={handleContentUpdate}
onSave={handleContentSave}
/>
<BlogEditor
outline={outline}
research={research}
initialTitle={selectedTitle}
titleOptions={titleOptions}
researchTitles={researchTitles}
aiGeneratedTitles={aiGeneratedTitles}
sections={sections}
onContentUpdate={handleContentUpdate}
onSave={handleContentSave}
continuityRefresh={continuityRefresh}
flowAnalysisResults={flowAnalysisResults}
/>
) : (
/* Outline Editor - Show when outline is not confirmed */
<>
@@ -374,9 +510,9 @@ Available tools:
- enhanceSection(sectionId: string, focus?: string) - Enhance a specific section with AI improvements
- optimizeOutline(focus?: string) - Optimize entire outline for better flow, SEO, and engagement
- rebalanceOutline(targetWords?: number) - Rebalance word count distribution across sections
- runSEOAnalyze(keywords?: string)
- confirmBlogContent() - Confirm that blog content is ready and move to SEO stage
- analyzeSEO() - Analyze SEO for blog content with comprehensive insights and visual interface
- generateSEOMetadata(title?: string)
- runHallucinationCheck()
- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string)
CRITICAL BEHAVIOR & USER GUIDANCE:
@@ -392,16 +528,26 @@ Available tools:
- After outline generation, ALWAYS guide user to review and confirm the outline
- If user wants to discuss the outline, use chatWithOutline() to provide insights and answer questions
- If user wants to refine the outline, use refineOutline() to collect their feedback and refine
- When user clicks "Confirm & Generate Content", ONLY call confirmOutlineAndGenerateContent() - DO NOT automatically generate content
- When user says "I confirm the outline" or "I confirm the outline and am ready to generate content" or clicks "Confirm & Generate Content", IMMEDIATELY call confirmOutlineAndGenerateContent() - DO NOT ask for additional confirmation
- CRITICAL: If user explicitly confirms the outline, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given
- Only after outline confirmation, show content generation suggestions and wait for user to explicitly request content generation
- When user asks to generate content before outline confirmation, remind them to confirm the outline first
- Content generation should ONLY happen when user explicitly clicks "Generate all sections" or "Generate [specific section]"
- When user has generated content and wants to rewrite, use rewriteBlog() to collect feedback and rewriteBlog() to process
- For rewrite requests, collect detailed feedback about what they want to change, tone, audience, and focus
- After content generation, guide users to review and confirm their content before moving to SEO stage
- When user says "I have reviewed and confirmed my blog content is ready for the next stage" or clicks "Next: Confirm Blog Content", IMMEDIATELY call confirmBlogContent() - DO NOT ask for additional confirmation
- CRITICAL: If user explicitly confirms blog content, do NOT ask "are you sure?" or "please confirm" - the confirmation is already given
- Only after content confirmation, show SEO analysis and publishing suggestions
- When user asks for SEO analysis before content confirmation, remind them to confirm the content first
- For SEO analysis, ALWAYS use analyzeSEO() - this is the ONLY SEO analysis tool available and provides comprehensive insights with visual interface
- IMPORTANT: There is NO "basic" or "simple" SEO analysis - only the comprehensive one. Do NOT mention multiple SEO analysis options
ENGAGEMENT TACTICS:
- DO NOT ask for clarification - take action immediately with the information provided
- Always call the appropriate tool instead of just talking about what you could do
- Be aware of the current state and reference research results when relevant
- Guide users through the process: Research → Outline → Outline Review & Confirmation → Content → SEO → Publish
- Guide users through the process: Research → Outline → Outline Review & Confirmation → Content → Content Review & Confirmation → SEO → Publish
- Use encouraging language and highlight progress made
- If user seems lost, remind them of the current stage and suggest the next step
- When research is complete, emphasize the value of the data found and guide to outline creation
@@ -415,21 +561,36 @@ Available tools:
{/* Outline Progress Modal */}
{/* Outline modal */}
<OutlineProgressModal
isVisible={outlinePolling.isPolling}
isVisible={showOutlineModal}
status={outlinePolling.currentStatus}
progressMessages={outlinePolling.progressMessages.map(m => m.message)}
latestMessage={outlinePolling.progressMessages.length > 0 ? outlinePolling.progressMessages[outlinePolling.progressMessages.length - 1].message : ''}
error={outlinePolling.error}
/>
{/* Medium generation modal */}
{/* Medium generation / Rewrite modal */}
<OutlineProgressModal
isVisible={showModal}
status={mediumPolling.currentStatus}
progressMessages={mediumPolling.progressMessages.map(m => m.message)}
latestMessage={mediumPolling.progressMessages.length > 0 ? mediumPolling.progressMessages[mediumPolling.progressMessages.length - 1].message : ''}
error={mediumPolling.error}
titleOverride={'📝 Generating Your Blog Content'}
status={rewritePolling.isPolling ? rewritePolling.currentStatus : mediumPolling.currentStatus}
progressMessages={rewritePolling.isPolling ? rewritePolling.progressMessages.map(m => m.message) : mediumPolling.progressMessages.map(m => m.message)}
latestMessage={rewritePolling.isPolling ?
(rewritePolling.progressMessages.length > 0 ? rewritePolling.progressMessages[rewritePolling.progressMessages.length - 1].message : '') :
(mediumPolling.progressMessages.length > 0 ? mediumPolling.progressMessages[mediumPolling.progressMessages.length - 1].message : '')
}
error={rewritePolling.isPolling ? rewritePolling.error : mediumPolling.error}
titleOverride={rewritePolling.isPolling ? '🔄 Rewriting Your Blog' : '📝 Generating Your Blog Content'}
/>
{/* SEO Analysis Modal */}
<SEOAnalysisModal
isOpen={isSEOAnalysisModalOpen}
onClose={() => setIsSEOAnalysisModalOpen(false)}
blogContent={buildFullMarkdown()}
researchData={research}
onApplyRecommendations={(recommendations) => {
console.log('Applying SEO recommendations:', recommendations);
// TODO: Implement recommendation application logic
}}
/>
</div>
);

View File

@@ -1,26 +1,90 @@
import React, { useEffect, useState } from 'react';
import { blogWriterApi } from '../../services/blogWriterApi';
interface Props { sectionId: string; refreshToken?: number }
interface Props {
sectionId: string;
refreshToken?: number;
disabled?: boolean;
flowAnalysisResults?: any;
}
export const ContinuityBadge: React.FC<Props> = ({ sectionId, refreshToken }) => {
export const ContinuityBadge: React.FC<Props> = ({ sectionId, refreshToken, disabled = false, flowAnalysisResults }) => {
const [metrics, setMetrics] = useState<Record<string, number> | null>(null);
const [hover, setHover] = useState(false);
useEffect(() => {
let mounted = true;
// If we have flow analysis results, use them instead of API call
if (flowAnalysisResults && flowAnalysisResults.sections) {
console.log('🔍 [ContinuityBadge] Flow analysis results available:', flowAnalysisResults);
console.log('🔍 [ContinuityBadge] Looking for section ID:', sectionId);
console.log('🔍 [ContinuityBadge] Available section IDs:', flowAnalysisResults.sections.map((s: any) => s.section_id));
const sectionAnalysis = flowAnalysisResults.sections.find((s: any) => s.section_id === sectionId);
if (sectionAnalysis) {
console.log('🔍 [ContinuityBadge] Found section analysis:', sectionAnalysis);
if (mounted) {
setMetrics({
flow: sectionAnalysis.flow_score, // Already in decimal format (0.0-1.0)
consistency: sectionAnalysis.consistency_score,
progression: sectionAnalysis.progression_score
});
}
return;
} else {
console.log('🔍 [ContinuityBadge] No matching section found for ID:', sectionId);
}
}
// Fallback to API call if no flow analysis results
console.log('🔍 [ContinuityBadge] Fetching continuity for section:', sectionId);
blogWriterApi.getContinuity(sectionId)
.then(res => { if (mounted) setMetrics(res.continuity_metrics || null); })
.catch(() => { /* ignore */ });
.then(res => {
console.log('🔍 [ContinuityBadge] Received continuity data:', res);
if (mounted) setMetrics(res.continuity_metrics || null);
})
.catch((error) => {
console.log('🔍 [ContinuityBadge] Error fetching continuity:', error);
/* ignore */
});
return () => { mounted = false; };
}, [sectionId, refreshToken]);
}, [sectionId, refreshToken, flowAnalysisResults]);
if (!metrics) return null;
const flow = Math.round(((metrics.flow || 0) * 100));
const color = flow >= 80 ? '#2e7d32' : flow >= 60 ? '#f9a825' : '#c62828';
const consistency = Math.round(((metrics.consistency || 0) * 100));
const progression = Math.round(((metrics.progression || 0) * 100));
// Show badge even if metrics are null (for debugging)
const flow = metrics ? Math.round(((metrics.flow || 0) * 100)) : 0;
const consistency = metrics ? Math.round(((metrics.consistency || 0) * 100)) : 0;
const progression = metrics ? Math.round(((metrics.progression || 0) * 100)) : 0;
// Enable badge if we have flow analysis results or metrics
const isEnabled = !disabled || (flowAnalysisResults && flowAnalysisResults.sections) || metrics;
// Enhanced color coding with actionable feedback
const getFlowColor = (score: number) => {
if (score >= 80) return '#2e7d32'; // Green - Excellent
if (score >= 60) return '#f9a825'; // Yellow - Good
return '#c62828'; // Red - Needs improvement
};
const getFlowSuggestion = (score: number) => {
if (score >= 80) return "🎉 Excellent narrative flow!";
if (score >= 60) return "💡 Good flow - try connecting ideas more smoothly";
return "🔧 Consider adding transitions between paragraphs";
};
const getConsistencySuggestion = (score: number) => {
if (score >= 80) return "✨ Consistent tone and style";
if (score >= 60) return "📝 Good consistency - maintain your voice";
return "🎯 Work on maintaining consistent tone throughout";
};
const getProgressionSuggestion = (score: number) => {
if (score >= 80) return "🚀 Great logical progression!";
if (score >= 60) return "📈 Good progression - build on previous points";
return "🔗 Strengthen connections between ideas";
};
const color = getFlowColor(flow);
return (
<span
@@ -29,21 +93,23 @@ export const ContinuityBadge: React.FC<Props> = ({ sectionId, refreshToken }) =>
style={{ position: 'relative', display: 'inline-block' }}
>
<span
title={`Flow ${flow}%`}
title={!isEnabled ? 'Flow analysis disabled - use Copilot to enable' : (metrics ? `Flow ${flow}%` : 'Flow metrics not available')}
style={{
display: 'inline-block',
fontSize: 12,
color: color,
border: `1px solid ${color}`,
color: !isEnabled ? '#999' : (metrics ? color : '#666'),
border: `1px solid ${!isEnabled ? '#ddd' : (metrics ? color : '#ccc')}`,
padding: '2px 6px',
borderRadius: 10,
background: 'transparent'
background: !isEnabled ? '#f5f5f5' : 'transparent',
cursor: !isEnabled ? 'not-allowed' : 'default',
opacity: !isEnabled ? 0.6 : 1
}}
>
Flow {flow}%
{!isEnabled ? 'Flow --' : (metrics ? `Flow ${flow}%` : 'Flow --')}
</span>
{hover && (
{hover && isEnabled && (
<div
style={{
position: 'absolute',
@@ -53,21 +119,61 @@ export const ContinuityBadge: React.FC<Props> = ({ sectionId, refreshToken }) =>
background: '#fff',
color: '#333',
border: '1px solid #e0e0e0',
borderRadius: 8,
padding: '8px 10px',
minWidth: 180,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)'
borderRadius: 12,
padding: '12px 16px',
minWidth: 280,
maxWidth: 320,
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
backdropFilter: 'blur(8px)'
}}
>
<div style={{ fontWeight: 600, fontSize: 12, marginBottom: 6 }}>Continuity</div>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between' }}>
<span>Flow</span><span>{flow}%</span>
<div style={{ fontWeight: 700, fontSize: 14, marginBottom: 12, color: '#1a1a1a' }}>
📊 Content Quality Analysis
</div>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between' }}>
<span>Consistency</span><span>{consistency}%</span>
{/* Flow Analysis */}
<div style={{ marginBottom: 12 }}>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 600 }}>Flow</span>
<span style={{ color: getFlowColor(flow), fontWeight: 600 }}>{flow}%</span>
</div>
<div style={{ fontSize: 11, color: '#666', lineHeight: '1.4' }}>
{getFlowSuggestion(flow)}
</div>
</div>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between' }}>
<span>Progression</span><span>{progression}%</span>
{/* Consistency Analysis */}
<div style={{ marginBottom: 12 }}>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 600 }}>Consistency</span>
<span style={{ color: getFlowColor(consistency), fontWeight: 600 }}>{consistency}%</span>
</div>
<div style={{ fontSize: 11, color: '#666', lineHeight: '1.4' }}>
{getConsistencySuggestion(consistency)}
</div>
</div>
{/* Progression Analysis */}
<div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 12, display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 600 }}>Progression</span>
<span style={{ color: getFlowColor(progression), fontWeight: 600 }}>{progression}%</span>
</div>
<div style={{ fontSize: 11, color: '#666', lineHeight: '1.4' }}>
{getProgressionSuggestion(progression)}
</div>
</div>
{/* Overall Quality Indicator */}
<div style={{
borderTop: '1px solid #f0f0f0',
paddingTop: 8,
marginTop: 8,
fontSize: 11,
color: '#888',
fontStyle: 'italic'
}}>
💡 Hover over other sections to compare quality metrics
</div>
</div>
)}

View File

@@ -47,6 +47,9 @@ interface OutlineFeedbackFormProps {
onOutlineRefined: (feedback: string) => void;
onMediumGenerationStarted?: (taskId: string) => void;
onMediumGenerationTriggered?: () => void;
sections?: Record<string, string>;
blogTitle?: string;
onFlowAnalysisComplete?: (analysis: any) => void;
}
@@ -220,13 +223,16 @@ const FeedbackForm: React.FC<{
);
};
export const OutlineFeedbackForm: React.FC<OutlineFeedbackFormProps> = ({
outline,
research,
onOutlineConfirmed,
export const OutlineFeedbackForm: React.FC<OutlineFeedbackFormProps> = ({
outline,
research,
onOutlineConfirmed,
onOutlineRefined,
onMediumGenerationStarted,
onMediumGenerationTriggered
onMediumGenerationTriggered,
sections,
blogTitle,
onFlowAnalysisComplete
}) => {
// Refine outline action with HITL
@@ -492,6 +498,181 @@ export const OutlineFeedbackForm: React.FC<OutlineFeedbackFormProps> = ({
}
});
// Flow Analysis Actions
useCopilotActionTyped({
name: 'analyzeContentQuality',
description: 'Analyze the flow and quality of blog content to get improvement suggestions (basic analysis)',
parameters: [],
handler: async () => {
try {
if (!sections || Object.keys(sections).length === 0) {
return {
success: false,
message: 'No content available for analysis. Please generate content first.',
suggestion: 'Generate content for your blog sections before running quality analysis.'
};
}
// Prepare sections data for analysis
const sectionsData = Object.entries(sections).map(([id, content]: [string, any]) => {
const outlineSection = outline.find(s => s.id === id);
return {
id,
heading: outlineSection?.heading || 'Untitled Section',
content: typeof content === 'string' ? content : (content?.content || '')
};
});
if (sectionsData.length === 0) {
return {
success: false,
message: 'No valid sections found for analysis.',
suggestion: 'Ensure your blog has generated content before running analysis.'
};
}
// Call basic flow analysis API
const result = await blogWriterApi.analyzeFlowBasic({
title: blogTitle || 'Untitled Blog',
sections: sectionsData
});
if (result.success && result.analysis) {
// Notify parent component of analysis completion
onFlowAnalysisComplete?.(result.analysis);
const analysis = result.analysis;
const overallFlow = Math.round(analysis.overall_flow_score * 100);
const overallConsistency = Math.round(analysis.overall_consistency_score * 100);
const overallProgression = Math.round(analysis.overall_progression_score * 100);
return {
success: true,
message: `Content quality analysis completed! Your blog has an overall flow score of ${overallFlow}%, consistency of ${overallConsistency}%, and progression of ${overallProgression}%.`,
analysis: {
overall_scores: {
flow: overallFlow,
consistency: overallConsistency,
progression: overallProgression
},
sections: analysis.sections.map((s: any) => ({
heading: s.heading,
flow: Math.round(s.flow_score * 100),
consistency: Math.round(s.consistency_score * 100),
progression: Math.round(s.progression_score * 100),
suggestions: s.suggestions
})),
overall_suggestions: analysis.overall_suggestions
},
next_step_suggestion: 'Use "🔍 Deep Content Analysis" for detailed, section-by-section analysis with more specific recommendations.'
};
} else {
return {
success: false,
message: 'Content quality analysis failed.',
error: result.error || 'Unknown error occurred',
suggestion: 'Please try again or check if your content is properly generated.'
};
}
} catch (error) {
console.error('Content quality analysis error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
message: `Failed to analyze content quality: ${errorMessage}`,
suggestion: 'Please try again or ensure your content is properly generated.'
};
}
}
});
useCopilotActionTyped({
name: 'analyzeContentQualityAdvanced',
description: 'Get detailed, section-by-section analysis of content quality and flow (advanced analysis)',
parameters: [],
handler: async () => {
try {
if (!sections || Object.keys(sections).length === 0) {
return {
success: false,
message: 'No content available for advanced analysis. Please generate content first.',
suggestion: 'Generate content for your blog sections before running advanced analysis.'
};
}
// Prepare sections data for analysis
const sectionsData = Object.entries(sections).map(([id, content]: [string, any]) => {
const outlineSection = outline.find(s => s.id === id);
return {
id,
heading: outlineSection?.heading || 'Untitled Section',
content: typeof content === 'string' ? content : (content?.content || '')
};
});
if (sectionsData.length === 0) {
return {
success: false,
message: 'No valid sections found for advanced analysis.',
suggestion: 'Ensure your blog has generated content before running analysis.'
};
}
// Call advanced flow analysis API
const result = await blogWriterApi.analyzeFlowAdvanced({
title: blogTitle || 'Untitled Blog',
sections: sectionsData
});
if (result.success && result.analysis) {
// Notify parent component of analysis completion
onFlowAnalysisComplete?.(result.analysis);
const analysis = result.analysis;
const overallFlow = Math.round(analysis.overall_flow_score * 100);
const overallConsistency = Math.round(analysis.overall_consistency_score * 100);
const overallProgression = Math.round(analysis.overall_progression_score * 100);
return {
success: true,
message: `Advanced content analysis completed! Your blog has an overall flow score of ${overallFlow}%, consistency of ${overallConsistency}%, and progression of ${overallProgression}%.`,
analysis: {
overall_scores: {
flow: overallFlow,
consistency: overallConsistency,
progression: overallProgression
},
sections: analysis.sections.map((s: any) => ({
heading: s.heading,
flow: Math.round(s.flow_score * 100),
consistency: Math.round(s.consistency_score * 100),
progression: Math.round(s.progression_score * 100),
detailed_analysis: s.detailed_analysis,
suggestions: s.suggestions
}))
},
next_step_suggestion: 'Review the detailed analysis and implement the suggested improvements to enhance your content quality.'
};
} else {
return {
success: false,
message: 'Advanced content analysis failed.',
error: result.error || 'Unknown error occurred',
suggestion: 'Please try again or check if your content is properly generated.'
};
}
} catch (error) {
console.error('Advanced content analysis error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
message: `Failed to perform advanced content analysis: ${errorMessage}`,
suggestion: 'Please try again or ensure your content is properly generated.'
};
}
}
});
return null; // This component only provides the copilot actions
};

View File

@@ -6,6 +6,7 @@ interface OutlineGeneratorProps {
research: BlogResearchResponse | null;
onTaskStart: (taskId: string) => void;
onPollingStart: (taskId: string) => void;
onModalShow?: () => void; // Callback to show progress modal immediately
}
const useCopilotActionTyped = useCopilotAction as any;
@@ -13,7 +14,8 @@ const useCopilotActionTyped = useCopilotAction as any;
export const OutlineGenerator: React.FC<OutlineGeneratorProps> = ({
research,
onTaskStart,
onPollingStart
onPollingStart,
onModalShow
}) => {
useCopilotActionTyped({
name: 'generateOutline',
@@ -23,8 +25,14 @@ export const OutlineGenerator: React.FC<OutlineGeneratorProps> = ({
if (!research) return { success: false, message: 'No research yet. Please research a topic first.' };
try {
// Show progress modal immediately when user clicks "Create outline"
onModalShow?.();
// Start async outline generation
const { task_id } = await blogWriterApi.startOutlineGeneration({ research });
// Start polling immediately after getting task_id
// This ensures we catch progress messages from the very beginning
onTaskStart(task_id);
onPollingStart(task_id);

View File

@@ -0,0 +1,381 @@
import React, { useState } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchResponse, BlogOutlineSection } from '../../services/blogWriterApi';
// Type assertion for CopilotKit action
const useCopilotActionTyped = useCopilotAction as any;
// Separate component to manage rewrite feedback form state
const RewriteFeedbackFormComponent: React.FC<{
prompt?: string;
onSubmit: (data: { feedback: string; tone?: string; audience?: string; focus?: string }) => void;
onCancel: () => void;
}> = ({ prompt, onSubmit, onCancel }) => {
const [feedback, setFeedback] = useState('');
const [tone, setTone] = useState('');
const [audience, setAudience] = useState('');
const [focus, setFocus] = useState('');
const hasValidInput = feedback.trim().length >= 10;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (hasValidInput) {
onSubmit({
feedback: feedback.trim(),
tone: tone.trim() || undefined,
audience: audience.trim() || undefined,
focus: focus.trim() || undefined
});
} else {
window.alert('Please provide detailed feedback about what you want to change (at least 10 characters).');
}
};
return (
<form
onSubmit={handleSubmit}
style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}
>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🔄 Let's Rewrite Your Blog
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{prompt || 'Please provide feedback about what you\'d like to change in your blog:'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
What do you want to change? *
</label>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="e.g., I want to focus more on practical applications, make the tone more casual, emphasize real-world examples, etc."
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box',
minHeight: '80px',
resize: 'vertical'
}}
autoFocus
autoComplete="off"
spellCheck="false"
/>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
{feedback.length}/10 characters minimum
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Desired Tone (optional)
</label>
<select
value={tone}
onChange={(e) => setTone(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
>
<option value="">Keep current tone</option>
<option value="professional">Professional</option>
<option value="casual">Casual</option>
<option value="authoritative">Authoritative</option>
<option value="conversational">Conversational</option>
<option value="humorous">Humorous</option>
<option value="empathetic">Empathetic</option>
<option value="academic">Academic</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Target Audience (optional)
</label>
<input
type="text"
value={audience}
onChange={(e) => setAudience(e.target.value)}
placeholder="e.g., beginners, professionals, students, general audience"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoComplete="off"
spellCheck="false"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Main Focus/Angle (optional)
</label>
<input
type="text"
value={focus}
onChange={(e) => setFocus(e.target.value)}
placeholder="e.g., practical applications, technical deep-dive, beginner-friendly, industry trends"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoComplete="off"
spellCheck="false"
/>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
type="submit"
disabled={!hasValidInput}
style={{
backgroundColor: hasValidInput ? '#1976d2' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: hasValidInput ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
🔄 Rewrite Blog {hasValidInput ? '(Enabled)' : '(Disabled)'}
</button>
<button
type="button"
onClick={onCancel}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</form>
);
};
interface RewriteFeedbackFormProps {
research: BlogResearchResponse;
outline: BlogOutlineSection[];
sections: Record<string, string>;
blogTitle: string;
onRewriteStarted?: (taskId: string) => void;
onRewriteTriggered?: () => void;
}
export const RewriteFeedbackForm: React.FC<RewriteFeedbackFormProps> = ({
research,
outline,
sections,
blogTitle,
onRewriteStarted,
onRewriteTriggered
}) => {
const [isCollectingFeedback, setIsCollectingFeedback] = useState(false);
// Rewrite Blog Action with HITL
useCopilotActionTyped({
name: 'rewriteBlog',
description: 'Rewrite the entire blog based on user feedback and preferences',
parameters: [
{ name: 'prompt', type: 'string', description: 'Prompt to show user', required: false }
],
renderAndWaitForResponse: ({ respond, args, status }: { respond?: (value: string) => void; args: { prompt?: string }; status: string }) => {
if (status === 'complete') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f8ff',
borderRadius: '8px',
border: '1px solid #1976d2'
}}>
<p style={{ margin: 0, color: '#1976d2', fontWeight: '500' }}>
Rewrite feedback received! Starting blog rewrite...
</p>
</div>
);
}
return (
<RewriteFeedbackFormComponent
prompt={args.prompt}
onSubmit={(formData) => {
onRewriteTriggered?.();
respond?.(JSON.stringify(formData));
}}
onCancel={() => respond?.('CANCEL')}
/>
);
}
});
// Process Rewrite Feedback Action
useCopilotActionTyped({
name: 'processRewriteFeedback',
description: 'Process the rewrite feedback and start the blog rewrite task',
parameters: [
{ name: 'formData', type: 'string', description: 'JSON string with feedback, tone, audience, and focus', required: true }
],
handler: async ({ formData }: { formData: string }) => {
try {
const data = JSON.parse(formData);
const { feedback, tone, audience, focus } = data;
if (!feedback || feedback.trim().length < 10) {
return {
success: false,
message: 'Please provide more detailed feedback about what you\'d like to change.',
suggestion: 'Be specific about what aspects of the blog you want to improve, change, or rewrite.'
};
}
// Prepare the rewrite request
const sectionsData = Object.entries(sections).map(([id, content]: [string, any]) => {
const outlineSection = outline.find(s => s.id === id);
return {
id,
heading: outlineSection?.heading || 'Untitled Section',
content: typeof content === 'string' ? content : (content?.content || '')
};
});
if (sectionsData.length === 0) {
return {
success: false,
message: 'No content found to rewrite. Please generate content first.',
suggestion: 'Generate content for your blog before attempting to rewrite it.'
};
}
// Call the rewrite API
const result = await blogWriterApi.rewriteBlog({
title: blogTitle,
sections: sectionsData,
research: research,
outline: outline,
feedback: feedback.trim(),
tone: tone?.trim() || undefined,
audience: audience?.trim() || undefined,
focus: focus?.trim() || undefined
});
if (result.success && result.taskId) {
onRewriteStarted?.(result.taskId);
setIsCollectingFeedback(false);
return {
success: true,
message: `Blog rewrite initiated successfully! Your feedback has been processed and the rewrite is in progress.`,
taskId: result.taskId,
feedback: {
original: feedback,
tone: tone || 'Maintain current tone',
audience: audience || 'Keep current audience',
focus: focus || 'Maintain current focus'
},
nextStep: 'The rewrite process will take a few moments. You\'ll be notified when it\'s complete.'
};
} else {
return {
success: false,
message: 'Failed to initiate blog rewrite.',
error: result.error || 'Unknown error occurred',
suggestion: 'Please try again or check if your content is properly generated.'
};
}
} catch (error) {
console.error('Collect rewrite feedback error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
message: `Failed to process rewrite feedback: ${errorMessage}`,
suggestion: 'Please try again or provide more specific feedback about what you\'d like to change.'
};
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #1976d2',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#1976d2' }}>🔄 Rewriting Your Blog</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing your feedback and preferences...</p>
<p style={{ margin: '0 0 8px 0' }}> Processing current content structure...</p>
<p style={{ margin: '0 0 8px 0' }}> Generating improved content with new approach...</p>
<p style={{ margin: '0' }}> Applying tone and audience adjustments...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
return null; // This component doesn't render anything, it just provides actions
};
export default RewriteFeedbackForm;

View File

@@ -0,0 +1,240 @@
/**
* Keyword Analysis Component
*
* Displays comprehensive keyword analysis including keyword types, densities,
* missing keywords, over-optimization, and distribution analysis.
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
Chip,
IconButton,
Tooltip
} from '@mui/material';
import {
GpsFixed,
Search,
Warning
} from '@mui/icons-material';
interface KeywordAnalysisProps {
detailedAnalysis?: {
keyword_analysis?: {
primary_keywords: string[];
long_tail_keywords: string[];
semantic_keywords: string[];
keyword_density: Record<string, number>;
keyword_distribution: Record<string, any>;
missing_keywords: string[];
over_optimization: string[];
recommendations: string[];
};
};
}
export const KeywordAnalysis: React.FC<KeywordAnalysisProps> = ({ detailedAnalysis }) => {
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<GpsFixed sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Keyword Analysis
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Keyword Types Overview */}
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Keyword Types Found
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'success.main', mb: 1 }}>
Primary Keywords
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{detailedAnalysis?.keyword_analysis?.primary_keywords?.length || 0} found
</Typography>
{detailedAnalysis?.keyword_analysis?.primary_keywords?.slice(0, 3).map((keyword: string) => (
<Chip key={keyword} label={keyword} size="small" sx={{ mr: 0.5, mb: 0.5 }} />
))}
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'primary.main', mb: 1 }}>
Long-tail Keywords
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{detailedAnalysis?.keyword_analysis?.long_tail_keywords?.length || 0} found
</Typography>
{detailedAnalysis?.keyword_analysis?.long_tail_keywords?.slice(0, 2).map((keyword: string) => (
<Chip key={keyword} label={keyword} size="small" variant="outlined" sx={{ mr: 0.5, mb: 0.5 }} />
))}
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(156, 39, 176, 0.1)', borderRadius: 2, border: '1px solid rgba(156, 39, 176, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main', mb: 1 }}>
Semantic Keywords
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{detailedAnalysis?.keyword_analysis?.semantic_keywords?.length || 0} found
</Typography>
{detailedAnalysis?.keyword_analysis?.semantic_keywords?.slice(0, 2).map((keyword: string) => (
<Chip key={keyword} label={keyword} size="small" variant="outlined" color="secondary" sx={{ mr: 0.5, mb: 0.5 }} />
))}
</Box>
</Grid>
</Grid>
</Paper>
{/* Keyword Densities */}
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Keyword Densities
</Typography>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Keyword Density Analysis
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Shows how frequently each keyword appears in your content as a percentage of total words.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 1-3% for primary keywords
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Too Low (&lt;1%):</strong> Keyword may not be prominent enough
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Too High (&gt;3%):</strong> Risk of keyword stuffing
</Typography>
</Box>
}
arrow
>
<IconButton size="small" sx={{ color: 'primary.main' }}>
<Search />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{detailedAnalysis?.keyword_analysis?.keyword_density && Object.keys(detailedAnalysis.keyword_analysis.keyword_density).length > 0 ? (
Object.entries(detailedAnalysis.keyword_analysis.keyword_density).map(([keyword, density]) => (
<Box key={keyword} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 1, borderRadius: 1, background: 'rgba(0,0,0,0.02)' }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>{keyword}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{density > 3 ? 'Over-optimized' : density < 1 ? 'Under-optimized' : 'Optimal'}
</Typography>
<Chip
label={`${density.toFixed(1)}%`}
color={density > 3 ? 'error' : density < 1 ? 'warning' : 'success'}
size="small"
/>
</Box>
</Box>
))
) : (
<Typography variant="body2" sx={{ color: 'text.secondary', fontStyle: 'italic' }}>
No keyword density data available. Make sure your research data includes target keywords.
</Typography>
)}
</Box>
</Paper>
{/* Missing Keywords */}
{detailedAnalysis?.keyword_analysis?.missing_keywords && detailedAnalysis.keyword_analysis.missing_keywords.length > 0 && (
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: 'error.main' }}>
Missing Keywords
</Typography>
<Tooltip
title="Keywords from your research that are not found in the content. Consider adding these to improve SEO."
arrow
>
<IconButton size="small" sx={{ color: 'error.main' }}>
<Warning />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{detailedAnalysis.keyword_analysis.missing_keywords.map((keyword: string) => (
<Chip key={keyword} label={keyword} color="error" variant="outlined" />
))}
</Box>
</Paper>
)}
{/* Over-Optimized Keywords */}
{detailedAnalysis?.keyword_analysis?.over_optimization && detailedAnalysis.keyword_analysis.over_optimization.length > 0 && (
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, color: 'warning.main' }}>
Over-Optimized Keywords
</Typography>
<Tooltip
title="Keywords that appear too frequently (over 3% density). Consider reducing their usage to avoid keyword stuffing penalties."
arrow
>
<IconButton size="small" sx={{ color: 'warning.main' }}>
<Warning />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{detailedAnalysis.keyword_analysis.over_optimization.map((keyword: string) => (
<Chip key={keyword} label={keyword} color="warning" variant="outlined" />
))}
</Box>
</Paper>
)}
{/* Keyword Distribution Analysis */}
{detailedAnalysis?.keyword_analysis?.keyword_distribution && Object.keys(detailedAnalysis.keyword_analysis.keyword_distribution).length > 0 && (
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Keyword Distribution Analysis
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(detailedAnalysis.keyword_analysis.keyword_distribution).map(([keyword, data]: [string, any]) => (
<Box key={keyword} sx={{ p: 2, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
"{keyword}"
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Density: {data.density?.toFixed(1)}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
In Headings: {data.in_headings ? 'Yes' : 'No'}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
First Occurrence: Character {data.first_occurrence || 'Not found'}
</Typography>
</Grid>
</Grid>
</Box>
))}
</Box>
</Paper>
)}
</Box>
</Box>
);
};

View File

@@ -0,0 +1,103 @@
# SEO Components
This folder contains extracted SEO analysis components that were refactored from the main `SEOAnalysisModal` component to improve maintainability and code organization.
## Components
### KeywordAnalysis
- **File**: `KeywordAnalysis.tsx`
- **Purpose**: Displays comprehensive keyword analysis including:
- Keyword types overview (primary, long-tail, semantic)
- Keyword density analysis with optimal range indicators
- Missing keywords detection
- Over-optimized keywords detection
- Keyword distribution analysis
### ReadabilityAnalysis
- **File**: `ReadabilityAnalysis.tsx`
- **Purpose**: Displays comprehensive readability analysis including:
- 6 different readability metrics with tooltips
- Content statistics (word count, sections, paragraphs, etc.)
- Sentence and paragraph analysis
- Target audience determination
- Content quality metrics
### StructureAnalysis
- **File**: `StructureAnalysis.tsx`
- **Purpose**: Displays comprehensive content structure analysis including:
- Structure overview (sections, paragraphs, sentences, structure score)
- Content elements detection (introduction, conclusion, call-to-action)
- Heading structure analysis (H1, H2, H3 counts and actual headings)
- Heading hierarchy score
### Recommendations
- **File**: `Recommendations.tsx`
- **Purpose**: Displays actionable SEO recommendations including:
- Priority-based recommendation cards (High, Medium, Low)
- Category tags for each recommendation
- Impact descriptions
- Visual priority indicators with icons
### SEOProcessor
- **File**: `SEOProcessor.tsx`
- **Purpose**: Provides CopilotKit actions for SEO functionality including:
- `generateSEOMetadata` - Generate SEO metadata for blog content
- `optimizeSection` - Optimize individual sections for SEO
- Interactive UI components for user feedback
## Refactoring Benefits
1. **Improved Maintainability**: Each component is focused on a single responsibility
2. **Better Code Organization**: Related functionality is grouped together
3. **Easier Testing**: Individual components can be tested in isolation
4. **Reusability**: Components can be reused in other parts of the application
5. **Reduced File Size**: Main modal component reduced by ~600+ lines
6. **Modular Architecture**: Clean separation of concerns
## Usage
```tsx
import {
KeywordAnalysis,
ReadabilityAnalysis,
StructureAnalysis,
Recommendations,
SEOProcessor
} from './SEO';
// In your component
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
<ReadabilityAnalysis
detailedAnalysis={analysisResult.detailed_analysis}
visualizationData={analysisResult.visualization_data}
/>
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
<Recommendations recommendations={analysisResult.actionable_recommendations} />
<SEOProcessor
buildFullMarkdown={buildFullMarkdown}
seoMetadata={seoMetadata}
onSEOAnalysis={onSEOAnalysis}
onSEOMetadata={onSEOMetadata}
/>
```
## Props
### KeywordAnalysis Props
- `detailedAnalysis?: { keyword_analysis?: {...} }` - Detailed analysis data from backend
### ReadabilityAnalysis Props
- `detailedAnalysis?: { readability_analysis?: {...}, content_quality?: {...}, content_structure?: {...} }` - Detailed analysis data
- `visualizationData?: { content_stats?: {...} }` - Visualization data for fallback values
### StructureAnalysis Props
- `detailedAnalysis?: { content_structure?: {...}, heading_structure?: {...} }` - Detailed analysis data
### Recommendations Props
- `recommendations: Recommendation[]` - Array of actionable recommendations with priority and impact
### SEOProcessor Props
- `buildFullMarkdown: () => string` - Function to build full markdown content
- `seoMetadata: BlogSEOMetadataResponse | null` - Current SEO metadata
- `onSEOAnalysis: (analysis: any) => void` - Callback for SEO analysis results
- `onSEOMetadata: (metadata: BlogSEOMetadataResponse) => void` - Callback for SEO metadata results

View File

@@ -0,0 +1,286 @@
/**
* Readability Analysis Component
*
* Displays comprehensive readability analysis including readability metrics,
* content statistics, sentence/paragraph analysis, and target audience information.
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
IconButton,
Tooltip
} from '@mui/material';
import {
MenuBook
} from '@mui/icons-material';
interface ReadabilityAnalysisProps {
detailedAnalysis?: {
readability_analysis?: {
metrics: Record<string, number>;
avg_sentence_length: number;
avg_paragraph_length: number;
readability_score: number;
target_audience: string;
recommendations: string[];
};
content_quality?: {
word_count: number;
unique_words: number;
vocabulary_diversity: number;
transition_words_used: number;
content_depth_score: number;
flow_score: number;
recommendations: string[];
};
content_structure?: {
total_sections: number;
total_paragraphs: number;
total_sentences: number;
has_introduction: boolean;
has_conclusion: boolean;
has_call_to_action: boolean;
structure_score: number;
recommendations: string[];
};
};
visualizationData?: {
content_stats: {
word_count: number;
sections: number;
paragraphs: number;
};
};
}
export const ReadabilityAnalysis: React.FC<ReadabilityAnalysisProps> = ({
detailedAnalysis,
visualizationData
}) => {
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<MenuBook sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Readability Analysis
</Typography>
</Box>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Readability Metrics
</Typography>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Readability Analysis
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Measures how easy your content is to read and understand.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Flesch Reading Ease:</strong> 90-100 (Very Easy), 80-89 (Easy), 70-79 (Fairly Easy), 60-69 (Standard)
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Average Sentence Length:</strong> 15-20 words is optimal
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Average Syllables per Word:</strong> 1.5-1.7 is ideal
</Typography>
</Box>
}
arrow
>
<IconButton size="small" sx={{ color: 'primary.main' }}>
<MenuBook />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{detailedAnalysis?.readability_analysis?.metrics && Object.keys(detailedAnalysis.readability_analysis.metrics).length > 0 ? (
Object.entries(detailedAnalysis.readability_analysis.metrics).map(([metric, value]) => {
const getReadabilityTooltip = (metric: string, value: number) => {
const tooltips = {
flesch_reading_ease: {
description: "Measures how easy text is to read (0-100 scale)",
interpretation: value >= 80 ? "Very Easy" : value >= 60 ? "Standard" : "Difficult"
},
flesch_kincaid_grade: {
description: "U.S. grade level needed to understand the text",
interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult"
},
gunning_fog: {
description: "Years of formal education needed to understand the text",
interpretation: value <= 12 ? "Easy" : value <= 16 ? "Moderate" : "Difficult"
},
smog_index: {
description: "Simple Measure of Gobbledygook - readability formula",
interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult"
},
automated_readability: {
description: "Automated Readability Index based on character count",
interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult"
},
coleman_liau: {
description: "Readability test based on average sentence length and characters per word",
interpretation: value <= 8 ? "Easy" : value <= 12 ? "Moderate" : "Difficult"
}
};
return tooltips[metric as keyof typeof tooltips] || { description: "Readability metric", interpretation: "N/A" };
};
const tooltip = getReadabilityTooltip(metric, value);
return (
<Tooltip
key={metric}
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
{metric.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{tooltip.description}
</Typography>
<Typography variant="caption">
<strong>Interpretation:</strong> {tooltip.interpretation}
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 1, borderRadius: 1, background: 'rgba(0,0,0,0.02)', cursor: 'help' }}>
<Typography variant="body2" sx={{ textTransform: 'capitalize' }}>
{metric.replace('_', ' ')}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{value.toFixed(1)}
</Typography>
</Box>
</Tooltip>
);
})
) : (
<Typography variant="body2" sx={{ color: 'text.secondary', fontStyle: 'italic' }}>
No readability metrics available. This may indicate an issue with the content analysis.
</Typography>
)}
</Box>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Content Statistics
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Word Count</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.word_count || visualizationData?.content_stats.word_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || visualizationData?.content_stats.sections}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || visualizationData?.content_stats.paragraphs}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Unique Words</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.unique_words || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Vocabulary Diversity</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.vocabulary_diversity ?
(detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'}
</Typography>
</Box>
</Box>
</Paper>
</Grid>
</Grid>
{/* Additional Readability Metrics */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Sentence & Paragraph Analysis
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Avg Sentence Length</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.readability_analysis?.avg_sentence_length?.toFixed(1) || 'N/A'} words
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Avg Paragraph Length</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.readability_analysis?.avg_paragraph_length?.toFixed(1) || 'N/A'} words
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Transition Words</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.transition_words_used || 'N/A'}
</Typography>
</Box>
</Box>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Target Audience
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Reading Level</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.readability_analysis?.target_audience || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Content Depth Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.content_depth_score || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Flow Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_quality?.flow_score || 'N/A'}
</Typography>
</Box>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -0,0 +1,108 @@
/**
* Recommendations Component
*
* Displays actionable SEO recommendations with priority indicators,
* category tags, and impact descriptions.
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Chip
} from '@mui/material';
import {
Lightbulb,
CheckCircle,
Cancel,
Warning
} from '@mui/icons-material';
interface Recommendation {
category: string;
priority: 'High' | 'Medium' | 'Low';
recommendation: string;
impact: string;
}
interface RecommendationsProps {
recommendations: Recommendation[];
}
export const Recommendations: React.FC<RecommendationsProps> = ({ recommendations }) => {
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'High': return 'error.main';
case 'Medium': return 'warning.main';
case 'Low': return 'success.main';
default: return 'text.secondary';
}
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'High': return <Cancel sx={{ fontSize: 16 }} />;
case 'Medium': return <Warning sx={{ fontSize: 16 }} />;
case 'Low': return <CheckCircle sx={{ fontSize: 16 }} />;
default: return <Warning sx={{ fontSize: 16 }} />;
}
};
const getScoreBadgeVariant = (score: number) => {
if (score >= 80) return 'success';
if (score >= 60) return 'warning';
return 'error';
};
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Lightbulb sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Actionable Recommendations
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{recommendations.map((rec, index) => (
<Paper
key={index}
sx={{
p: 3,
border: '1px solid rgba(255,255,255,0.1)',
background: 'rgba(255,255,255,0.03)',
borderRadius: 2
}}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Box sx={{ color: getPriorityColor(rec.priority), mt: 0.5 }}>
{getPriorityIcon(rec.priority)}
</Box>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Chip
label={rec.category}
variant="outlined"
size="small"
sx={{ borderColor: 'rgba(255,255,255,0.3)' }}
/>
<Chip
label={rec.priority}
color={getScoreBadgeVariant(rec.priority === 'High' ? 30 : 70)}
size="small"
/>
</Box>
<Typography variant="body2" sx={{ mb: 1 }}>
{rec.recommendation}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{rec.impact}
</Typography>
</Box>
</Box>
</Paper>
))}
</Box>
</Box>
);
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogSEOMetadataResponse } from '../../services/blogWriterApi';
import { blogWriterApi, BlogSEOMetadataResponse } from '../../../services/blogWriterApi';
interface SEOProcessorProps {
buildFullMarkdown: () => string;
@@ -17,22 +17,7 @@ export const SEOProcessor: React.FC<SEOProcessorProps> = ({
onSEOAnalysis,
onSEOMetadata
}) => {
useCopilotActionTyped({
name: 'runSEOAnalyze',
description: 'Analyze SEO for the full draft',
parameters: [ { name: 'keywords', type: 'string', description: 'Comma-separated keywords', required: false } ],
handler: async ({ keywords }: { keywords?: string }) => {
const content = buildFullMarkdown();
const res = await blogWriterApi.seoAnalyze({ content, keywords: keywords ? keywords.split(',').map(k => k.trim()) : [] });
onSEOAnalysis(res);
return { success: true, seo_score: res.seo_score };
},
render: ({ status, result }: any) => status === 'complete' ? (
<div style={{ padding: 12 }}>
<div>SEO Score: {result?.seo_score ?? '—'}</div>
</div>
) : null
});
// Removed old runSEOAnalyze action - now using runComprehensiveSEOAnalysis in BlogWriter.tsx
useCopilotActionTyped({
name: 'generateSEOMetadata',
@@ -63,7 +48,24 @@ export const SEOProcessor: React.FC<SEOProcessorProps> = ({
handler: async ({ sectionId, goals }: { sectionId: string; goals?: string }) => {
const current = buildFullMarkdown();
if (!current) return { success: false, message: 'No content yet for this section' };
const res = await blogWriterApi.seoAnalyze({ content: current, keywords: [] });
// Use comprehensive SEO analysis endpoint
const response = await fetch('/api/blog-writer/seo/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: current,
keywords: []
})
});
if (!response.ok) {
throw new Error('Failed to analyze blog content');
}
const res = await response.json();
onSEOAnalysis(res);
return { success: true, message: 'Analysis ready' };
},

View File

@@ -0,0 +1,196 @@
/**
* Structure Analysis Component
*
* Displays comprehensive content structure analysis including structure overview,
* content elements detection, and heading structure analysis.
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
Chip
} from '@mui/material';
import {
BarChart
} from '@mui/icons-material';
interface StructureAnalysisProps {
detailedAnalysis?: {
content_structure?: {
total_sections: number;
total_paragraphs: number;
total_sentences: number;
has_introduction: boolean;
has_conclusion: boolean;
has_call_to_action: boolean;
structure_score: number;
recommendations: string[];
};
heading_structure?: {
h1_count: number;
h2_count: number;
h3_count: number;
h1_headings: string[];
h2_headings: string[];
h3_headings: string[];
heading_hierarchy_score: number;
recommendations: string[];
};
};
}
export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAnalysis }) => {
// Debug logging
console.log('🏗️ StructureAnalysis received data:', detailedAnalysis);
console.log('📊 Content Structure:', detailedAnalysis?.content_structure);
console.log('📋 Heading Structure:', detailedAnalysis?.heading_structure);
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<BarChart sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Content Structure Analysis
</Typography>
</Box>
<Grid container spacing={3}>
{/* Content Structure Overview */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Structure Overview
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Structure Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.structure_score || 'N/A'}
</Typography>
</Box>
</Box>
</Paper>
</Grid>
{/* Content Elements */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Content Elements
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Introduction</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_introduction ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_introduction ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Conclusion</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_conclusion ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_conclusion ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Call to Action</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_call_to_action ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_call_to_action ? 'success' : 'error'}
size="small"
/>
</Box>
</Box>
</Paper>
</Grid>
</Grid>
{/* Heading Structure */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Heading Structure Analysis
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'success.main', mb: 1 }}>
H1 Headings ({detailedAnalysis?.heading_structure?.h1_count || 0})
</Typography>
{detailedAnalysis?.heading_structure?.h1_headings?.map((heading: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block', mb: 0.5 }}>
{heading}
</Typography>
))}
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'primary.main', mb: 1 }}>
H2 Headings ({detailedAnalysis?.heading_structure?.h2_count || 0})
</Typography>
{detailedAnalysis?.heading_structure?.h2_headings?.slice(0, 3).map((heading: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block', mb: 0.5 }}>
{heading}
</Typography>
))}
{detailedAnalysis?.heading_structure?.h2_headings && detailedAnalysis.heading_structure.h2_headings.length > 3 && (
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
... and {detailedAnalysis.heading_structure.h2_headings.length - 3} more
</Typography>
)}
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, background: 'rgba(156, 39, 176, 0.1)', borderRadius: 2, border: '1px solid rgba(156, 39, 176, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main', mb: 1 }}>
H3 Headings ({detailedAnalysis?.heading_structure?.h3_count || 0})
</Typography>
{detailedAnalysis?.heading_structure?.h3_headings?.slice(0, 3).map((heading: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block', mb: 0.5 }}>
{heading}
</Typography>
))}
{detailedAnalysis?.heading_structure?.h3_headings && detailedAnalysis.heading_structure.h3_headings.length > 3 && (
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
... and {detailedAnalysis.heading_structure.h3_headings.length - 3} more
</Typography>
)}
</Box>
</Grid>
</Grid>
<Box sx={{ mt: 2, p: 2, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'}
</Typography>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -0,0 +1,11 @@
/**
* SEO Components Index
*
* Exports all SEO-related components for easy importing.
*/
export { KeywordAnalysis } from './KeywordAnalysis';
export { ReadabilityAnalysis } from './ReadabilityAnalysis';
export { StructureAnalysis } from './StructureAnalysis';
export { Recommendations } from './Recommendations';
export { SEOProcessor } from './SEOProcessor';

View File

@@ -0,0 +1,710 @@
/**
* SEO Analysis Modal Component
*
* Displays comprehensive SEO analysis results with visual charts and actionable recommendations.
* Integrates with CopilotKit for real-time progress updates and user interactions.
*/
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogTitle,
Button,
Chip,
LinearProgress,
Card,
CardContent,
CardHeader,
Typography,
Box,
Tabs,
Tab,
Alert,
Grid,
Paper,
Divider,
IconButton,
Tooltip
} from '@mui/material';
import {
CheckCircle,
Cancel,
Warning,
TrendingUp,
GpsFixed,
MenuBook,
Search,
BarChart,
Lightbulb,
Refresh,
Close
} from '@mui/icons-material';
import { KeywordAnalysis, ReadabilityAnalysis, StructureAnalysis, Recommendations } from './SEO';
interface SEOAnalysisResult {
overall_score: number;
category_scores: {
structure: number;
keywords: number;
readability: number;
quality: number;
headings: number;
ai_insights: number;
};
analysis_summary: {
overall_grade: string;
status: string;
strongest_category: string;
weakest_category: string;
key_strengths: string[];
key_weaknesses: string[];
ai_summary: string;
};
actionable_recommendations: Array<{
category: string;
priority: 'High' | 'Medium' | 'Low';
recommendation: string;
impact: string;
}>;
visualization_data: {
score_radar: {
categories: string[];
scores: number[];
max_score: number;
};
keyword_analysis: {
densities: Record<string, number>;
missing_keywords: string[];
over_optimization: string[];
};
readability_metrics: Record<string, number>;
content_stats: {
word_count: number;
sections: number;
paragraphs: number;
};
};
detailed_analysis?: {
content_structure?: {
total_sections: number;
total_paragraphs: number;
total_sentences: number;
has_introduction: boolean;
has_conclusion: boolean;
has_call_to_action: boolean;
structure_score: number;
recommendations: string[];
};
keyword_analysis?: {
primary_keywords: string[];
long_tail_keywords: string[];
semantic_keywords: string[];
keyword_density: Record<string, number>;
keyword_distribution: Record<string, any>;
missing_keywords: string[];
over_optimization: string[];
recommendations: string[];
};
readability_analysis?: {
metrics: Record<string, number>;
avg_sentence_length: number;
avg_paragraph_length: number;
readability_score: number;
target_audience: string;
recommendations: string[];
};
content_quality?: {
word_count: number;
unique_words: number;
vocabulary_diversity: number;
transition_words_used: number;
content_depth_score: number;
flow_score: number;
recommendations: string[];
};
heading_structure?: {
h1_count: number;
h2_count: number;
h3_count: number;
h1_headings: string[];
h2_headings: string[];
h3_headings: string[];
heading_hierarchy_score: number;
recommendations: string[];
};
};
generated_at: string;
}
interface SEOAnalysisModalProps {
isOpen: boolean;
onClose: () => void;
blogContent: string;
researchData: any;
onApplyRecommendations?: (recommendations: any[]) => void;
}
export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
isOpen,
onClose,
blogContent,
researchData,
onApplyRecommendations
}) => {
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysisResult, setAnalysisResult] = useState<SEOAnalysisResult | null>(null);
const [progress, setProgress] = useState(0);
const [progressMessage, setProgressMessage] = useState('');
const [error, setError] = useState<string | null>(null);
const [tabValue, setTabValue] = useState('recommendations');
// Debug logging
console.log('SEOAnalysisModal render:', { isOpen, blogContent: blogContent?.length, researchData: !!researchData });
const runSEOAnalysis = async () => {
try {
setIsAnalyzing(true);
setError(null);
setProgress(0);
setProgressMessage('Starting SEO analysis...');
// Simulate progress updates (in real implementation, this would be SSE)
const progressStages = [
{ progress: 20, message: 'Extracting keywords from research data...' },
{ progress: 40, message: 'Analyzing content structure and readability...' },
{ progress: 70, message: 'Generating AI-powered insights...' },
{ progress: 90, message: 'Compiling analysis results...' },
{ progress: 100, message: 'SEO analysis completed!' }
];
for (const stage of progressStages) {
setProgress(stage.progress);
setProgressMessage(stage.message);
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Make API call to analyze blog content
const response = await fetch('/api/blog-writer/seo/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
blog_content: blogContent,
research_data: researchData
})
});
if (!response.ok) {
throw new Error('Failed to analyze blog content');
}
const result = await response.json();
console.log('🔍 Backend SEO Analysis Response:', result);
console.log('📊 Category Scores:', result.category_scores);
console.log('💡 Recommendations:', result.actionable_recommendations);
console.log('🔍 Visualization Data:', result.visualization_data);
console.log('📝 Detailed Analysis:', result.detailed_analysis);
console.log('🏗️ Content Structure:', result.detailed_analysis?.content_structure);
console.log('📋 Heading Structure:', result.detailed_analysis?.heading_structure);
// Convert API response to frontend format - fail fast if data is missing
if (!result.success) {
throw new Error(result.recommendations?.[0] || 'SEO analysis failed');
}
if (!result.overall_score && result.overall_score !== 0) {
throw new Error('Invalid SEO score received from API');
}
const convertedResult: SEOAnalysisResult = {
overall_score: result.overall_score,
category_scores: {
structure: result.category_scores?.structure || 0,
keywords: result.category_scores?.keywords || 0,
readability: result.category_scores?.readability || 0,
quality: result.category_scores?.quality || 0,
headings: result.category_scores?.headings || 0,
ai_insights: result.category_scores?.ai_insights || 0
},
analysis_summary: result.analysis_summary || {
overall_grade: result.overall_score >= 80 ? 'A' : result.overall_score >= 60 ? 'B' : 'C',
status: result.overall_score >= 80 ? 'Excellent' : result.overall_score >= 60 ? 'Good' : 'Needs Improvement',
strongest_category: 'structure',
weakest_category: 'keywords',
key_strengths: ['Good content structure', 'Appropriate length'],
key_weaknesses: ['Keyword optimization needs work'],
ai_summary: 'Content provides good value with room for SEO improvements.'
},
actionable_recommendations: (result.actionable_recommendations || []).map((rec: any) => ({
category: rec.category || 'General',
priority: rec.priority || 'Medium' as const,
recommendation: rec.recommendation || rec,
impact: rec.impact || 'Improves SEO performance'
})),
visualization_data: {
score_radar: {
categories: ['structure', 'keywords', 'readability', 'quality', 'headings', 'ai_insights'],
scores: [
result.category_scores?.structure || 0,
result.category_scores?.keywords || 0,
result.category_scores?.readability || 0,
result.category_scores?.quality || 0,
result.category_scores?.headings || 0,
result.category_scores?.ai_insights || 0
],
max_score: 100
},
keyword_analysis: {
densities: result.visualization_data?.keyword_analysis?.densities || {},
missing_keywords: result.visualization_data?.keyword_analysis?.missing_keywords || [],
over_optimization: result.visualization_data?.keyword_analysis?.over_optimization || []
},
readability_metrics: result.visualization_data?.readability_metrics || {},
content_stats: {
word_count: result.visualization_data?.content_stats?.word_count || 0,
sections: result.visualization_data?.content_stats?.sections || 0,
paragraphs: result.visualization_data?.content_stats?.paragraphs || 0
}
},
detailed_analysis: result.detailed_analysis || undefined,
generated_at: new Date().toISOString()
};
setAnalysisResult(convertedResult);
setIsAnalyzing(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Analysis failed');
setIsAnalyzing(false);
}
};
const getScoreColor = (score: number) => {
if (score >= 80) return 'success.main';
if (score >= 60) return 'warning.main';
return 'error.main';
};
const getScoreBadgeVariant = (score: number) => {
if (score >= 80) return 'success';
if (score >= 60) return 'warning';
return 'error';
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'High': return 'error.main';
case 'Medium': return 'warning.main';
case 'Low': return 'success.main';
default: return 'text.secondary';
}
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'High': return <Cancel sx={{ fontSize: 16 }} />;
case 'Medium': return <Warning sx={{ fontSize: 16 }} />;
case 'Low': return <CheckCircle sx={{ fontSize: 16 }} />;
default: return <Warning sx={{ fontSize: 16 }} />;
}
};
// Tooltip content for each metric
const getMetricTooltip = (category: string) => {
const tooltips = {
structure: {
title: "Content Structure Analysis",
description: "Evaluates how well your content is organized and structured for both readers and search engines.",
methodology: "Analyzes heading hierarchy (H1, H2, H3), paragraph length, section organization, and logical flow.",
score_meaning: "Higher scores indicate better content organization, clear headings, and logical structure.",
examples: "Good: Clear H1 title, logical H2 sections, short paragraphs. Poor: No headings, long walls of text."
},
keywords: {
title: "Keyword Optimization Analysis",
description: "Measures how effectively your target keywords are used throughout the content.",
methodology: "Analyzes keyword density, distribution, placement in headings, and semantic keyword usage.",
score_meaning: "Higher scores indicate optimal keyword usage without over-optimization.",
examples: "Good: 1-3% keyword density, keywords in headings. Poor: Keyword stuffing or missing target keywords."
},
readability: {
title: "Readability Assessment",
description: "Evaluates how easy your content is to read and understand for your target audience.",
methodology: "Uses Flesch Reading Ease, sentence length, word complexity, and paragraph structure.",
score_meaning: "Higher scores indicate content that's easier to read and understand.",
examples: "Good: Short sentences, simple words, clear paragraphs. Poor: Long complex sentences, jargon."
},
quality: {
title: "Content Quality Evaluation",
description: "Assesses the depth, value, and comprehensiveness of your content.",
methodology: "Analyzes word count, content depth, information density, and topic coverage.",
score_meaning: "Higher scores indicate more comprehensive and valuable content.",
examples: "Good: Detailed explanations, examples, comprehensive coverage. Poor: Thin content, lack of detail."
},
headings: {
title: "Heading Structure Analysis",
description: "Evaluates the effectiveness of your heading hierarchy and organization.",
methodology: "Analyzes heading distribution, hierarchy levels, keyword usage in headings, and logical flow.",
score_meaning: "Higher scores indicate better heading structure and organization.",
examples: "Good: Clear H1, logical H2/H3 progression. Poor: Missing headings, poor hierarchy."
},
ai_insights: {
title: "AI-Powered Content Insights",
description: "Advanced analysis of content engagement potential and user value.",
methodology: "Uses AI to analyze content quality, engagement factors, and user value proposition.",
score_meaning: "Higher scores indicate content that's more likely to engage and provide value to readers.",
examples: "Good: Clear value proposition, engaging content, actionable insights. Poor: Generic content, low engagement potential."
}
};
return tooltips[category as keyof typeof tooltips] || tooltips.structure;
};
useEffect(() => {
if (isOpen && !analysisResult) {
runSEOAnalysis();
}
}, [isOpen]);
return (
<Dialog
open={isOpen}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
maxHeight: '90vh',
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(0,0,0,0.1)',
color: 'text.primary'
}
}}
>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ p: 3, borderBottom: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Search sx={{ color: 'primary.main' }} />
<Typography variant="h5" component="h2" sx={{ fontWeight: 600 }}>
SEO Analysis Results
</Typography>
</Box>
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}>
<Close />
</IconButton>
</Box>
<Typography variant="body2" sx={{ color: 'text.secondary', mt: 1 }}>
Comprehensive analysis of your blog content's SEO optimization
</Typography>
</Box>
{isAnalyzing && (
<Box sx={{ p: 3 }}>
<Box sx={{ textAlign: 'center', mb: 3 }}>
<Refresh sx={{
fontSize: 32,
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' }
}
}} />
<Typography variant="body2" sx={{ color: 'text.secondary', mt: 1 }}>
{progressMessage}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #4caf50, #8bc34a)'
}
}}
/>
</Box>
)}
{error && (
<Box sx={{ p: 3 }}>
<Alert severity="error" sx={{ borderRadius: 2 }}>
<Cancel sx={{ mr: 1 }} />
{error}
</Alert>
</Box>
)}
{analysisResult && (
<Box sx={{ p: 3 }}>
{/* Overall Score Section */}
<Card sx={{ mb: 3, background: 'rgba(255,255,255,0.9)', border: '1px solid rgba(0,0,0,0.1)' }}>
<CardHeader>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<BarChart sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Overall SEO Score
</Typography>
</Box>
</CardHeader>
<CardContent>
<Grid container spacing={3} alignItems="center">
<Grid item xs={4}>
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h2"
sx={{
fontWeight: 'bold',
color: getScoreColor(analysisResult.overall_score),
background: 'linear-gradient(45deg, #4caf50, #8bc34a)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
}}
>
{analysisResult.overall_score}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Overall Score
</Typography>
</Box>
</Grid>
<Grid item xs={4}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h3" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{analysisResult.analysis_summary.overall_grade}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Grade
</Typography>
</Box>
</Grid>
<Grid item xs={4}>
<Box sx={{ textAlign: 'center' }}>
<Chip
label={analysisResult.analysis_summary.status}
color={getScoreBadgeVariant(analysisResult.overall_score)}
variant="filled"
sx={{
fontWeight: 600,
fontSize: '0.9rem',
px: 2,
py: 1
}}
/>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Category Scores */}
<Card sx={{ mb: 3, background: 'rgba(255,255,255,0.9)', border: '1px solid rgba(0,0,0,0.1)' }}>
<CardHeader>
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
Category Breakdown
</Typography>
</CardHeader>
<CardContent>
<Grid container spacing={2}>
{Object.entries(analysisResult.category_scores).map(([category, score]) => {
const tooltip = getMetricTooltip(category);
return (
<Grid item xs={6} md={4} key={category}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
{tooltip.title}
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{tooltip.description}
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1, fontStyle: 'italic' }}>
<strong>Methodology:</strong> {tooltip.methodology}
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Score Meaning:</strong> {tooltip.score_meaning}
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Examples:</strong> {tooltip.examples}
</Typography>
</Box>
}
arrow
placement="top"
>
<Paper
sx={{
p: 2,
textAlign: 'center',
background: 'rgba(255,255,255,0.8)',
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 2,
cursor: 'help',
'&:hover': {
background: 'rgba(255,255,255,0.9)',
transform: 'translateY(-2px)',
transition: 'all 0.2s ease-in-out'
}
}}
>
<Typography
variant="h4"
sx={{
fontWeight: 'bold',
color: getScoreColor(score),
mb: 1
}}
>
{score}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', textTransform: 'capitalize' }}>
{category.replace('_', ' ')}
</Typography>
</Paper>
</Tooltip>
</Grid>
);
})}
</Grid>
</CardContent>
</Card>
{/* Detailed Analysis Tabs */}
<Card sx={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)' }}>
<Tabs
value={tabValue}
onChange={(e, newValue) => setTabValue(newValue)}
variant="fullWidth"
sx={{
'& .MuiTab-root': {
color: 'text.secondary',
fontWeight: 500,
'&.Mui-selected': {
color: 'primary.main',
fontWeight: 600
}
},
'& .MuiTabs-indicator': {
background: 'linear-gradient(90deg, #4caf50, #8bc34a)',
height: 3
}
}}
>
<Tab label="Recommendations" value="recommendations" />
<Tab label="Keywords" value="keywords" />
<Tab label="Readability" value="readability" />
<Tab label="Structure" value="structure" />
<Tab label="AI Insights" value="insights" />
</Tabs>
<Box sx={{ p: 3 }}>
{tabValue === 'recommendations' && (
<Recommendations recommendations={analysisResult.actionable_recommendations} />
)}
{tabValue === 'keywords' && (
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'readability' && (
<ReadabilityAnalysis
detailedAnalysis={analysisResult.detailed_analysis}
visualizationData={analysisResult.visualization_data}
/>
)}
{tabValue === 'structure' && (
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'insights' && (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<TrendingUp sx={{ color: 'primary.main' }} />
<Typography variant="h6" component="h3" sx={{ fontWeight: 600 }}>
AI-Powered Insights
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Content Summary
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{analysisResult.analysis_summary.ai_summary}
</Typography>
</Paper>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Key Strengths
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{analysisResult.analysis_summary.key_strengths.map((strength, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="body2">{strength}</Typography>
</Box>
))}
</Box>
</Paper>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Areas for Improvement
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{analysisResult.analysis_summary.key_weaknesses.map((weakness, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ color: 'warning.main', fontSize: 16 }} />
<Typography variant="body2">{weakness}</Typography>
</Box>
))}
</Box>
</Paper>
</Box>
</Box>
)}
</Box>
</Card>
{/* Action Buttons */}
<Box sx={{ p: 3, borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button variant="outlined" onClick={onClose} sx={{ color: 'text.secondary' }}>
Close
</Button>
<Button
variant="contained"
onClick={() => {
if (onApplyRecommendations) {
onApplyRecommendations(analysisResult.actionable_recommendations);
}
onClose();
}}
disabled={!onApplyRecommendations}
sx={{
background: 'linear-gradient(45deg, #4caf50, #8bc34a)',
'&:hover': {
background: 'linear-gradient(45deg, #45a049, #7cb342)'
}
}}
>
Apply Recommendations
</Button>
</Box>
</Box>
</Box>
)}
</DialogContent>
</Dialog>
);
};

View File

@@ -8,6 +8,9 @@ interface SuggestionsGeneratorProps {
researchPolling?: { isPolling: boolean; currentStatus: string };
outlinePolling?: { isPolling: boolean; currentStatus: string };
mediumPolling?: { isPolling: boolean; currentStatus: string };
hasContent?: boolean;
flowAnalysisCompleted?: boolean;
contentConfirmed?: boolean;
}
export const useSuggestions = (
@@ -16,7 +19,10 @@ export const useSuggestions = (
outlineConfirmed: boolean = false,
researchPolling?: { isPolling: boolean; currentStatus: string },
outlinePolling?: { isPolling: boolean; currentStatus: string },
mediumPolling?: { isPolling: boolean; currentStatus: string }
mediumPolling?: { isPolling: boolean; currentStatus: string },
hasContent: boolean = false,
flowAnalysisCompleted: boolean = false,
contentConfirmed: boolean = false
) => {
return useMemo(() => {
const items = [] as { title: string; message: string; priority?: 'high' | 'normal' }[];
@@ -82,7 +88,7 @@ export const useSuggestions = (
// Outline created but not confirmed - focus on outline review and confirmation
items.push({
title: 'Next: Confirm & Generate Content',
message: 'I\'m happy with the outline, let\'s generate content for all sections',
message: 'I confirm the outline and am ready to generate content',
priority: 'high'
});
items.push({
@@ -98,17 +104,49 @@ export const useSuggestions = (
message: 'Rebalance word count distribution across sections'
});
} else if (outline.length > 0 && outlineConfirmed) {
// Outline confirmed, focus on content generation
items.push({ title: '📝 Generate all sections', message: 'Generate all sections of my blog post' });
outline.forEach(s => items.push({ title: `✍️ Generate ${s.heading}`, message: `Generate the section: ${s.heading}` }));
items.push({ title: '📈 Run SEO analysis', message: 'Analyze SEO for my blog post' });
items.push({ title: '🧾 Generate SEO metadata', message: 'Generate SEO metadata and title' });
items.push({ title: '🧪 Hallucination check', message: 'Check for any false claims in my content' });
items.push({ title: '🚀 Publish to WordPress', message: 'Publish my blog to WordPress' });
// Outline confirmed, focus on content generation and optimization
if (hasContent && !contentConfirmed) {
// User has content but hasn't confirmed it yet - show content review suggestions
items.push({
title: 'Next: Confirm Blog Content',
message: 'I have reviewed and confirmed my blog content is ready for the next stage',
priority: 'high'
});
items.push({
title: '🔄 ReWrite Blog',
message: 'I want to rewrite my blog with different approach, tone, or focus'
});
items.push({
title: '📊 Content Analysis',
message: 'Analyze the flow and quality of my blog content to get improvement suggestions'
});
items.push({
title: '📈 Run SEO Analysis',
message: 'Analyze SEO for my blog post'
});
} else if (hasContent && contentConfirmed) {
// Content confirmed - move to SEO stage
items.push({
title: '📈 Run SEO Analysis',
message: 'Analyze SEO for my blog post',
priority: 'high'
});
items.push({
title: '🧾 Generate SEO Metadata',
message: 'Generate SEO metadata and title'
});
items.push({
title: '🚀 Publish to WordPress',
message: 'Publish my blog to WordPress'
});
} else {
// No content yet, show generation option
items.push({ title: '📝 Generate all sections', message: 'Generate all sections of my blog post' });
}
}
return items;
}, [research, outline, outlineConfirmed, researchPolling, outlinePolling, mediumPolling]);
}, [research, outline, outlineConfirmed, researchPolling, outlinePolling, mediumPolling, hasContent, flowAnalysisCompleted, contentConfirmed]);
};
export const SuggestionsGenerator: React.FC<SuggestionsGeneratorProps> = ({ research, outline, outlineConfirmed = false }) => {

View File

@@ -28,6 +28,8 @@ interface BlogEditorProps {
sections?: Record<string, string>;
onContentUpdate?: (sections: any[]) => void;
onSave?: (content: any) => void;
continuityRefresh?: number;
flowAnalysisResults?: any;
}
const BlogEditor: React.FC<BlogEditorProps> = ({
@@ -39,7 +41,9 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
aiGeneratedTitles = [],
sections: parentSections,
onContentUpdate,
onSave
onSave,
continuityRefresh,
flowAnalysisResults
}) => {
const [blogTitle, setBlogTitle] = useState(initialTitle || 'Your Amazing Blog Title');
const [sections, setSections] = useState<any[]>([]);
@@ -146,6 +150,8 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
onContentUpdate={onContentUpdate}
expandedSections={expandedSections}
toggleSectionExpansion={toggleSectionExpansion}
refreshToken={continuityRefresh}
flowAnalysisResults={flowAnalysisResults}
/>
))}
</div>

View File

@@ -20,6 +20,7 @@ import {
ExpandLess as ExpandLessIcon,
} from '@mui/icons-material';
import useBlogTextSelectionHandler from './BlogTextSelectionHandler';
import { ContinuityBadge } from '../ContinuityBadge';
interface BlogSectionProps {
id: any;
@@ -37,6 +38,8 @@ interface BlogSectionProps {
onContentUpdate?: (sections: any[]) => void;
expandedSections: Set<any>;
toggleSectionExpansion: (sectionId: any) => void;
refreshToken?: number;
flowAnalysisResults?: any;
}
const BlogSection: React.FC<BlogSectionProps> = ({
@@ -48,7 +51,9 @@ const BlogSection: React.FC<BlogSectionProps> = ({
outlineData,
onContentUpdate,
expandedSections,
toggleSectionExpansion
toggleSectionExpansion,
refreshToken,
flowAnalysisResults
}) => {
const [isEditing, setIsEditing] = useState(false);
const [sectionTitle, setSectionTitle] = useState(title);
@@ -110,6 +115,7 @@ const BlogSection: React.FC<BlogSectionProps> = ({
const handleContentChange = (e: any) => {
const newContent = e.target.value;
console.log('🔍 [BlogSection] handleContentChange called, content length:', newContent.length);
setContent(newContent);
// Trigger smart typing assist
@@ -147,24 +153,27 @@ const BlogSection: React.FC<BlogSectionProps> = ({
onMouseLeave={() => setIsHovered(false)}
>
{isEditing ? (
<TextField
fullWidth
variant="standard"
value={sectionTitle}
onChange={(e) => setSectionTitle(e.target.value)}
onBlur={() => setIsEditing(false)}
autoFocus
InputProps={{ className: 'text-2xl md:text-3xl font-bold !font-serif text-gray-800 mb-4' }}
/>
) : (
<h2
className="text-2xl md:text-3xl font-bold font-serif text-gray-800 mb-4 cursor-pointer"
onClick={() => setIsEditing(true)}
>
{sectionTitle}
</h2>
)}
<div className="flex items-center gap-3 mb-4">
{isEditing ? (
<TextField
fullWidth
variant="standard"
value={sectionTitle}
onChange={(e) => setSectionTitle(e.target.value)}
onBlur={() => setIsEditing(false)}
autoFocus
InputProps={{ className: 'text-2xl md:text-3xl font-bold !font-serif text-gray-800' }}
/>
) : (
<h2
className="text-2xl md:text-3xl font-bold font-serif text-gray-800 cursor-pointer"
onClick={() => setIsEditing(true)}
>
{sectionTitle}
</h2>
)}
</div>
<div
className="relative"
@@ -359,6 +368,15 @@ const BlogSection: React.FC<BlogSectionProps> = ({
<AutoAwesomeIcon fontSize="small" />
</IconButton>
</Tooltip>
{/* Flow Analysis Badge - Enabled when flow analysis results are available */}
<ContinuityBadge
sectionId={id}
refreshToken={refreshToken}
disabled={!flowAnalysisResults}
flowAnalysisResults={flowAnalysisResults}
/>
<Tooltip title="Copy Section"><IconButton size="small"><FileCopyOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Edit Metadata"><IconButton size="small"><EditIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete Section"><IconButton size="small" className="text-red-500"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>

View File

@@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { hallucinationDetectorService, HallucinationDetectionResponse } from '../../../services/hallucinationDetectorService';
import FactCheckResults from '../../LinkedInWriter/components/FactCheckResults';
import TextSelectionMenu from './TextSelectionMenu';
import useSmartTypingAssist from './SmartTypingAssist';
interface BlogTextSelectionHandlerProps {
contentRef: React.RefObject<HTMLDivElement | HTMLTextAreaElement>;
@@ -17,11 +18,8 @@ const useBlogTextSelectionHandler = (
const [factCheckProgress, setFactCheckProgress] = useState<{ step: string; progress: number } | null>(null);
const selectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Smart typing assist states
const [smartSuggestion, setSmartSuggestion] = useState<{ text: string; position: { x: number; y: number } } | null>(null);
const [isGeneratingSuggestion, setIsGeneratingSuggestion] = useState(false);
const [hasShownFirstSuggestion, setHasShownFirstSuggestion] = useState(false);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Use the extracted smart typing assist hook
const smartTypingAssist = useSmartTypingAssist(contentRef, onTextReplace);
// Fact-checking functionality
const handleCheckFacts = async (text: string) => {
@@ -179,84 +177,6 @@ const useBlogTextSelectionHandler = (
setSelectionMenu(null);
};
// Smart typing assist functionality
const generateSmartSuggestion = async (currentText: string) => {
if (currentText.length < 20) return; // Only suggest after some meaningful content
setIsGeneratingSuggestion(true);
try {
// Simulate AI generation with contextual suggestions
await new Promise(resolve => setTimeout(resolve, 1500));
const suggestions = [
"This approach provides significant value to readers by offering actionable insights they can implement immediately.",
"Research indicates that this strategy has proven effective across multiple industries and use cases.",
"Furthermore, this method demonstrates measurable improvements in key performance indicators.",
"Additionally, industry experts recommend this technique for sustainable long-term growth.",
"Moreover, this framework addresses common challenges while providing practical solutions."
];
const randomSuggestion = suggestions[Math.floor(Math.random() * suggestions.length)];
// Get cursor position for suggestion placement
if (contentRef.current) {
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const x = rect.left + 20;
const y = rect.bottom + 5;
setSmartSuggestion({
text: randomSuggestion,
position: { x, y }
});
}
} catch (error) {
console.error('Failed to generate smart suggestion:', error);
} finally {
setIsGeneratingSuggestion(false);
}
};
const handleTypingChange = (newText: string) => {
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Clear any existing suggestion when user types
setSmartSuggestion(null);
// Set new timeout for suggestion generation
typingTimeoutRef.current = setTimeout(() => {
// First time suggestion appears automatically
if (!hasShownFirstSuggestion && newText.length > 20) {
generateSmartSuggestion(newText);
setHasShownFirstSuggestion(true);
}
// After first time, only suggest after longer pauses or more content
else if (hasShownFirstSuggestion && newText.length > 50 && Math.random() > 0.7) {
generateSmartSuggestion(newText);
}
}, 3000); // 3 second pause before suggesting
};
const handleAcceptSuggestion = () => {
if (smartSuggestion && onTextReplace && contentRef.current) {
const element = contentRef.current;
const currentContent = (element as HTMLTextAreaElement).value || (element as HTMLDivElement).textContent || '';
const newContent = currentContent + ' ' + smartSuggestion.text;
// Use the text replacement callback
onTextReplace(currentContent, newContent, 'smart-suggestion');
setSmartSuggestion(null);
}
};
const handleRejectSuggestion = () => {
setSmartSuggestion(null);
};
// Cleanup progress and timeouts on unmount
useEffect(() => {
@@ -265,9 +185,6 @@ const useBlogTextSelectionHandler = (
if (selectionTimeoutRef.current) {
clearTimeout(selectionTimeoutRef.current);
}
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
@@ -306,9 +223,29 @@ const useBlogTextSelectionHandler = (
console.log('🔍 [BlogTextSelectionHandler] Range rect:', rect);
// Check if rect has valid dimensions
if (rect.width === 0 && rect.height === 0) {
console.log('🔍 [BlogTextSelectionHandler] Invalid rect dimensions, trying alternative positioning');
// Try to get position from the textarea element itself
if (contentRef.current) {
const textareaRect = contentRef.current.getBoundingClientRect();
console.log('🔍 [BlogTextSelectionHandler] Textarea rect:', textareaRect);
// Position menu near the textarea center
const x = Math.max(8, Math.min(textareaRect.left + (textareaRect.width / 2), window.innerWidth - 280));
const y = Math.max(8, textareaRect.top + window.scrollY - 60);
const menuPosition = { x, y, text };
console.log('🔍 [BlogTextSelectionHandler] Using textarea position:', menuPosition);
setSelectionMenu(menuPosition);
return;
}
}
// Use viewport coordinates for absolute positioning
const x = Math.max(8, Math.min(rect.left + (rect.width / 2), window.innerWidth - 280)); // Account for menu width
const y = Math.max(8, rect.top + window.scrollY);
const y = Math.max(8, rect.top + window.scrollY - 60); // Position above selection
const menuPosition = { x, y, text };
console.log('🔍 [BlogTextSelectionHandler] Setting selection menu at position (debounced):', menuPosition);
@@ -327,464 +264,30 @@ const useBlogTextSelectionHandler = (
factCheckResults,
isFactChecking,
factCheckProgress,
smartSuggestion,
isGeneratingSuggestion,
handleTextSelection,
handleCheckFacts,
handleCloseFactCheckResults,
handleQuickEdit,
handleTypingChange,
handleAcceptSuggestion,
handleRejectSuggestion,
// Smart typing assist functionality from extracted hook
...smartTypingAssist,
// Render the selection menu and fact-check components
renderSelectionMenu: () => (
<>
{/* Text Selection Menu */}
{selectionMenu && (
<div
onClick={(e) => {
console.log('🔍 [BlogTextSelectionHandler] Selection menu clicked!', e.target);
e.stopPropagation();
}}
style={{
position: 'fixed',
top: selectionMenu.y - 60,
left: Math.max(8, selectionMenu.x - 140),
background: 'rgba(79, 70, 229, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.25)',
borderRadius: '12px',
display: 'flex',
flexDirection: 'column',
gap: '6px',
padding: '12px 16px',
boxShadow: '0 12px 28px rgba(0, 0, 0, 0.35)',
backdropFilter: 'blur(12px)',
zIndex: 10000,
minWidth: '240px',
maxWidth: '280px'
}}
>
{/* Fact Check Button */}
<button
onClick={(e) => {
console.log('🔍 [BlogTextSelectionHandler] Check Facts button clicked!', selectionMenu.text);
e.preventDefault();
e.stopPropagation();
handleCheckFacts(selectionMenu.text);
}}
disabled={isFactChecking}
style={{
background: isFactChecking ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '8px',
padding: '8px 16px',
color: 'white',
fontSize: '13px',
fontWeight: '600',
cursor: isFactChecking ? 'not-allowed' : 'pointer',
opacity: isFactChecking ? 0.6 : 1,
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
width: '100%',
justifyContent: 'center'
}}
onMouseEnter={(e) => {
if (!isFactChecking) {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)';
}
}}
onMouseLeave={(e) => {
if (!isFactChecking) {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}
}}
>
{isFactChecking ? (
<>
<div style={{
width: '14px',
height: '14px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
Fact-checking...
</>
) : (
<>
🔍 Fact Check
</>
)}
</button>
{/* Quick Edit Options */}
<div style={{
borderTop: '1px solid rgba(255, 255, 255, 0.2)',
paddingTop: '10px',
marginTop: '6px'
}}>
<div style={{
color: 'rgba(255, 255, 255, 0.9)',
fontSize: '11px',
fontWeight: '600',
marginBottom: '8px',
textAlign: 'center',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Assistive Writing
</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '6px'
}}>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('improve', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Improve
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('add-transition', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
🔗 Transition
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('shorten', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Shorten
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('expand', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
📝 Expand
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('professionalize', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
🎓 Professional
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleQuickEdit('add-data', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
📊 Add Data
</button>
</div>
</div>
</div>
)}
{/* Fact Check Progress */}
{factCheckProgress && (
<div style={{
position: 'fixed',
top: '20px',
right: '20px',
background: 'rgba(79, 70, 229, 0.95)',
color: 'white',
padding: '12px 20px',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(10px)',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
gap: '12px',
minWidth: '280px'
}}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<div>
<div style={{ fontSize: '13px', fontWeight: '600' }}>
Fact-checking content...
</div>
<div style={{ fontSize: '11px', opacity: 0.8 }}>
{factCheckProgress.step}
</div>
</div>
</div>
)}
{/* Fact Check Results */}
{factCheckResults && (
<FactCheckResults
results={factCheckResults}
onClose={handleCloseFactCheckResults}
/>
)}
{/* Smart Typing Suggestion */}
{smartSuggestion && (
<div
style={{
position: 'fixed',
top: smartSuggestion.position.y,
left: smartSuggestion.position.x,
background: 'rgba(34, 197, 94, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.25)',
borderRadius: '12px',
padding: '16px 20px',
boxShadow: '0 12px 28px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(12px)',
zIndex: 10002,
maxWidth: '400px',
minWidth: '320px',
color: 'white'
}}
>
<div style={{
fontSize: '12px',
fontWeight: '600',
marginBottom: '8px',
opacity: 0.9,
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Smart Writing Suggestion
</div>
<div style={{
fontSize: '14px',
lineHeight: '1.4',
marginBottom: '16px',
fontStyle: 'italic'
}}>
"{smartSuggestion.text}"
</div>
<div style={{
display: 'flex',
gap: '8px',
justifyContent: 'flex-end'
}}>
<button
onClick={handleRejectSuggestion}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 12px',
color: 'white',
fontSize: '12px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Dismiss
</button>
<button
onClick={handleAcceptSuggestion}
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '6px',
padding: '6px 12px',
color: 'white',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}}
>
Accept
</button>
</div>
</div>
)}
{/* Smart Suggestion Loading Indicator */}
{isGeneratingSuggestion && (
<div style={{
position: 'fixed',
bottom: '20px',
right: '20px',
background: 'rgba(34, 197, 94, 0.95)',
color: 'white',
padding: '12px 20px',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(10px)',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
gap: '12px',
minWidth: '240px'
}}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<div>
<div style={{ fontSize: '13px', fontWeight: '600' }}>
Generating suggestion...
</div>
<div style={{ fontSize: '11px', opacity: 0.8 }}>
AI is crafting helpful content
</div>
</div>
</div>
)}
{/* CSS for spinner animation */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</>
<TextSelectionMenu
selectionMenu={selectionMenu}
factCheckResults={factCheckResults}
isFactChecking={isFactChecking}
factCheckProgress={factCheckProgress}
smartSuggestion={smartTypingAssist.smartSuggestion}
isGeneratingSuggestion={smartTypingAssist.isGeneratingSuggestion}
allSuggestions={smartTypingAssist.allSuggestions}
suggestionIndex={smartTypingAssist.suggestionIndex}
onCheckFacts={handleCheckFacts}
onCloseFactCheckResults={handleCloseFactCheckResults}
onQuickEdit={handleQuickEdit}
onAcceptSuggestion={smartTypingAssist.handleAcceptSuggestion}
onRejectSuggestion={smartTypingAssist.handleRejectSuggestion}
onNextSuggestion={smartTypingAssist.handleNextSuggestion}
/>
)
};
};

View File

@@ -0,0 +1,297 @@
import React, { useState, useRef, useEffect } from 'react';
interface SmartTypingAssistProps {
contentRef: React.RefObject<HTMLDivElement | HTMLTextAreaElement>;
onTextReplace?: (originalText: string, newText: string, editType: string) => void;
}
interface Suggestion {
text: string;
confidence?: number;
sources?: Array<{
title: string;
url: string;
text?: string;
author?: string;
published_date?: string;
score: number;
}>;
}
const useSmartTypingAssist = (
contentRef: React.RefObject<HTMLDivElement | HTMLTextAreaElement>,
onTextReplace?: (originalText: string, newText: string, editType: string) => void
) => {
// Smart typing assist states
const [smartSuggestion, setSmartSuggestion] = useState<{
text: string;
position: { x: number; y: number };
confidence?: number;
sources?: Array<{
title: string;
url: string;
text?: string;
author?: string;
published_date?: string;
score: number;
}>;
} | null>(null);
const [suggestionIndex, setSuggestionIndex] = useState(0);
const [allSuggestions, setAllSuggestions] = useState<Suggestion[]>([]);
const [isGeneratingSuggestion, setIsGeneratingSuggestion] = useState(false);
const [hasShownFirstSuggestion, setHasShownFirstSuggestion] = useState(false);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Quality improvement tracking
const [suggestionStats, setSuggestionStats] = useState({
totalShown: 0,
totalAccepted: 0,
totalRejected: 0,
totalCycled: 0
});
// Smart typing assist functionality
const generateSmartSuggestion = async (currentText: string) => {
console.log('🔍 [SmartTypingAssist] generateSmartSuggestion called with text length:', currentText.length);
if (currentText.length < 20) {
console.log('🔍 [SmartTypingAssist] Text too short for suggestion');
return; // Only suggest after some meaningful content
}
console.log('🔍 [SmartTypingAssist] Starting suggestion generation...');
setIsGeneratingSuggestion(true);
try {
// Import the assistive writing API
const { assistiveWritingApi } = await import('../../../services/blogWriterApi');
console.log('🔍 [SmartTypingAssist] Calling assistive writing API...');
const response = await assistiveWritingApi.getSuggestion(currentText, 3); // Get 3 suggestions
if (response.success && response.suggestions.length > 0) {
console.log('🔍 [SmartTypingAssist] Received', response.suggestions.length, 'suggestions from API');
// Store all suggestions
setAllSuggestions(response.suggestions);
setSuggestionIndex(0);
// Show first suggestion
const firstSuggestion = response.suggestions[0];
console.log('🔍 [SmartTypingAssist] Showing first suggestion:', firstSuggestion.text.substring(0, 50) + '...');
// Track suggestion shown
setSuggestionStats(prev => ({
...prev,
totalShown: prev.totalShown + 1
}));
// Get cursor position for suggestion placement
if (contentRef.current) {
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const x = Math.max(20, Math.min(rect.left + 20, window.innerWidth - 420)); // Ensure it stays on screen
const y = Math.max(20, rect.bottom + 10);
setSmartSuggestion({
text: firstSuggestion.text,
position: { x, y },
confidence: firstSuggestion.confidence,
sources: firstSuggestion.sources
});
}
} else {
console.log('🔍 [SmartTypingAssist] No suggestions received from API');
// Fallback to generic suggestions if API fails
const fallbackSuggestions = [
"This approach provides significant value to readers by offering actionable insights they can implement immediately.",
"Research indicates that this strategy has proven effective across multiple industries and use cases.",
"Furthermore, this method demonstrates measurable improvements in key performance indicators.",
"Additionally, industry experts recommend this technique for sustainable long-term growth.",
"Moreover, this framework addresses common challenges while providing practical solutions."
];
const randomSuggestion = fallbackSuggestions[Math.floor(Math.random() * fallbackSuggestions.length)];
if (contentRef.current) {
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const x = rect.left + 20;
const y = rect.bottom + 5;
setSmartSuggestion({
text: randomSuggestion,
position: { x, y }
});
}
}
} catch (error) {
console.error('🔍 [SmartTypingAssist] Failed to generate smart suggestion:', error);
// Fallback to generic suggestions on error
const fallbackSuggestions = [
"This approach provides significant value to readers by offering actionable insights they can implement immediately.",
"Research indicates that this strategy has proven effective across multiple industries and use cases.",
"Furthermore, this method demonstrates measurable improvements in key performance indicators.",
"Additionally, industry experts recommend this technique for sustainable long-term growth.",
"Moreover, this framework addresses common challenges while providing practical solutions."
];
const randomSuggestion = fallbackSuggestions[Math.floor(Math.random() * fallbackSuggestions.length)];
if (contentRef.current) {
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const x = rect.left + 20;
const y = rect.bottom + 5;
setSmartSuggestion({
text: randomSuggestion,
position: { x, y }
});
}
} finally {
setIsGeneratingSuggestion(false);
}
};
const handleTypingChange = (newText: string) => {
console.log('🔍 [SmartTypingAssist] handleTypingChange called with text length:', newText.length);
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Clear any existing suggestion when user types
setSmartSuggestion(null);
// Set new timeout for suggestion generation
typingTimeoutRef.current = setTimeout(() => {
console.log('🔍 [SmartTypingAssist] Typing timeout triggered, text length:', newText.length, 'hasShownFirstSuggestion:', hasShownFirstSuggestion);
// First time suggestion appears automatically
if (!hasShownFirstSuggestion && newText.length > 20) {
console.log('🔍 [SmartTypingAssist] Generating first suggestion');
generateSmartSuggestion(newText);
setHasShownFirstSuggestion(true);
}
// After first time, only suggest after longer pauses or more content
else if (hasShownFirstSuggestion && newText.length > 50 && Math.random() > 0.7) {
console.log('🔍 [SmartTypingAssist] Generating subsequent suggestion');
generateSmartSuggestion(newText);
} else {
console.log('🔍 [SmartTypingAssist] No suggestion generated - conditions not met');
}
}, 3000); // 3 second pause before suggesting
};
const handleAcceptSuggestion = () => {
if (smartSuggestion && onTextReplace && contentRef.current) {
const element = contentRef.current;
const currentContent = (element as HTMLTextAreaElement).value || (element as HTMLDivElement).textContent || '';
const newContent = currentContent + ' ' + smartSuggestion.text;
// Track suggestion accepted
setSuggestionStats(prev => ({
...prev,
totalAccepted: prev.totalAccepted + 1
}));
console.log('🔍 [SmartTypingAssist] Suggestion accepted! Stats:', {
...suggestionStats,
totalAccepted: suggestionStats.totalAccepted + 1
});
// Use the text replacement callback
onTextReplace(currentContent, newContent, 'smart-suggestion');
setSmartSuggestion(null);
}
};
const handleRejectSuggestion = () => {
// Track suggestion rejected
setSuggestionStats(prev => ({
...prev,
totalRejected: prev.totalRejected + 1
}));
console.log('🔍 [SmartTypingAssist] Suggestion rejected! Stats:', {
...suggestionStats,
totalRejected: suggestionStats.totalRejected + 1
});
setSmartSuggestion(null);
setAllSuggestions([]);
setSuggestionIndex(0);
};
const handleNextSuggestion = () => {
if (allSuggestions.length > 0 && suggestionIndex < allSuggestions.length - 1) {
const nextIndex = suggestionIndex + 1;
const nextSuggestion = allSuggestions[nextIndex];
// Track suggestion cycled
setSuggestionStats(prev => ({
...prev,
totalCycled: prev.totalCycled + 1
}));
console.log('🔍 [SmartTypingAssist] Showing next suggestion:', nextIndex + 1, 'of', allSuggestions.length);
console.log('🔍 [SmartTypingAssist] Suggestion cycled! Stats:', {
...suggestionStats,
totalCycled: suggestionStats.totalCycled + 1
});
setSuggestionIndex(nextIndex);
setSmartSuggestion(prev => prev ? {
...prev,
text: nextSuggestion.text,
confidence: nextSuggestion.confidence,
sources: nextSuggestion.sources
} : null);
}
};
// Get suggestion statistics for quality improvement
const getSuggestionStats = () => {
const acceptanceRate = suggestionStats.totalShown > 0
? Math.round((suggestionStats.totalAccepted / suggestionStats.totalShown) * 100)
: 0;
return {
...suggestionStats,
acceptanceRate,
engagementRate: suggestionStats.totalShown > 0
? Math.round(((suggestionStats.totalAccepted + suggestionStats.totalCycled) / suggestionStats.totalShown) * 100)
: 0
};
};
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return {
smartSuggestion,
isGeneratingSuggestion,
allSuggestions,
suggestionIndex,
suggestionStats: getSuggestionStats(),
handleTypingChange,
handleAcceptSuggestion,
handleRejectSuggestion,
handleNextSuggestion,
getSuggestionStats,
generateSmartSuggestion
};
};
export default useSmartTypingAssist;
export type { SmartTypingAssistProps, Suggestion };

View File

@@ -0,0 +1,554 @@
import React from 'react';
import { HallucinationDetectionResponse } from '../../../services/hallucinationDetectorService';
import FactCheckResults from '../../LinkedInWriter/components/FactCheckResults';
interface TextSelectionMenuProps {
selectionMenu: { x: number; y: number; text: string } | null;
factCheckResults: HallucinationDetectionResponse | null;
isFactChecking: boolean;
factCheckProgress: { step: string; progress: number } | null;
smartSuggestion: {
text: string;
position: { x: number; y: number };
confidence?: number;
sources?: Array<{
title: string;
url: string;
text?: string;
author?: string;
published_date?: string;
score: number;
}>;
} | null;
isGeneratingSuggestion: boolean;
allSuggestions: Array<{
text: string;
confidence?: number;
sources?: Array<{
title: string;
url: string;
text?: string;
author?: string;
published_date?: string;
score: number;
}>;
}>;
suggestionIndex: number;
onCheckFacts: (text: string) => void;
onCloseFactCheckResults: () => void;
onQuickEdit: (editType: string, selectedText: string) => void;
onAcceptSuggestion: () => void;
onRejectSuggestion: () => void;
onNextSuggestion: () => void;
}
const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({
selectionMenu,
factCheckResults,
isFactChecking,
factCheckProgress,
smartSuggestion,
isGeneratingSuggestion,
allSuggestions,
suggestionIndex,
onCheckFacts,
onCloseFactCheckResults,
onQuickEdit,
onAcceptSuggestion,
onRejectSuggestion,
onNextSuggestion
}) => {
return (
<>
{/* Text Selection Menu */}
{selectionMenu && (
<div
onClick={(e) => {
console.log('🔍 [TextSelectionMenu] Selection menu clicked!', e.target);
e.stopPropagation();
}}
style={{
position: 'fixed',
top: selectionMenu.y - 60,
left: Math.max(8, selectionMenu.x - 140),
background: 'rgba(79, 70, 229, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.25)',
borderRadius: '12px',
display: 'flex',
flexDirection: 'column',
gap: '6px',
padding: '12px 16px',
boxShadow: '0 12px 28px rgba(0, 0, 0, 0.35)',
backdropFilter: 'blur(12px)',
zIndex: 10000,
minWidth: '240px',
maxWidth: '280px'
}}
>
{/* Fact Check Button */}
<button
onClick={(e) => {
console.log('🔍 [TextSelectionMenu] Check Facts button clicked!', selectionMenu.text);
e.preventDefault();
e.stopPropagation();
onCheckFacts(selectionMenu.text);
}}
disabled={isFactChecking}
style={{
background: isFactChecking ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '8px',
padding: '8px 16px',
color: 'white',
fontSize: '13px',
fontWeight: '600',
cursor: isFactChecking ? 'not-allowed' : 'pointer',
opacity: isFactChecking ? 0.6 : 1,
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
width: '100%',
justifyContent: 'center'
}}
onMouseEnter={(e) => {
if (!isFactChecking) {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)';
}
}}
onMouseLeave={(e) => {
if (!isFactChecking) {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}
}}
>
{isFactChecking ? (
<>
<div style={{
width: '14px',
height: '14px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
Fact-checking...
</>
) : (
<>
🔍 Fact Check
</>
)}
</button>
{/* Quick Edit Options */}
<div style={{
borderTop: '1px solid rgba(255, 255, 255, 0.2)',
paddingTop: '10px',
marginTop: '6px'
}}>
<div style={{
color: 'rgba(255, 255, 255, 0.9)',
fontSize: '11px',
fontWeight: '600',
marginBottom: '8px',
textAlign: 'center',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Assistive Writing
</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '6px'
}}>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('improve', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Improve
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('add-transition', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
🔗 Transition
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('shorten', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Shorten
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('expand', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
📝 Expand
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('professionalize', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
🎓 Professional
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onQuickEdit('add-data', selectionMenu.text);
}}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 10px',
color: 'white',
fontSize: '11px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
📊 Add Data
</button>
</div>
</div>
</div>
)}
{/* Fact Check Progress */}
{factCheckProgress && (
<div style={{
position: 'fixed',
top: '20px',
right: '20px',
background: 'rgba(79, 70, 229, 0.95)',
color: 'white',
padding: '12px 20px',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(10px)',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
gap: '12px',
minWidth: '280px'
}}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<div>
<div style={{ fontSize: '13px', fontWeight: '600' }}>
Fact-checking content...
</div>
<div style={{ fontSize: '11px', opacity: 0.8 }}>
{factCheckProgress.step}
</div>
</div>
</div>
)}
{/* Fact Check Results */}
{factCheckResults && (
<FactCheckResults
results={factCheckResults}
onClose={onCloseFactCheckResults}
/>
)}
{/* Smart Typing Suggestion */}
{smartSuggestion && (
<div
onClick={(e) => {
console.log('🔍 [TextSelectionMenu] Smart suggestion modal clicked!', smartSuggestion);
e.stopPropagation();
}}
style={{
position: 'fixed',
top: smartSuggestion.position.y,
left: smartSuggestion.position.x,
background: 'rgba(34, 197, 94, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.25)',
borderRadius: '12px',
padding: '16px 20px',
boxShadow: '0 12px 28px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(12px)',
zIndex: 10002,
maxWidth: '400px',
minWidth: '320px',
color: 'white'
}}
>
<div style={{
fontSize: '12px',
fontWeight: '600',
marginBottom: '8px',
opacity: 0.9,
textTransform: 'uppercase',
letterSpacing: '0.5px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span> Smart Writing Suggestion</span>
{allSuggestions.length > 1 && (
<span style={{ fontSize: '10px', opacity: 0.7 }}>
{suggestionIndex + 1} of {allSuggestions.length}
</span>
)}
</div>
<div style={{
fontSize: '14px',
lineHeight: '1.4',
marginBottom: '16px',
fontStyle: 'italic'
}}>
"{smartSuggestion.text}"
</div>
<div style={{
display: 'flex',
gap: '8px',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ display: 'flex', gap: '8px' }}>
{allSuggestions.length > 1 && suggestionIndex < allSuggestions.length - 1 && (
<button
onClick={onNextSuggestion}
style={{
background: 'rgba(255, 255, 255, 0.1)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 12px',
color: 'white',
fontSize: '12px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
}}
>
Next
</button>
)}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={onRejectSuggestion}
style={{
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '6px',
padding: '6px 12px',
color: 'white',
fontSize: '12px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
}}
>
Dismiss
</button>
<button
onClick={onAcceptSuggestion}
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '6px',
padding: '6px 12px',
color: 'white',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}}
>
Accept
</button>
</div>
</div>
</div>
)}
{/* Smart Suggestion Loading Indicator */}
{isGeneratingSuggestion && (
<div style={{
position: 'fixed',
bottom: '20px',
right: '20px',
background: 'rgba(34, 197, 94, 0.95)',
color: 'white',
padding: '12px 20px',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.25)',
backdropFilter: 'blur(10px)',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
gap: '12px',
minWidth: '240px'
}}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid rgba(255, 255, 255, 0.3)',
borderTop: '2px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<div>
<div style={{ fontSize: '13px', fontWeight: '600' }}>
Generating suggestion...
</div>
<div style={{ fontSize: '11px', opacity: 0.8 }}>
AI is crafting helpful content
</div>
</div>
</div>
)}
{/* CSS for spinner animation */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</>
);
};
export default TextSelectionMenu;