Updated SEO Analysis Modal
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
381
frontend/src/components/BlogWriter/RewriteFeedbackForm.tsx
Normal file
381
frontend/src/components/BlogWriter/RewriteFeedbackForm.tsx
Normal 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;
|
||||
240
frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx
Normal file
240
frontend/src/components/BlogWriter/SEO/KeywordAnalysis.tsx
Normal 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 (<1%):</strong> Keyword may not be prominent enough
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block' }}>
|
||||
<strong>Too High (>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>
|
||||
);
|
||||
};
|
||||
103
frontend/src/components/BlogWriter/SEO/README.md
Normal file
103
frontend/src/components/BlogWriter/SEO/README.md
Normal 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
|
||||
286
frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx
Normal file
286
frontend/src/components/BlogWriter/SEO/ReadabilityAnalysis.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
108
frontend/src/components/BlogWriter/SEO/Recommendations.tsx
Normal file
108
frontend/src/components/BlogWriter/SEO/Recommendations.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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' };
|
||||
},
|
||||
196
frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx
Normal file
196
frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
11
frontend/src/components/BlogWriter/SEO/index.ts
Normal file
11
frontend/src/components/BlogWriter/SEO/index.ts
Normal 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';
|
||||
710
frontend/src/components/BlogWriter/SEOAnalysisModal.tsx
Normal file
710
frontend/src/components/BlogWriter/SEOAnalysisModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
297
frontend/src/components/BlogWriter/WYSIWYG/SmartTypingAssist.tsx
Normal file
297
frontend/src/components/BlogWriter/WYSIWYG/SmartTypingAssist.tsx
Normal 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 };
|
||||
554
frontend/src/components/BlogWriter/WYSIWYG/TextSelectionMenu.tsx
Normal file
554
frontend/src/components/BlogWriter/WYSIWYG/TextSelectionMenu.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user