Scheduled research persona generation

This commit is contained in:
ajaysi
2025-11-05 08:51:00 +05:30
parent 55087c4f37
commit d99c7c83a7
98 changed files with 14518 additions and 828 deletions

View File

@@ -29,6 +29,16 @@ import { BlogWriterLandingSection } from './BlogWriterUtils/BlogWriterLandingSec
import { CopilotKitComponents } from './BlogWriterUtils/CopilotKitComponents';
export const BlogWriter: React.FC = () => {
// Add light theme class to body/html on mount, remove on unmount
React.useEffect(() => {
document.body.classList.add('blog-writer-page');
document.documentElement.classList.add('blog-writer-page');
return () => {
document.body.classList.remove('blog-writer-page');
document.documentElement.classList.remove('blog-writer-page');
};
}, []);
// Check CopilotKit health status
const { isAvailable: copilotKitAvailable } = useCopilotKitHealth({
enabled: true, // Enable health checking
@@ -313,6 +323,7 @@ export const BlogWriter: React.FC = () => {
sections,
research,
openSEOMetadata: () => setIsSEOMetadataModalOpen(true),
navigateToPhase,
});
@@ -320,7 +331,14 @@ export const BlogWriter: React.FC = () => {
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#ffffff',
color: '#1a1a1a',
overflow: 'auto'
}} className="blog-writer-container">
{/* CopilotKit-dependent components - extracted to CopilotKitComponents */}
{copilotKitAvailable && (
<CopilotKitComponents
@@ -349,6 +367,7 @@ export const BlogWriter: React.FC = () => {
setFlowAnalysisResults={setFlowAnalysisResults}
setContinuityRefresh={setContinuityRefresh}
researchPolling={researchPolling}
navigateToPhase={navigateToPhase}
/>
)}
@@ -359,6 +378,14 @@ export const BlogWriter: React.FC = () => {
onTaskStart={(taskId) => setOutlineTaskId(taskId)}
onPollingStart={(taskId) => outlinePolling.startPolling(taskId)}
onModalShow={() => setShowOutlineModal(true)}
navigateToPhase={navigateToPhase}
onOutlineCreated={(outline, titleOptions) => {
// Handle cached outline from CopilotKit action (same as header button)
setOutline(outline);
if (titleOptions) {
setTitleOptions(titleOptions);
}
}}
/>
<OutlineRefiner
outline={outline}
@@ -381,31 +408,29 @@ export const BlogWriter: React.FC = () => {
seoMetadata={seoMetadata}
/>
{/* Always show HeaderBar when CopilotKit is unavailable, or when research exists */}
{(!copilotKitAvailable || research) && (
<HeaderBar
phases={phases}
currentPhase={currentPhase}
onPhaseClick={handlePhaseClick}
copilotKitAvailable={copilotKitAvailable}
actionHandlers={{
onResearchAction: handleResearchAction,
onOutlineAction: handleOutlineAction,
onContentAction: handleContentAction,
onSEOAction: handleSEOAction,
onApplySEORecommendations: handleApplySEORecommendations,
onPublishAction: handlePublishAction,
}}
hasResearch={!!research}
hasOutline={outline.length > 0}
outlineConfirmed={outlineConfirmed}
hasContent={Object.keys(sections).length > 0}
contentConfirmed={contentConfirmed}
hasSEOAnalysis={!!seoAnalysis}
seoRecommendationsApplied={seoRecommendationsApplied}
hasSEOMetadata={!!seoMetadata}
/>
)}
{/* Phase navigation header - always visible as default interface */}
<HeaderBar
phases={phases}
currentPhase={currentPhase}
onPhaseClick={handlePhaseClick}
copilotKitAvailable={copilotKitAvailable}
actionHandlers={{
onResearchAction: handleResearchAction,
onOutlineAction: handleOutlineAction,
onContentAction: handleContentAction,
onSEOAction: handleSEOAction,
onApplySEORecommendations: handleApplySEORecommendations,
onPublishAction: handlePublishAction,
}}
hasResearch={!!research}
hasOutline={outline.length > 0}
outlineConfirmed={outlineConfirmed}
hasContent={Object.keys(sections).length > 0}
contentConfirmed={contentConfirmed}
hasSEOAnalysis={!!seoAnalysis}
seoRecommendationsApplied={seoRecommendationsApplied}
hasSEOMetadata={!!seoMetadata}
/>
{/* Landing section - extracted to BlogWriterLandingSection */}
<BlogWriterLandingSection

View File

@@ -70,21 +70,12 @@ const BlogWriterLanding: React.FC<BlogWriterLandingProps> = ({ onStartWriting })
backgroundSize: '56% auto',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
backgroundColor: 'transparent',
backgroundColor: '#ffffff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden'
}}>
{/* Animated overlay for subtle movement */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(135deg, rgba(25, 118, 210, 0.05) 0%, rgba(156, 39, 176, 0.05) 100%)'
}} />
{/* Main content container */}
<div style={{
@@ -109,7 +100,7 @@ const BlogWriterLanding: React.FC<BlogWriterLandingProps> = ({ onStartWriting })
textShadow: '0 4px 8px rgba(0,0,0,0.1)',
lineHeight: '1.2'
}}>
Step1- Research Your Blog Topic
AI-First, Contextual, Click through Blog Writer
</h1>
</div>

View File

@@ -17,27 +17,24 @@ export const BlogWriterLandingSection: React.FC<BlogWriterLandingSectionProps> =
navigateToPhase,
onResearchComplete,
}) => {
// Only show landing/initial content when no research exists
// Phase navigation header is always visible, so this is just the initial content
if (!research) {
return (
<>
{/* Show manual research form when on research phase and CopilotKit unavailable */}
{!copilotKitAvailable && currentPhase === 'research' && (
<ManualResearchForm onResearchComplete={onResearchComplete} />
)}
{copilotKitAvailable && (
{/* Show landing page for CopilotKit flow or when not on research phase */}
{(!copilotKitAvailable && currentPhase !== 'research') || copilotKitAvailable ? (
<BlogWriterLanding
onStartWriting={() => {
// Trigger the copilot to start the research process
}}
/>
)}
{!copilotKitAvailable && currentPhase !== 'research' && (
<BlogWriterLanding
onStartWriting={() => {
// Navigate to research phase when CopilotKit unavailable
// Navigate to research phase to start the workflow
navigateToPhase('research');
}}
/>
)}
) : null}
</>
);
}

View File

@@ -27,6 +27,7 @@ interface CopilotKitComponentsProps {
setFlowAnalysisResults: (results: any) => void;
setContinuityRefresh: (refresh: number | ((prev: number) => number)) => void;
researchPolling: any;
navigateToPhase?: (phase: string) => void;
}
export const CopilotKitComponents: React.FC<CopilotKitComponentsProps> = ({
@@ -49,6 +50,7 @@ export const CopilotKitComponents: React.FC<CopilotKitComponentsProps> = ({
setFlowAnalysisResults,
setContinuityRefresh,
researchPolling,
navigateToPhase,
}) => {
return (
<>
@@ -57,12 +59,13 @@ export const CopilotKitComponents: React.FC<CopilotKitComponentsProps> = ({
onTaskStart={(taskId) => researchPolling.startPolling(taskId)}
/>
<CustomOutlineForm onOutlineCreated={onOutlineCreated} />
<ResearchAction onResearchComplete={onResearchComplete} />
<ResearchAction onResearchComplete={onResearchComplete} navigateToPhase={navigateToPhase} />
<ResearchDataActions
research={research}
onOutlineCreated={onOutlineCreated}
onTitleOptionsSet={onTitleOptionsSet}
onTitleOptionsSet={onTitleOptionsSet}
navigateToPhase={navigateToPhase}
/>
<EnhancedOutlineActions
outline={outline}
@@ -77,6 +80,7 @@ export const CopilotKitComponents: React.FC<CopilotKitComponentsProps> = ({
onMediumGenerationTriggered={onMediumGenerationTriggered}
sections={sections}
blogTitle={selectedTitle ?? undefined}
navigateToPhase={navigateToPhase}
onFlowAnalysisComplete={(analysis) => {
console.log('Flow analysis completed:', analysis);
setFlowAnalysisCompleted(true);

View File

@@ -16,14 +16,242 @@ export const WriterCopilotSidebar: React.FC<WriterCopilotSidebarProps> = ({
outlineConfirmed,
}) => {
return (
<CopilotSidebar
labels={{
title: 'ALwrity Co-Pilot',
initial: !research
? 'Hi! I can help you research, outline, and draft your blog. Just tell me what topic you want to write about and I\'ll get started!'
: 'Great! I can see you have research data. Let me help you create an outline and generate content for your blog.',
}}
suggestions={suggestions}
<>
<style>{`
/* Enterprise CopilotKit Suggestion Styling */
/* All suggestion chips - base styling */
.copilotkit-suggestions button,
.copilot-suggestions button,
[class*="suggestion"] button,
[class*="Suggestion"] button {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 12px;
border: 1px solid rgba(99, 102, 241, 0.2);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(249, 250, 251, 0.98) 100%);
color: #4b5563;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(255, 255, 255, 0.5) inset;
letter-spacing: 0.01em;
}
/* Shine effect on hover */
.copilotkit-suggestions button::before,
.copilot-suggestions button::before,
[class*="suggestion"] button::before,
[class*="Suggestion"] button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
transition: left 0.6s ease;
}
.copilotkit-suggestions button:hover::before,
.copilot-suggestions button:hover::before,
[class*="suggestion"] button:hover::before,
[class*="Suggestion"] button:hover::before {
left: 100%;
}
/* Regular suggestions - hover effects */
.copilotkit-suggestions button:hover,
.copilot-suggestions button:hover,
[class*="suggestion"] button:hover:not([class*="next-suggestion"]),
[class*="Suggestion"] button:hover:not([class*="next-suggestion"]) {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.6) inset;
border-color: rgba(99, 102, 241, 0.3);
background: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(249, 250, 251, 1) 100%);
}
/* "Next:" Suggestions - Premium Enterprise Style */
.copilotkit-suggestions button[data-is-next="true"],
.copilot-suggestions button[data-is-next="true"],
.copilotkit-suggestions button.next-suggestion,
.copilot-suggestions button.next-suggestion,
.copilotkit-suggestions button[aria-label*="Next:"],
.copilot-suggestions button[aria-label*="Next:"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%) !important;
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2) inset,
0 2px 4px rgba(0, 0, 0, 0.1) inset,
0 0 20px rgba(102, 126, 234, 0.3) !important;
font-weight: 700 !important;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
animation: nextSuggestionPulse 3s ease-in-out infinite;
}
/* Pulse animation for Next suggestions */
@keyframes nextSuggestionPulse {
0%, 100% {
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2) inset,
0 2px 4px rgba(0, 0, 0, 0.1) inset,
0 0 20px rgba(102, 126, 234, 0.3);
}
50% {
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.25) inset,
0 2px 4px rgba(0, 0, 0, 0.1) inset,
0 0 30px rgba(102, 126, 234, 0.5);
}
}
/* Next suggestion hover - enhanced */
.copilotkit-suggestions button[data-is-next="true"]:hover,
.copilot-suggestions button[data-is-next="true"]:hover,
.copilotkit-suggestions button.next-suggestion:hover,
.copilot-suggestions button.next-suggestion:hover,
.copilotkit-suggestions button[aria-label*="Next:"]:hover,
.copilot-suggestions button[aria-label*="Next:"]:hover {
transform: translateY(-3px) scale(1.05) !important;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
0 3px 6px rgba(0, 0, 0, 0.15) inset,
0 0 40px rgba(102, 126, 234, 0.6) !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 40%, #f093fb 60%, #4facfe 100%) !important;
animation: none;
}
/* Next suggestion active */
.copilotkit-suggestions button[data-is-next="true"]:active,
.copilot-suggestions button[data-is-next="true"]:active,
.copilotkit-suggestions button.next-suggestion:active,
.copilot-suggestions button.next-suggestion:active,
.copilotkit-suggestions button[aria-label*="Next:"]:active,
.copilot-suggestions button[aria-label*="Next:"]:active {
transform: translateY(-1px) scale(1.02) !important;
box-shadow: 0 3px 12px rgba(102, 126, 234, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.25) inset,
0 1px 3px rgba(0, 0, 0, 0.1) inset !important;
}
/* Next suggestion focus */
.copilotkit-suggestions button[data-is-next="true"]:focus-visible,
.copilot-suggestions button[data-is-next="true"]:focus-visible,
.copilotkit-suggestions button.next-suggestion:focus-visible,
.copilot-suggestions button.next-suggestion:focus-visible,
.copilotkit-suggestions button[aria-label*="Next:"]:focus-visible,
.copilot-suggestions button[aria-label*="Next:"]:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.4),
0 4px 16px rgba(102, 126, 234, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2) inset,
0 0 30px rgba(102, 126, 234, 0.5) !important;
}
/* Match buttons by text content using data attributes or class */
/* We'll inject a data attribute via JS to identify Next suggestions */
/* Regular suggestion active state */
.copilotkit-suggestions button:active:not([data-is-next="true"]):not(.next-suggestion),
.copilot-suggestions button:active:not([data-is-next="true"]):not(.next-suggestion) {
transform: translateY(0) scale(0.98);
box-shadow: 0 2px 6px rgba(99, 102, 241, 0.15);
}
/* Focus states for regular suggestions */
.copilotkit-suggestions button:focus-visible:not([data-is-next="true"]):not(.next-suggestion),
.copilot-suggestions button:focus-visible:not([data-is-next="true"]):not(.next-suggestion) {
outline: none;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3), 0 4px 12px rgba(99, 102, 241, 0.2);
}
/* Enhanced suggestion container */
.copilotkit-suggestions,
.copilot-suggestions {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
margin: 16px 0;
padding: 12px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(249, 250, 251, 0.6) 100%);
border-radius: 12px;
backdrop-filter: blur(8px);
}
@media (min-width: 420px) {
.copilotkit-suggestions,
.copilot-suggestions {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
}
/* Smooth transitions */
* {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
`}</style>
{/* Inject data attributes to identify Next suggestions */}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const observer = new MutationObserver(() => {
const suggestionButtons = document.querySelectorAll(
'.copilotkit-suggestions button, .copilot-suggestions button, [class*="suggestion"] button'
);
suggestionButtons.forEach(btn => {
const text = btn.textContent || btn.innerText || '';
if (text.includes('Next:')) {
btn.setAttribute('data-is-next', 'true');
btn.classList.add('next-suggestion');
} else {
btn.removeAttribute('data-is-next');
btn.classList.remove('next-suggestion');
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Initial run
setTimeout(() => {
const suggestionButtons = document.querySelectorAll(
'.copilotkit-suggestions button, .copilot-suggestions button, [class*="suggestion"] button'
);
suggestionButtons.forEach(btn => {
const text = btn.textContent || btn.innerText || '';
if (text.includes('Next:')) {
btn.setAttribute('data-is-next', 'true');
btn.classList.add('next-suggestion');
}
});
}, 100);
})();
`
}}
/>
<CopilotSidebar
labels={{
title: 'ALwrity Co-Pilot',
initial: !research
? 'Hi! I can help you research, outline, and draft your blog. Just tell me what topic you want to write about and I\'ll get started!'
: 'Great! I can see you have research data. Let me help you create an outline and generate content for your blog.',
}}
suggestions={suggestions}
makeSystemMessage={(context: string, additional?: string) => {
const hasResearch = research !== null && research !== undefined;
const hasOutline = outline && outline.length > 0;
@@ -132,6 +360,7 @@ Available tools:
return [toolGuide, additional].filter(Boolean).join('\n\n');
}}
/>
</>
);
};

View File

@@ -14,6 +14,7 @@ interface UseBlogWriterCopilotActionsParams {
sections: Record<string, string>;
research: any;
openSEOMetadata: OpenMetadataCb;
navigateToPhase?: (phase: string) => void;
}
// Consolidates all Copilot actions used by BlogWriter
@@ -25,6 +26,7 @@ export function useBlogWriterCopilotActions({
sections,
research,
openSEOMetadata,
navigateToPhase,
}: UseBlogWriterCopilotActionsParams) {
// Maintain the same any-cast pattern for parity with component
const useCopilotActionTyped = useCopilotAction as any;
@@ -35,6 +37,8 @@ export function useBlogWriterCopilotActions({
description: 'Confirm that the blog content is ready and move to the next stage (SEO analysis)',
parameters: [],
handler: async () => {
// Navigate to SEO phase when content is confirmed
navigateToPhase?.('seo');
const msg = await confirmBlogContent();
return msg;
},
@@ -46,6 +50,9 @@ export function useBlogWriterCopilotActions({
description: 'Analyze the blog content for SEO optimization and provide detailed recommendations',
parameters: [],
handler: async () => {
// Navigate to SEO phase when SEO analysis starts
navigateToPhase?.('seo');
debug.log('[BlogWriter] SEO analysis action', {
modalOpen: isSEOAnalysisModalOpen,
hasSections: !!sections && Object.keys(sections).length > 0,
@@ -73,6 +80,9 @@ export function useBlogWriterCopilotActions({
},
],
handler: async ({ title }: { title?: string }) => {
// Navigate to SEO phase when SEO metadata generation starts
navigateToPhase?.('seo');
if (!sections || Object.keys(sections).length === 0) {
return 'Please generate blog content first before creating SEO metadata. Use the content generation features to create your blog post.';
}

View File

@@ -13,27 +13,15 @@ interface KeywordInputFormProps {
}
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete, onTaskStart }) => {
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
// This component now only provides polling functionality
// The keyword input form is handled by ResearchAction component
return (
<>
{/* Polling handler for research progress */}
<ResearchPollingHandler
taskId={currentTaskId}
onResearchComplete={(result) => {
onResearchComplete?.(result);
setCurrentTaskId(null);
}}
onError={(error) => {
console.error('Research error:', error);
setCurrentTaskId(null);
}}
/>
</>
);
// This component is now a lightweight wrapper
// The actual keyword input form is handled by ResearchAction component
// Polling is handled by ResearchPollingHandler in ResearchAction
// This component exists for backward compatibility but doesn't create unnecessary polling hooks
// Note: If onTaskStart is called, it should use the researchPolling from parent
// (passed via CopilotKitComponents), not create a new polling instance here
return null; // No UI needed - ResearchAction handles everything
};
export default KeywordInputForm;

View File

@@ -50,6 +50,7 @@ interface OutlineFeedbackFormProps {
sections?: Record<string, string>;
blogTitle?: string;
onFlowAnalysisComplete?: (analysis: any) => void;
navigateToPhase?: (phase: string) => void;
}
@@ -225,7 +226,8 @@ const FeedbackForm: React.FC<{
export const OutlineFeedbackForm: React.FC<OutlineFeedbackFormProps> = ({
outline,
research,
research,
navigateToPhase,
onOutlineConfirmed,
onOutlineRefined,
onMediumGenerationStarted,
@@ -352,6 +354,9 @@ export const OutlineFeedbackForm: React.FC<OutlineFeedbackFormProps> = ({
}
try {
// Navigate to content phase when outline is confirmed
navigateToPhase?.('content');
onOutlineConfirmed();
// If research specifies a short/medium blog (<=1000), kick off medium generation

View File

@@ -8,6 +8,8 @@ interface OutlineGeneratorProps {
onTaskStart: (taskId: string) => void;
onPollingStart: (taskId: string) => void;
onModalShow?: () => void; // Callback to show progress modal immediately
navigateToPhase?: (phase: string) => void;
onOutlineCreated?: (outline: any[], titleOptions?: any[]) => void; // Callback when outline is created/found (for cached outlines)
}
const useCopilotActionTyped = useCopilotAction as any;
@@ -16,7 +18,9 @@ export const OutlineGenerator = forwardRef<any, OutlineGeneratorProps>(({
research,
onTaskStart,
onPollingStart,
onModalShow
onModalShow,
navigateToPhase,
onOutlineCreated
}, ref) => {
// Expose an imperative method to trigger outline generation directly (bypass LLM)
useImperativeHandle(ref, () => ({
@@ -67,6 +71,15 @@ export const OutlineGenerator = forwardRef<any, OutlineGeneratorProps>(({
if (cachedOutline) {
console.log('[OutlineGenerator] Using cached outline from CopilotKit action', { sections: cachedOutline.outline.length });
// Navigate to outline phase when cached outline is found
navigateToPhase?.('outline');
// Update parent state with cached outline (same as header button does)
if (onOutlineCreated) {
onOutlineCreated(cachedOutline.outline, cachedOutline.title_options);
}
return {
success: true,
message: `✅ Outline already exists! ${cachedOutline.outline.length} sections loaded from cache.`,
@@ -77,6 +90,9 @@ export const OutlineGenerator = forwardRef<any, OutlineGeneratorProps>(({
}
try {
// Navigate to outline phase when outline generation starts
navigateToPhase?.('outline');
// Show progress modal immediately when user clicks "Create outline"
onModalShow?.();

View File

@@ -51,9 +51,16 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
seoRecommendationsApplied = false,
hasSEOMetadata = false,
}) => {
// Phase Navigation: Default interface for blog writing workflow
// - Phase buttons are always clickable and functional (for both CopilotKit and manual flows)
// - Action buttons (▶) only appear when CopilotKit is unavailable (manual fallback)
// - When CopilotKit is available, users can use either phase buttons or CopilotKit suggestions
// Determine which action to show for each phase when CopilotKit is unavailable
const getActionForPhase = (phaseId: string): { label: string; handler: (() => void) | null } => {
if (copilotKitAvailable || !actionHandlers) {
// Show action buttons for both CopilotKit and manual flows (dual mode)
// Users can use either CopilotKit suggestions or phase navigation buttons
if (!actionHandlers) {
return { label: '', handler: null };
}
@@ -104,159 +111,317 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
};
return (
<div style={{
display: 'flex',
gap: '8px',
alignItems: 'center',
padding: '8px 0',
flexWrap: 'wrap'
}}>
{phases.map((phase) => {
const isCurrent = phase.current;
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) 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;
// 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
)
});
<>
<style>{`
/* Enterprise Phase Navigation Styles */
.phase-nav-container {
display: flex;
gap: 10px;
alignItems: center;
padding: 12px 0;
flexWrap: wrap;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(249, 250, 251, 0.98) 100%);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
// 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 ||
isOutlinePhase ||
isSEOPhase // Show SEO actions when handler exists - handler existence means prerequisites are met, so ignore isDisabled
);
.phase-chip {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: 24px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
letter-spacing: 0.01em;
}
return (
<div key={phase.id} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<button
onClick={() => !isDisabled && onPhaseClick(phase.id)}
disabled={isDisabled}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 12px',
borderRadius: '20px',
border: 'none',
fontSize: '14px',
fontWeight: '500',
cursor: isDisabled ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
backgroundColor: isCurrent
? '#1976d2'
: isCompleted
? '#4caf50'
: isDisabled
? '#f5f5f5'
: '#e3f2fd',
color: isCurrent
? 'white'
: isCompleted
? 'white'
: isDisabled
? '#999'
: '#1976d2',
opacity: isDisabled ? 0.6 : 1,
boxShadow: isCurrent ? '0 2px 4px rgba(25, 118, 210, 0.3)' : 'none',
transform: isCurrent ? 'translateY(-1px)' : 'none'
}}
title={phase.disabled ? `Complete ${phase.name} first` : phase.description}
>
<span style={{ fontSize: '16px' }}>
{phase.icon}
</span>
<span>{phase.name}</span>
{isCompleted && !isCurrent && (
<span style={{ fontSize: '12px', marginLeft: '4px' }}>
</span>
)}
</button>
{showAction && (
.phase-chip::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
.phase-chip:hover::before {
left: 100%;
}
/* Current Phase - Active Gradient */
.phase-chip.current {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
transform: translateY(-2px) scale(1.02);
}
.phase-chip.current:hover {
transform: translateY(-3px) scale(1.03);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.2) inset;
}
.phase-chip.current:active {
transform: translateY(-1px) scale(1.01);
box-shadow: 0 3px 12px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.15) inset;
}
/* Completed Phase - Success Gradient */
.phase-chip.completed {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
box-shadow: 0 3px 12px rgba(16, 185, 129, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
}
.phase-chip.completed:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 5px 16px rgba(16, 185, 129, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.15) inset;
}
/* Pending Phase - Subtle Gradient */
.phase-chip.pending {
background: linear-gradient(135deg, #e0e7ff 0%, #dbeafe 100%);
color: #4b5563;
border: 1px solid rgba(99, 102, 241, 0.2);
box-shadow: 0 2px 6px rgba(99, 102, 241, 0.1);
}
.phase-chip.pending:hover {
background: linear-gradient(135deg, #c7d2fe 0%, #bfdbfe 100%);
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
border-color: rgba(99, 102, 241, 0.3);
}
/* Disabled Phase */
.phase-chip.disabled {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
color: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
border: 1px solid #e5e7eb;
}
.phase-chip.disabled:hover {
transform: none;
box-shadow: none;
}
/* Phase Icon */
.phase-icon {
font-size: 18px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
transition: transform 0.3s ease;
}
.phase-chip.current .phase-icon,
.phase-chip.completed .phase-icon {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.phase-chip:hover:not(.disabled) .phase-icon {
transform: scale(1.1) rotate(5deg);
}
/* Checkmark for completed */
.phase-checkmark {
font-size: 14px;
margin-left: 4px;
animation: checkmarkPop 0.3s ease;
}
@keyframes checkmarkPop {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
/* Action Button - Enterprise Style */
.phase-action-btn {
position: relative;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 20px;
border: none;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.5px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.1) inset,
0 1px 2px rgba(0, 0, 0, 0.1) inset;
}
.phase-action-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.25), transparent);
transition: left 0.5s ease;
}
.phase-action-btn:hover::before {
left: 100%;
}
.phase-action-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.2) inset,
0 2px 4px rgba(0, 0, 0, 0.15) inset;
}
.phase-action-btn:active {
transform: translateY(0) scale(1.02);
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.15) inset;
}
.phase-action-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3),
0 4px 12px rgba(102, 126, 234, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
}
.phase-action-icon {
font-size: 12px;
transition: transform 0.3s ease;
}
.phase-action-btn:hover .phase-action-icon {
transform: translateX(2px);
}
`}</style>
<div className="phase-nav-container">
{phases.map((phase) => {
const isCurrent = phase.current;
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) 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;
// 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
// DUAL MODE: Show action buttons even when CopilotKit is available (users can use either method)
const showAction = action.handler && (
isCurrent ||
(!isCompleted && !isDisabled) ||
isResearchPhase ||
isOutlinePhase ||
isSEOPhase // Show SEO actions when handler exists - handler existence means prerequisites are met, so ignore isDisabled
);
// Determine chip class
const chipClass = [
'phase-chip',
isCurrent ? 'current' : '',
isCompleted && !isCurrent ? 'completed' : '',
!isCurrent && !isCompleted && !isDisabled ? 'pending' : '',
isDisabled ? 'disabled' : ''
].filter(Boolean).join(' ');
return (
<div key={phase.id} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
onClick={(e) => {
e.stopPropagation();
action.handler?.();
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '6px 12px',
borderRadius: '16px',
border: '1px solid #1976d2',
fontSize: '12px',
fontWeight: '600',
cursor: 'pointer',
backgroundColor: '#1976d2',
color: 'white',
transition: 'all 0.2s ease',
boxShadow: '0 2px 4px rgba(25, 118, 210, 0.2)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#1565c0';
e.currentTarget.style.transform = 'translateY(-1px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#1976d2';
e.currentTarget.style.transform = 'none';
}}
title={`${action.label} (Chat unavailable - click to proceed)`}
onClick={() => !isDisabled && onPhaseClick(phase.id)}
disabled={isDisabled}
className={chipClass}
title={phase.disabled ? `Complete ${phase.name} first` : phase.description}
>
<span style={{ fontSize: '12px' }}></span>
<span>{action.label}</span>
<span className="phase-icon">
{phase.icon}
</span>
<span>{phase.name}</span>
{isCompleted && !isCurrent && (
<span className="phase-checkmark">
</span>
)}
</button>
)}
</div>
);
})}
</div>
{showAction && (
<button
onClick={(e) => {
e.stopPropagation();
action.handler?.();
}}
className="phase-action-btn"
title={`${action.label}`}
>
<span className="phase-action-icon"></span>
<span>{action.label}</span>
</button>
)}
</div>
);
})}
</div>
</>
);
};

View File

@@ -62,8 +62,12 @@ export const Publisher: React.FC<PublisherProps> = ({
try {
const status = await wordpressAPI.getStatus();
setWordpressSites(status.sites || []);
} catch (error) {
console.error('Failed to check WordPress connection status:', error);
} catch (error: any) {
// getStatus now handles 404 gracefully, so we should rarely hit this
// Only log non-404 errors
if (error?.response?.status !== 404) {
console.error('Failed to check WordPress connection status:', error);
}
setWordpressSites([]);
} finally {
setCheckingWordPressStatus(false);

View File

@@ -9,9 +9,10 @@ const useCopilotActionTyped = useCopilotAction as any;
interface ResearchActionProps {
onResearchComplete?: (research: BlogResearchResponse) => void;
navigateToPhase?: (phase: string) => void;
}
export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComplete }) => {
export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComplete, navigateToPhase }) => {
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
const [currentMessage, setCurrentMessage] = useState<string>('');
const [showProgressModal, setShowProgressModal] = useState<boolean>(false);
@@ -20,28 +21,36 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
// Refs for form inputs (uncontrolled, avoids typing issues inside Copilot render)
const keywordsRef = useRef<HTMLInputElement | null>(null);
const blogLengthRef = useRef<HTMLSelectElement | null>(null);
// Track if we've navigated to research phase for this form display
const hasNavigatedRef = useRef<boolean>(false);
const polling = useResearchPolling({
onProgress: (message) => {
setCurrentMessage(message);
setForceUpdate(prev => prev + 1); // Force re-render
},
onComplete: (result) => {
if (result && result.keywords) {
researchCache.cacheResult(
result.keywords,
result.industry || 'General',
result.target_audience || 'General',
result
);
}
onResearchComplete?.(result);
setCurrentTaskId(null);
setCurrentMessage('');
setShowProgressModal(false);
setForceUpdate(prev => prev + 1);
},
onComplete: (result) => {
console.info('[ResearchAction] ✅ Research completed', { hasResult: !!result });
if (result && result.keywords) {
researchCache.cacheResult(
result.keywords,
result.industry || 'General',
result.target_audience || 'General',
result
);
}
// Reset navigation tracking when research completes
hasNavigatedRef.current = false;
onResearchComplete?.(result);
setCurrentTaskId(null);
setCurrentMessage('');
setShowProgressModal(false);
setForceUpdate(prev => prev + 1);
},
onError: (error) => {
console.error('Research polling error:', error);
setCurrentTaskId(null);
@@ -55,14 +64,40 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
name: 'showResearchForm',
description: 'Show keyword input form for blog research',
parameters: [],
handler: async () => ({
success: true,
message: "🔍 Let's Research Your Blog Topic\n\nWhat keywords and information would you like to use for your research? Please also specify the desired length of the blog post.\n\nKeywords or Topic *\ne.g., artificial intelligence, machine learning, AI trends\n\nBlog Length (words)\n\n1000 words (Medium blog)\n\n🚀 Start Research",
showForm: true
}),
handler: async () => {
// Navigate to research phase when research form is shown
// Reset navigation tracking so form render can navigate again if needed
hasNavigatedRef.current = false;
// Navigate immediately when handler is called
if (navigateToPhase) {
navigateToPhase('research');
}
return {
success: true,
message: "🔍 Let's Research Your Blog Topic\n\nWhat keywords and information would you like to use for your research? Please also specify the desired length of the blog post.\n\nKeywords or Topic *\ne.g., artificial intelligence, machine learning, AI trends\n\nBlog Length (words)\n\n1000 words (Medium blog)\n\n🚀 Start Research",
showForm: true
};
},
render: ({ status }: any) => {
const _ = forceUpdate;
// Navigate to research phase when form is rendered (if not already navigated and form is shown)
// This ensures phase navigation updates when CopilotKit shows the research form
// Only navigate when showing the form (not progress or completion states)
const isShowingForm = polling.currentStatus !== 'completed' &&
polling.currentStatus !== 'in_progress' &&
polling.currentStatus !== 'running';
if (isShowingForm && !hasNavigatedRef.current && navigateToPhase) {
// Use setTimeout to avoid calling during render
setTimeout(() => {
if (!hasNavigatedRef.current) {
navigateToPhase('research');
hasNavigatedRef.current = true;
}
}, 0);
}
if (polling.currentStatus === 'completed' && polling.progressMessages.length > 0) {
const latestMessage = polling.progressMessages[polling.progressMessages.length - 1];
return (
@@ -135,6 +170,8 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
target_audience: 'General',
word_count_target: parseInt(blogLength)
};
// Navigate to research phase when research starts
navigateToPhase?.('research');
const { task_id } = await blogWriterApi.startResearch(payload);
setCurrentTaskId(task_id);
setShowProgressModal(true);
@@ -173,6 +210,8 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
const keywordList = trimmed.includes(',')
? trimmed.split(',').map((k: string) => k.trim()).filter(Boolean)
: [trimmed];
// Navigate to research phase when research starts
navigateToPhase?.('research');
const payload: BlogResearchRequest = {
keywords: keywordList,
industry,
@@ -191,6 +230,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
}
});
return (
<>
{showProgressModal && (

View File

@@ -8,12 +8,14 @@ interface ResearchDataActionsProps {
research: BlogResearchResponse | null;
onOutlineCreated: (outline: BlogOutlineSection[]) => void;
onTitleOptionsSet: (titles: string[]) => void;
navigateToPhase?: (phase: string) => void;
}
export const ResearchDataActions: React.FC<ResearchDataActionsProps> = ({
research,
onOutlineCreated,
onTitleOptionsSet
onTitleOptionsSet,
navigateToPhase
}) => {
// Chat with Research Data
useCopilotActionTyped({
@@ -110,6 +112,9 @@ export const ResearchDataActions: React.FC<ResearchDataActionsProps> = ({
}
try {
// Navigate to outline phase when outline generation starts
navigateToPhase?.('outline');
// Create a custom outline request with user instructions
const customOutlineRequest = {
research: research,

View File

@@ -51,16 +51,13 @@ export const ResearchPollingHandler: React.FC<ResearchPollingHandlerProps> = ({
if (taskId) {
polling.startPolling(taskId);
} else {
polling.stopPolling();
// Only stop if actually polling (not on every render when taskId is null)
if (polling.isPolling) {
polling.stopPolling();
}
}
}, [taskId, polling]);
// Cleanup on unmount
useEffect(() => {
return () => {
polling.stopPolling();
};
}, [polling]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [taskId]); // Removed polling from dependencies - usePolling already handles cleanup
// Only log on meaningful changes
useEffect(() => {

View File

@@ -183,7 +183,12 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
const [isApplying, setIsApplying] = useState(false);
const [applyError, setApplyError] = useState<string | null>(null);
console.log('SEOAnalysisModal render:', { isOpen, blogContent: blogContent?.length, researchData: !!researchData });
// Debug logging only in development and when modal state changes meaningfully
useEffect(() => {
if (process.env.NODE_ENV === 'development' && isOpen) {
console.log('SEOAnalysisModal render:', { isOpen, blogContent: blogContent?.length, researchData: !!researchData });
}
}, [isOpen, blogContent?.length, researchData]);
const runSEOAnalysis = useCallback(async (forceRefresh = false) => {
try {
@@ -318,7 +323,7 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
status,
data: err?.response?.data
});
const handled = triggerSubscriptionError(err);
const handled = await triggerSubscriptionError(err);
if (handled) {
console.log('SEOAnalysisModal: Global subscription error handler triggered successfully');
// Don't set local error - let the global modal handle it

View File

@@ -125,15 +125,17 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
return unsub;
}, [metadataResult]);
// Debug logging
// Debug logging only in development and when modal state changes meaningfully
useEffect(() => {
console.log('🔍 SEOMetadataModal render:', {
isOpen,
blogContent: blogContent?.length,
blogTitle,
researchData: !!researchData
});
}, [isOpen, blogContent, blogTitle, researchData]);
if (process.env.NODE_ENV === 'development' && isOpen) {
console.log('🔍 SEOMetadataModal render:', {
isOpen,
blogContent: blogContent?.length,
blogTitle,
researchData: !!researchData
});
}
}, [isOpen, blogContent?.length, blogTitle, researchData]);
// Reset state when modal closes
useEffect(() => {
@@ -229,7 +231,7 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
status,
data: err?.response?.data
});
const handled = triggerSubscriptionError(err);
const handled = await triggerSubscriptionError(err);
if (handled) {
console.log('SEOMetadataModal: Global subscription error handler triggered successfully');
// Don't set local error - let the global modal handle it

View File

@@ -8,6 +8,7 @@ interface SectionGeneratorProps {
genMode: 'draft' | 'polished';
onSectionGenerated: (sectionId: string, markdown: string) => void;
onContinuityRefresh: () => void;
navigateToPhase?: (phase: string) => void;
}
const useCopilotActionTyped = useCopilotAction as any;
@@ -17,7 +18,8 @@ export const SectionGenerator: React.FC<SectionGeneratorProps> = ({
research,
genMode,
onSectionGenerated,
onContinuityRefresh
onContinuityRefresh,
navigateToPhase
}) => {
useCopilotActionTyped({
name: 'generateSection',
@@ -27,6 +29,9 @@ export const SectionGenerator: React.FC<SectionGeneratorProps> = ({
const section = outline.find(s => s.id === sectionId);
if (!section) return { success: false, message: 'Section not found. Please generate an outline first.' };
// Navigate to content phase when content generation starts
navigateToPhase?.('content');
try {
const res = await blogWriterApi.generateSection({ section, mode: genMode });
if (res?.markdown) {
@@ -98,6 +103,9 @@ export const SectionGenerator: React.FC<SectionGeneratorProps> = ({
description: 'Generate content for every section in the outline',
parameters: [],
handler: async () => {
// Navigate to content phase when content generation starts
navigateToPhase?.('content');
for (const s of outline) {
const res = await blogWriterApi.generateSection({ section: s, mode: genMode });
onSectionGenerated(s.id, res.markdown);