Research Wizard and CopilotKit mitigation review

This commit is contained in:
ajaysi
2025-11-04 08:11:57 +05:30
parent e69107b07c
commit 55087c4f37
27 changed files with 2167 additions and 277 deletions

View File

@@ -270,6 +270,7 @@ export const BlogWriter: React.FC = () => {
handleOutlineAction,
handleContentAction,
handleSEOAction,
handleApplySEORecommendations,
handlePublishAction,
} = usePhaseActionHandlers({
research,
@@ -284,6 +285,7 @@ export const BlogWriter: React.FC = () => {
outlineGenRef,
setOutline,
setContentConfirmed,
setIsSEOAnalysisModalOpen,
setIsSEOMetadataModalOpen,
runSEOAnalysisDirect,
onOutlineComplete: handleCachedOutlineComplete,
@@ -391,6 +393,7 @@ export const BlogWriter: React.FC = () => {
onOutlineAction: handleOutlineAction,
onContentAction: handleContentAction,
onSEOAction: handleSEOAction,
onApplySEORecommendations: handleApplySEORecommendations,
onPublishAction: handlePublishAction,
}}
hasResearch={!!research}
@@ -399,6 +402,7 @@ export const BlogWriter: React.FC = () => {
hasContent={Object.keys(sections).length > 0}
contentConfirmed={contentConfirmed}
hasSEOAnalysis={!!seoAnalysis}
seoRecommendationsApplied={seoRecommendationsApplied}
hasSEOMetadata={!!seoMetadata}
/>
)}

View File

@@ -13,6 +13,7 @@ interface HeaderBarProps {
hasContent?: boolean;
contentConfirmed?: boolean;
hasSEOAnalysis?: boolean;
seoRecommendationsApplied?: boolean;
hasSEOMetadata?: boolean;
}
@@ -28,6 +29,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({
hasContent = false,
contentConfirmed = false,
hasSEOAnalysis = false,
seoRecommendationsApplied = false,
hasSEOMetadata = false,
}) => {
return (
@@ -61,6 +63,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({
hasContent={hasContent}
contentConfirmed={contentConfirmed}
hasSEOAnalysis={hasSEOAnalysis}
seoRecommendationsApplied={seoRecommendationsApplied}
hasSEOMetadata={hasSEOMetadata}
/>
</div>

View File

@@ -25,28 +25,28 @@ export const WriterCopilotSidebar: React.FC<WriterCopilotSidebarProps> = ({
}}
suggestions={suggestions}
makeSystemMessage={(context: string, additional?: string) => {
const hasResearch = research !== null;
const hasOutline = outline.length > 0;
const hasResearch = research !== null && research !== undefined;
const hasOutline = outline && outline.length > 0;
const isOutlineConfirmed = outlineConfirmed;
const researchInfo = hasResearch
const researchInfo = hasResearch && research
? {
sources: research.sources?.length || 0,
queries: research.search_queries?.length || 0,
angles: research.suggested_angles?.length || 0,
primaryKeywords: research.keyword_analysis?.primary || [],
searchIntent: research.keyword_analysis?.search_intent || 'informational',
sources: research?.sources?.length || 0,
queries: research?.search_queries?.length || 0,
angles: research?.suggested_angles?.length || 0,
primaryKeywords: research?.keyword_analysis?.primary || [],
searchIntent: research?.keyword_analysis?.search_intent || 'informational',
}
: null;
const outlineContext = hasOutline
const outlineContext = hasOutline && outline
? `
OUTLINE DETAILS:
- Total sections: ${outline.length}
- Section headings: ${outline.map((s: any) => s.heading).join(', ')}
- Total target words: ${outline.reduce((sum: number, s: any) => sum + (s.target_words || 0), 0)}
- Section breakdown: ${outline
- Section headings: ${(outline || []).map((s: any) => s?.heading || 'Untitled').join(', ')}
- Total target words: ${(outline || []).reduce((sum: number, s: any) => sum + (s?.target_words || 0), 0)}
- Section breakdown: ${(outline || [])
.map(
(s: any) => `${s.heading} (${s.target_words || 0} words, ${s.subheadings?.length || 0} subheadings, ${s.key_points?.length || 0} key points)`
(s: any) => `${s?.heading || 'Untitled'} (${s?.target_words || 0} words, ${s?.subheadings?.length || 0} subheadings, ${s?.key_points?.length || 0} key points)`
)
.join('; ')}
`
@@ -65,7 +65,7 @@ ${hasResearch && researchInfo ? `
- Search intent: ${researchInfo.searchIntent}
` : '❌ No research completed yet'}
${hasOutline ? `✅ OUTLINE GENERATED: ${outline.length} sections created${isOutlineConfirmed ? ' (CONFIRMED)' : ' (PENDING CONFIRMATION)'}` : '❌ No outline generated yet'}
${hasOutline && outline ? `✅ OUTLINE GENERATED: ${outline.length} sections created${isOutlineConfirmed ? ' (CONFIRMED)' : ' (PENDING CONFIRMATION)'}` : '❌ No outline generated yet'}
${outlineContext}
Available tools:

View File

@@ -17,6 +17,7 @@ interface UsePhaseActionHandlersProps {
outlineGenRef: React.RefObject<any>;
setOutline: (outline: any[]) => void;
setContentConfirmed: (confirmed: boolean) => void;
setIsSEOAnalysisModalOpen: (open: boolean) => void;
setIsSEOMetadataModalOpen: (open: boolean) => void;
runSEOAnalysisDirect: () => string;
onOutlineComplete?: (outline: any) => void;
@@ -36,6 +37,7 @@ export const usePhaseActionHandlers = ({
outlineGenRef,
setOutline,
setContentConfirmed,
setIsSEOAnalysisModalOpen,
setIsSEOMetadataModalOpen,
runSEOAnalysisDirect,
onOutlineComplete,
@@ -162,13 +164,20 @@ export const usePhaseActionHandlers = ({
}
navigateToPhase('seo');
runSEOAnalysisDirect();
debug.log('[BlogWriter] SEO action triggered');
debug.log('[BlogWriter] SEO action triggered - running SEO analysis');
}, [contentConfirmed, setContentConfirmed, navigateToPhase, runSEOAnalysisDirect]);
const handleApplySEORecommendations = useCallback(() => {
navigateToPhase('seo');
setIsSEOAnalysisModalOpen(true);
debug.log('[BlogWriter] Apply SEO Recommendations action triggered - opening SEO analysis modal');
}, [navigateToPhase, setIsSEOAnalysisModalOpen]);
const handlePublishAction = useCallback(() => {
navigateToPhase('publish');
// Can be called from SEO phase (after recommendations applied) or publish phase
navigateToPhase('seo'); // Stay in SEO phase if called from there
setIsSEOMetadataModalOpen(true);
debug.log('[BlogWriter] Publish action triggered - opening SEO metadata modal');
debug.log('[BlogWriter] Generate SEO Metadata action triggered - opening SEO metadata modal');
}, [navigateToPhase, setIsSEOMetadataModalOpen]);
return {
@@ -176,6 +185,7 @@ export const usePhaseActionHandlers = ({
handleOutlineAction,
handleContentAction,
handleSEOAction,
handleApplySEORecommendations,
handlePublishAction,
};
};

View File

@@ -15,6 +15,7 @@ export interface PhaseActionHandlers {
onOutlineAction?: () => void; // Generate outline
onContentAction?: () => void; // Confirm outline + generate content
onSEOAction?: () => void; // Run SEO analysis
onApplySEORecommendations?: () => void; // Apply SEO recommendations
onPublishAction?: () => void; // Generate SEO metadata or publish
}
@@ -31,6 +32,7 @@ interface PhaseNavigationProps {
hasContent?: boolean;
contentConfirmed?: boolean;
hasSEOAnalysis?: boolean;
seoRecommendationsApplied?: boolean;
hasSEOMetadata?: boolean;
}
@@ -46,6 +48,7 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
hasContent = false,
contentConfirmed = false,
hasSEOAnalysis = false,
seoRecommendationsApplied = false,
hasSEOMetadata = false,
}) => {
// Determine which action to show for each phase when CopilotKit is unavailable
@@ -61,7 +64,10 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
}
break;
case 'outline':
if (hasResearch && !hasOutline) {
// Show "Create Outline" if research exists and outline is not yet confirmed
// This ensures users can create/regenerate outline after research, even if cached one exists
// Once outline is confirmed, we hide the button to avoid confusion during content generation
if (hasResearch && !outlineConfirmed) {
return { label: 'Create Outline', handler: actionHandlers.onOutlineAction || null };
}
break;
@@ -71,13 +77,26 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
}
break;
case 'seo':
if (hasContent && contentConfirmed && !hasSEOAnalysis) {
// Priority order matching CopilotKit suggestions:
// 1. No SEO analysis yet - Run SEO Analysis
// Note: We check hasContent (sections exist) - contentConfirmed is checked but not strictly required
// This allows users to run SEO analysis even if contentConfirmed hasn't been explicitly set
if (hasContent && !hasSEOAnalysis) {
return { label: 'Run SEO Analysis', handler: actionHandlers.onSEOAction || null };
}
// 2. SEO analysis exists but recommendations not applied - Apply SEO Recommendations
if (hasSEOAnalysis && !seoRecommendationsApplied) {
return { label: 'Apply SEO Recommendations', handler: actionHandlers.onApplySEORecommendations || null };
}
// 3. SEO analysis exists and recommendations applied but no metadata - Generate SEO Metadata
if (hasSEOAnalysis && seoRecommendationsApplied && !hasSEOMetadata) {
return { label: 'Generate SEO Metadata', handler: actionHandlers.onPublishAction || null };
}
break;
case 'publish':
if (hasSEOAnalysis && !hasSEOMetadata) {
return { label: 'Generate SEO Metadata', handler: actionHandlers.onPublishAction || null };
// Only show if SEO metadata exists (ready to publish)
if (hasSEOAnalysis && seoRecommendationsApplied && hasSEOMetadata) {
return { label: 'Ready to Publish', handler: null }; // Publish handled separately
}
break;
}
@@ -97,17 +116,59 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
const isCompleted = phase.completed;
const isDisabled = phase.disabled;
const action = getActionForPhase(phase.id);
// Show action button when:
// 1. CopilotKit is unavailable
// 2. Action handler exists
// 3. Phase is not disabled
// 4. Show for current phase OR next actionable phase (not completed)
// For research phase specifically, always show if no research exists
// 4. Show for current phase OR next actionable phase (not completed) OR phases with available actions
// For research phase: always show if no research exists
// For outline phase: always show if research exists but no outline (like research phase)
// For SEO phase: always show if action handler exists (prerequisites are met)
const isResearchPhase = phase.id === 'research' && !hasResearch;
const showAction = !copilotKitAvailable && action.handler && !isDisabled && (
// Outline phase: show action whenever research exists and action handler is available
// This allows users to create/regenerate outline after research, even if cached one exists
const isOutlinePhase = phase.id === 'outline' && hasResearch && action.handler;
// SEO phase: show action whenever prerequisites are met (action handler exists)
// Similar to research/outline, show SEO actions whenever handler exists and phase is enabled
const isSEOPhase = phase.id === 'seo' && action.handler;
// Debug logging for SEO phase (temporary - for troubleshooting)
if (phase.id === 'seo' && !copilotKitAvailable && process.env.NODE_ENV === 'development') {
console.log('[PhaseNavigation] SEO phase debug:', {
phaseId: phase.id,
isCurrent,
isCompleted,
isDisabled,
hasContent,
contentConfirmed,
hasSEOAnalysis,
seoRecommendationsApplied,
hasSEOMetadata,
actionLabel: action.label,
actionHandler: !!action.handler,
copilotKitAvailable,
isSEOPhase,
showActionWillBe: !copilotKitAvailable && action.handler && !isDisabled && (
isCurrent ||
(!isCompleted && !isDisabled) ||
isResearchPhase ||
isOutlinePhase ||
isSEOPhase
)
});
}
// Show action if: current phase, or phase is not completed and not disabled, or it's research/outline/SEO with available action
// For SEO: show whenever action handler exists (prerequisites are met), even if phase is marked as disabled/completed
// This is critical because SEO prerequisites (hasContent && contentConfirmed) are validated in getActionForPhase,
// so if action.handler exists, we should show it regardless of phase navigation's disabled state
const showAction = !copilotKitAvailable && action.handler && (
isCurrent ||
(!isCompleted && !isDisabled) ||
isResearchPhase
isResearchPhase ||
isOutlinePhase ||
isSEOPhase // Show SEO actions when handler exists - handler existence means prerequisites are met, so ignore isDisabled
);
return (

View File

@@ -0,0 +1,238 @@
import React from 'react';
import { BlogResearchResponse } from '../../../services/blogWriterApi';
interface GoogleSearchModalProps {
research: BlogResearchResponse;
onClose: () => void;
}
export const GoogleSearchModal: React.FC<GoogleSearchModalProps> = ({ research, onClose }) => {
if (!research.search_widget && !research.search_queries?.length) {
return null;
}
const handleSearchClick = (query: string) => {
// Open Google Search in new tab per Google requirements
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
window.open(searchUrl, '_blank', 'noopener,noreferrer');
};
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(4px)'
}}
onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '16px',
padding: '32px',
maxWidth: '800px',
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
border: '1px solid #e5e7eb',
position: 'relative'
}}
onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '24px',
borderBottom: '2px solid #f3f4f6',
paddingBottom: '16px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '48px',
height: '48px',
backgroundColor: '#f8fafc',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #e2e8f0'
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" stroke="#4285F4" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>
<h3 style={{
margin: 0,
color: '#1f2937',
fontSize: '24px',
fontWeight: '700'
}}>
Google Search Suggestions
</h3>
<p style={{
margin: '4px 0 0 0',
color: '#6b7280',
fontSize: '14px'
}}>
Explore related searches and sources
</p>
</div>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '28px',
cursor: 'pointer',
color: '#6b7280',
padding: '8px',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
width: '40px',
height: '40px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f3f4f6';
e.currentTarget.style.color = '#374151';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = '#6b7280';
}}
>
×
</button>
</div>
{/* Google Search Widget - Display exactly as provided per Google requirements */}
{research.search_widget && (
<div style={{
marginBottom: '32px',
width: '100%',
padding: '20px',
backgroundColor: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<div style={{
marginBottom: '12px',
fontSize: '13px',
fontWeight: '600',
color: '#475569',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}>
<span>🔍</span>
<span>Search Suggestions (Click to open in Google)</span>
</div>
{/* Render Google's HTML exactly as provided - no modifications */}
<div
dangerouslySetInnerHTML={{ __html: research.search_widget }}
/>
</div>
)}
{/* Search Queries List */}
{research.search_queries && research.search_queries.length > 0 && (
<div style={{ marginTop: '32px' }}>
<h4 style={{
margin: '0 0 16px 0',
color: '#1f2937',
fontSize: '18px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span>📋</span>
Additional Search Queries
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{research.search_queries.map((query, index) => (
<button
key={index}
onClick={() => handleSearchClick(query)}
style={{
width: '100%',
padding: '12px 16px',
backgroundColor: 'white',
border: '1px solid #d1d5db',
borderRadius: '8px',
cursor: 'pointer',
textAlign: 'left',
fontSize: '14px',
color: '#374151',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f9fafb';
e.currentTarget.style.borderColor = '#4285F4';
e.currentTarget.style.transform = 'translateX(4px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'white';
e.currentTarget.style.borderColor = '#d1d5db';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
<span style={{ flex: 1 }}>{query}</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
<path d="M7 17L17 7M17 7H7M17 7V17" stroke="#4285F4" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
))}
</div>
</div>
)}
{/* Info Footer */}
<div style={{
marginTop: '24px',
padding: '16px',
backgroundColor: '#eff6ff',
borderRadius: '8px',
border: '1px solid #bfdbfe'
}}>
<div style={{
display: 'flex',
alignItems: 'flex-start',
gap: '8px',
fontSize: '13px',
color: '#1e40af'
}}>
<span style={{ fontSize: '16px', lineHeight: '1.5' }}></span>
<div>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>
About These Suggestions
</div>
<div style={{ lineHeight: '1.6' }}>
These search suggestions are generated by Google's AI to help you explore related topics.
Clicking any suggestion will open Google Search in a new tab to find the latest and most relevant information.
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default GoogleSearchModal;

View File

@@ -436,20 +436,7 @@ export const ResearchSources: React.FC<ResearchSourcesProps> = ({ research }) =>
</div>
)}
{/* Google Search Suggestions - Per Google Display Requirements */}
{research.search_widget && (
<div style={{
marginBottom: '24px',
width: '100%',
position: 'relative'
}}>
{/* Google Search Widget - Display exactly as provided without modifications */}
<div
dangerouslySetInnerHTML={{ __html: research.search_widget }}
/>
</div>
)}
{/* Note: Google Search Widget is shown in GoogleSearchModal instead */}
<div style={{
display: 'grid',
gap: '12px',

View File

@@ -1,2 +1,3 @@
export { ResearchSources } from './ResearchSources';
export { ResearchGrounding } from './ResearchGrounding';
export { GoogleSearchModal } from './GoogleSearchModal';

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { BlogResearchResponse } from '../../services/blogWriterApi';
import { ResearchSources, ResearchGrounding } from './ResearchComponents';
import { ResearchSources, ResearchGrounding, GoogleSearchModal } from './ResearchComponents';
interface ResearchResultsProps {
research: BlogResearchResponse;
@@ -10,6 +10,7 @@ export const ResearchResults: React.FC<ResearchResultsProps> = ({ research }) =>
const [showAnglesModal, setShowAnglesModal] = useState(false);
const [showCompetitorModal, setShowCompetitorModal] = useState(false);
const [showGroundingModal, setShowGroundingModal] = useState(false);
const [showSearchModal, setShowSearchModal] = useState(false);
const [showToast, setShowToast] = useState(false);
// Show toast message on component mount
@@ -501,6 +502,38 @@ export const ResearchResults: React.FC<ResearchResultsProps> = ({ research }) =>
>
📝 Use Research Blog Topics
</div>
{/* Google Search Suggestions Chip - Only show when we have search data */}
{(research.search_widget || (research.search_queries && research.search_queries.length > 0)) && (
<div
onClick={() => setShowSearchModal(true)}
style={{
backgroundColor: '#fff8e1',
color: '#f57c00',
border: '1px solid #ffb74d',
borderRadius: '20px',
padding: '6px 16px',
fontSize: '13px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#ffe082';
e.currentTarget.style.transform = 'scale(1.05)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#fff8e1';
e.currentTarget.style.transform = 'scale(1)';
}}
>
🔍 Google Search
</div>
)}
</div>
</div>
@@ -539,6 +572,14 @@ export const ResearchResults: React.FC<ResearchResultsProps> = ({ research }) =>
{renderAnglesModal()}
{renderCompetitorModal()}
{renderGroundingModal()}
{/* Google Search Modal */}
{showSearchModal && (
<GoogleSearchModal
research={research}
onClose={() => setShowSearchModal(false)}
/>
)}
</div>
</>
);