AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.

This commit is contained in:
ajaysi
2026-01-10 19:32:50 +05:30
parent 0b63ae7fc1
commit 8193cdba67
298 changed files with 45678 additions and 10952 deletions

View File

@@ -275,6 +275,12 @@ export const BlogWriter: React.FC = () => {
const handlePhaseClick = useCallback((phaseId: string) => {
navigateToPhase(phaseId);
// When clicking Research phase, ensure we navigate to research phase (this will trigger research form to show)
if (phaseId === 'research' && !research) {
debug.log('[BlogWriter] Research phase clicked - navigating to research phase to show form');
// navigateToPhase already called above, which will set currentPhase to 'research'
// BlogWriterLandingSection will detect currentPhase === 'research' and show ManualResearchForm
}
if (phaseId === 'seo') {
if (seoAnalysis) {
setIsSEOAnalysisModalOpen(true);
@@ -283,7 +289,7 @@ export const BlogWriter: React.FC = () => {
runSEOAnalysisDirect();
}
}
}, [navigateToPhase, seoAnalysis, runSEOAnalysisDirect, setIsSEOAnalysisModalOpen]);
}, [navigateToPhase, seoAnalysis, research, runSEOAnalysisDirect, setIsSEOAnalysisModalOpen]);
const outlineGenRef = useRef<any>(null);

View File

@@ -0,0 +1,245 @@
/**
* Blog Writer Cost Alerts Integration
*
* Example integration of Priority 2 alerts (cost estimation, trends, OSS recommendations)
* into the Blog Writer component.
*/
import React, { useEffect } from 'react';
import { Box, Alert, AlertTitle, Button, Collapse } from '@mui/material';
import { usePriority2Alerts, useCostEstimationAlert } from '../../../hooks/usePriority2Alerts';
import Priority2AlertBanner from '../../shared/Priority2AlertBanner';
import { useSubscription } from '../../../contexts/SubscriptionContext';
import { checkPreflight, PreflightOperation } from '../../../services/billingService';
import { showToastNotification } from '../../../utils/toastNotifications';
interface BlogWriterCostAlertsProps {
userId?: string;
onResearchStart?: () => void;
onOutlineStart?: () => void;
onContentStart?: () => void;
}
/**
* Blog Writer Cost Alerts Component
*
* Displays Priority 2 alerts and provides cost estimation before operations.
* Integrates with Blog Writer's research, outline, and content generation workflows.
*/
export const BlogWriterCostAlerts: React.FC<BlogWriterCostAlertsProps> = ({
userId,
onResearchStart,
onOutlineStart,
onContentStart,
}) => {
const { subscription } = useSubscription();
const { alerts, refreshAlerts, dismissAlert } = usePriority2Alerts({
userId,
enabled: !!userId && subscription?.active,
checkInterval: 120000, // Check every 2 minutes
});
const { showEstimationAlert } = useCostEstimationAlert();
// Estimate cost for blog generation workflow
const estimateBlogWorkflowCost = async (workflowType: 'research' | 'outline' | 'content') => {
if (!userId) return;
try {
const operations: PreflightOperation[] = [];
if (workflowType === 'research') {
// Research typically involves: 3-5 Exa searches + 1 LLM call for analysis
operations.push(
{
provider: 'exa',
operation_type: 'research',
tokens_requested: 0,
},
{
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'research',
tokens_requested: 2000, // Estimated tokens for research analysis
}
);
} else if (workflowType === 'outline') {
// Outline generation: 1 LLM call
operations.push({
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'outline_generation',
tokens_requested: 1500, // Estimated tokens for outline
});
} else if (workflowType === 'content') {
// Content generation: 2-3 LLM calls (one per section typically)
operations.push(
{
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'content_generation',
tokens_requested: 3000, // Estimated tokens per section
},
{
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'content_generation',
tokens_requested: 3000,
}
);
}
const preflightResult = await checkPreflight(operations[0]); // Check first operation
const estimatedCost = preflightResult.estimated_cost || 0;
if (estimatedCost > 0.01) {
showEstimationAlert(
estimatedCost,
`${workflowType} generation`,
() => {
// User confirmed - proceed with operation
if (workflowType === 'research' && onResearchStart) {
onResearchStart();
} else if (workflowType === 'outline' && onOutlineStart) {
onOutlineStart();
} else if (workflowType === 'content' && onContentStart) {
onContentStart();
}
},
() => {
showToastNotification('Operation cancelled', 'info');
}
);
}
} catch (error) {
console.error('[BlogWriterCostAlerts] Error estimating cost:', error);
// Don't block operation on estimation failure
}
};
// Filter alerts relevant to Blog Writer
const blogWriterAlerts = alerts.filter(alert =>
alert.type === 'cost_trend' ||
alert.type === 'oss_recommendation' ||
(alert.type === 'cost_estimation' && alert.message.includes('blog'))
);
return (
<Box sx={{ mb: 2 }}>
{/* Priority 2 Alert Banner */}
{blogWriterAlerts.length > 0 && (
<Priority2AlertBanner
alerts={blogWriterAlerts}
onDismiss={dismissAlert}
maxAlerts={2}
/>
)}
{/* Cost Estimation Info Alert */}
<Collapse in={blogWriterAlerts.length === 0}>
<Alert
severity="info"
icon={<></>}
sx={{
mb: 2,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
border: '1px solid rgba(59, 130, 246, 0.2)',
}}
>
<AlertTitle sx={{ fontWeight: 'bold', mb: 0.5 }}>
💡 Cost Transparency
</AlertTitle>
<Box sx={{ fontSize: '0.875rem' }}>
Blog generation typically costs:
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
<li><strong>Research</strong>: ~$0.01-0.02 (3-5 searches + analysis)</li>
<li><strong>Outline</strong>: ~$0.005-0.01 (1 LLM call)</li>
<li><strong>Content</strong>: ~$0.01-0.03 (2-3 LLM calls per section)</li>
</ul>
<strong>Total per blog</strong>: ~$0.03-0.06 (using OSS models)
</Box>
</Alert>
</Collapse>
</Box>
);
};
/**
* Hook for Blog Writer cost estimation
* Use this in Blog Writer components before triggering operations
*/
export const useBlogWriterCostEstimation = () => {
const { showEstimationAlert } = useCostEstimationAlert();
const estimateAndProceed = async (
workflowType: 'research' | 'outline' | 'content',
onProceed: () => void,
userId?: string
) => {
if (!userId) {
// No user ID - proceed without estimation
onProceed();
return;
}
try {
const operations: PreflightOperation[] = [];
// Define operations based on workflow type
if (workflowType === 'research') {
operations.push(
{ provider: 'exa', operation_type: 'research', tokens_requested: 0 },
{
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'research',
tokens_requested: 2000
}
);
} else if (workflowType === 'outline') {
operations.push({
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'outline_generation',
tokens_requested: 1500,
});
} else if (workflowType === 'content') {
operations.push(
{
provider: 'gemini',
model: 'gemini-2.5-flash',
operation_type: 'content_generation',
tokens_requested: 3000,
}
);
}
if (operations.length > 0) {
const preflightResult = await checkPreflight(operations[0]);
const estimatedCost = preflightResult.estimated_cost || 0;
if (estimatedCost > 0.01) {
showEstimationAlert(
estimatedCost,
`${workflowType} generation`,
onProceed,
() => showToastNotification('Operation cancelled', 'info')
);
} else {
// Low cost - proceed directly
onProceed();
}
} else {
onProceed();
}
} catch (error) {
console.error('[BlogWriterCostEstimation] Error:', error);
// On error, proceed anyway (don't block user)
onProceed();
}
};
return { estimateAndProceed };
};
export default BlogWriterCostAlerts;

View File

@@ -20,24 +20,24 @@ export const BlogWriterLandingSection: React.FC<BlogWriterLandingSectionProps> =
// Only show landing/initial content when no research exists
// Phase navigation header is always visible, so this is just the initial content
if (!research) {
// Show research form only when user explicitly navigated to research phase (clicked "Start Research")
if (currentPhase === 'research') {
return <ManualResearchForm onResearchComplete={onResearchComplete} />;
}
// Default: Always show landing page when no research exists
// This ensures landing page is shown on initial load
return (
<>
{/* Show manual research form when on research phase and CopilotKit unavailable */}
{!copilotKitAvailable && currentPhase === 'research' && (
<ManualResearchForm onResearchComplete={onResearchComplete} />
)}
{/* Show landing page for CopilotKit flow or when not on research phase */}
{(!copilotKitAvailable && currentPhase !== 'research') || copilotKitAvailable ? (
<BlogWriterLanding
onStartWriting={() => {
// Navigate to research phase to start the workflow
navigateToPhase('research');
}}
/>
) : null}
</>
<BlogWriterLanding
onStartWriting={() => {
// Navigate to research phase to show the research form
navigateToPhase('research');
}}
/>
);
}
// If research exists, don't show landing section (phase content will be shown instead)
return null;
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import {
useResearchPolling,
useBlogWriterResearchPolling,
useOutlinePolling,
useMediumGenerationPolling,
useRewritePolling,
@@ -24,8 +24,8 @@ export const useBlogWriterPolling = ({
onContentConfirmed,
navigateToPhase,
}: UseBlogWriterPollingProps) => {
// Research polling hook (for context awareness)
const researchPolling = useResearchPolling({
// Research polling hook (for context awareness) - uses blog writer endpoint
const researchPolling = useBlogWriterResearchPolling({
onComplete: onResearchComplete,
onError: (error) => console.error('Research polling error:', error)
});

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef } from 'react';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
import { useResearchPolling } from '../../hooks/usePolling';
import { useBlogWriterResearchPolling } from '../../hooks/usePolling';
import ResearchProgressModal from './ResearchProgressModal';
import { researchCache } from '../../services/researchCache';
@@ -22,7 +22,7 @@ export const ManualResearchForm: React.FC<ManualResearchFormProps> = ({ onResear
const keywordsRef = useRef<HTMLInputElement | null>(null);
const blogLengthRef = useRef<HTMLSelectElement | null>(null);
const polling = useResearchPolling({
const polling = useBlogWriterResearchPolling({
onProgress: (message) => {
setCurrentMessage(message);
},

View File

@@ -66,6 +66,9 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
switch (phaseId) {
case 'research':
// Always show "Start Research" button when on research phase and no research exists yet
// This allows users to manually trigger research form
// If research already exists, don't show the button (user can click the phase button to view)
if (!hasResearch) {
return { label: 'Start Research', handler: actionHandlers.onResearchAction || null };
}
@@ -326,10 +329,10 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
// 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 research phase: always show button when on research phase (allows manual trigger)
// 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 isResearchPhase = phase.id === 'research' && action.handler; // Always show if handler exists
// 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;
@@ -368,12 +371,12 @@ export const PhaseNavigation: React.FC<PhaseNavigationProps> = ({
// 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)
// For research phase: show action button when on research phase and no research exists yet (to start research)
const showAction = action.handler && (
isCurrent ||
(isCurrent && phase.id === 'research' && !hasResearch) || // Show "Start Research" when on research phase with no research
(isCurrent && phase.id !== 'research') || // For other phases, show action when current
(!isCompleted && !isDisabled) ||
isResearchPhase ||
isOutlinePhase ||
isSEOPhase // Show SEO actions when handler exists - handler existence means prerequisites are met, so ignore isDisabled
(phase.id !== 'research' && (isResearchPhase || isOutlinePhase || isSEOPhase)) // Show for outline/SEO when appropriate
);
// Determine chip class

View File

@@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
import { useResearchPolling } from '../../hooks/usePolling';
import { useBlogWriterResearchPolling } from '../../hooks/usePolling';
import ResearchProgressModal from './ResearchProgressModal';
import { researchCache } from '../../services/researchCache';
@@ -25,7 +25,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
// Track if we've navigated to research phase for this form display
const hasNavigatedRef = useRef<boolean>(false);
const polling = useResearchPolling({
const polling = useBlogWriterResearchPolling({
onProgress: (message) => {
setCurrentMessage(message);
setForceUpdate(prev => prev + 1); // Force re-render
@@ -128,40 +128,64 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
};
},
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;
try {
const _ = forceUpdate;
// Safely access polling state with defaults - handle case where polling might not be initialized
let currentStatus = 'idle';
let progressMessages: Array<{ timestamp: string; message: string }> = [];
try {
if (polling) {
currentStatus = polling.currentStatus || 'idle';
progressMessages = polling.progressMessages || [];
}
}, 0);
}
if (polling.currentStatus === 'completed' && polling.progressMessages.length > 0) {
const latestMessage = polling.progressMessages[polling.progressMessages.length - 1];
} catch (pollingError) {
console.warn('[ResearchAction] Error accessing polling state in render:', pollingError);
// Use defaults already set above
}
// 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 = currentStatus !== 'completed' &&
currentStatus !== 'in_progress' &&
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 (currentStatus === 'completed' && progressMessages.length > 0) {
const latestMessage = progressMessages[progressMessages.length - 1];
return (
<div style={{ padding: '16px', backgroundColor: '#e8f5e8', borderRadius: '8px', border: '1px solid #4caf50', margin: '8px 0' }}>
<p style={{ margin: 0, color: '#4caf50', fontWeight: '500' }}> Research completed successfully!</p>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: '14px' }}>{latestMessage?.message || 'Research data is now available for your blog.'}</p>
</div>
);
}
if (currentStatus === 'in_progress' || currentStatus === 'running') {
return (
<div style={{ padding: '16px', backgroundColor: '#fff3e0', borderRadius: '8px', border: '1px solid #ff9800', margin: '8px 0' }}>
<p style={{ margin: 0, color: '#ff9800', fontWeight: '500' }}>🔄 Research in progress...</p>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: '14px' }}>{currentMessage || 'Gathering research data...'}</p>
</div>
);
}
} catch (renderError) {
console.error('[ResearchAction] Error in render function:', renderError);
// Return a safe fallback UI
return (
<div style={{ padding: '16px', backgroundColor: '#e8f5e8', borderRadius: '8px', border: '1px solid #4caf50', margin: '8px 0' }}>
<p style={{ margin: 0, color: '#4caf50', fontWeight: '500' }}> Research completed successfully!</p>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: '14px' }}>{latestMessage?.message || 'Research data is now available for your blog.'}</p>
</div>
);
}
if (polling.currentStatus === 'in_progress' || polling.currentStatus === 'running') {
return (
<div style={{ padding: '16px', backgroundColor: '#fff3e0', borderRadius: '8px', border: '1px solid #ff9800', margin: '8px 0' }}>
<p style={{ margin: 0, color: '#ff9800', fontWeight: '500' }}>🔄 Research in progress...</p>
<p style={{ margin: '8px 0 0 0', color: '#666', fontSize: '14px' }}>{currentMessage || 'Gathering research data...'}</p>
<div style={{ padding: '16px', backgroundColor: '#f8f9fa', borderRadius: '8px', border: '1px solid #e0e0e0', margin: '8px 0' }}>
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>🔍 Research form is loading...</p>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useResearchPolling } from '../../hooks/usePolling';
import { useBlogWriterResearchPolling } from '../../hooks/usePolling';
import ResearchProgressModal from './ResearchProgressModal';
import { BlogResearchResponse } from '../../services/blogWriterApi';
import { researchCache } from '../../services/researchCache';
@@ -18,7 +18,7 @@ export const ResearchPollingHandler: React.FC<ResearchPollingHandlerProps> = ({
}) => {
const [currentMessage, setCurrentMessage] = useState<string>('');
const polling = useResearchPolling({
const polling = useBlogWriterResearchPolling({
onProgress: (message) => {
debug.log('[ResearchPollingHandler] progress', { message });
setCurrentMessage(message);