Scheduled research persona generation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user