Allowing AI to generate suggestions for the blog writer
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { CopilotSidebar } from '@copilotkit/react-ui';
|
||||
import '@copilotkit/react-ui/styles.css';
|
||||
import { blogWriterApi } from '../../services/blogWriterApi';
|
||||
import { useOutlinePolling } from '../../hooks/usePolling';
|
||||
import { useOutlinePolling, useMediumGenerationPolling, useResearchPolling } from '../../hooks/usePolling';
|
||||
import { useClaimFixer } from '../../hooks/useClaimFixer';
|
||||
import { useMarkdownProcessor } from '../../hooks/useMarkdownProcessor';
|
||||
import { useBlogWriterState } from '../../hooks/useBlogWriterState';
|
||||
@@ -20,11 +20,12 @@ import { EnhancedOutlineActions } from './EnhancedOutlineActions';
|
||||
import HallucinationChecker from './HallucinationChecker';
|
||||
import Publisher from './Publisher';
|
||||
import OutlineGenerator from './OutlineGenerator';
|
||||
import SectionGenerator from './SectionGenerator';
|
||||
import OutlineRefiner from './OutlineRefiner';
|
||||
import SEOProcessor from './SEOProcessor';
|
||||
import BlogWriterLanding from './BlogWriterLanding';
|
||||
import ResearchProgressModal from './ResearchProgressModal';
|
||||
import { OutlineProgressModal } from './OutlineProgressModal';
|
||||
import OutlineFeedbackForm from './OutlineFeedbackForm';
|
||||
import { BlogEditor } from './WYSIWYG';
|
||||
|
||||
export const BlogWriter: React.FC = () => {
|
||||
// Use custom hook for all state management
|
||||
@@ -45,6 +46,7 @@ export const BlogWriter: React.FC = () => {
|
||||
researchCoverage,
|
||||
researchTitles,
|
||||
aiGeneratedTitles,
|
||||
outlineConfirmed,
|
||||
setOutline,
|
||||
setTitleOptions,
|
||||
setSections,
|
||||
@@ -55,10 +57,12 @@ export const BlogWriter: React.FC = () => {
|
||||
handleResearchComplete,
|
||||
handleOutlineComplete,
|
||||
handleOutlineError,
|
||||
handleSectionGenerated,
|
||||
handleContinuityRefresh,
|
||||
handleTitleSelect,
|
||||
handleCustomTitle
|
||||
handleCustomTitle,
|
||||
handleOutlineConfirmed,
|
||||
handleOutlineRefined,
|
||||
handleContentUpdate,
|
||||
handleContentSave
|
||||
} = useBlogWriterState();
|
||||
|
||||
// Custom hooks for complex functionality
|
||||
@@ -68,13 +72,16 @@ export const BlogWriter: React.FC = () => {
|
||||
setSections
|
||||
);
|
||||
|
||||
const { convertMarkdownToHTML, getTotalWords, getOutlineStats } = useMarkdownProcessor(
|
||||
const { convertMarkdownToHTML } = useMarkdownProcessor(
|
||||
outline,
|
||||
sections
|
||||
);
|
||||
|
||||
// Get suggestions
|
||||
const suggestions = useSuggestions(research, outline);
|
||||
// Research polling hook (for context awareness)
|
||||
const researchPolling = useResearchPolling({
|
||||
onComplete: handleResearchComplete,
|
||||
onError: (error) => console.error('Research polling error:', error)
|
||||
});
|
||||
|
||||
// Outline polling hook
|
||||
const outlinePolling = useOutlinePolling({
|
||||
@@ -82,22 +89,90 @@ export const BlogWriter: React.FC = () => {
|
||||
onError: handleOutlineError
|
||||
});
|
||||
|
||||
// Medium generation polling (used after confirm if short blog)
|
||||
const mediumPolling = useMediumGenerationPolling({
|
||||
onComplete: (result: any) => {
|
||||
try {
|
||||
if (result && result.sections) {
|
||||
const newSections: Record<string, string> = {};
|
||||
result.sections.forEach((s: any) => {
|
||||
newSections[String(s.id)] = s.content || '';
|
||||
});
|
||||
setSections(newSections);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to apply medium generation result:', e);
|
||||
}
|
||||
},
|
||||
onError: (err) => console.error('Medium generation failed:', err)
|
||||
});
|
||||
|
||||
// Get context-aware suggestions based on current task status
|
||||
const suggestions = useSuggestions(
|
||||
research,
|
||||
outline,
|
||||
outlineConfirmed,
|
||||
{ isPolling: researchPolling.isPolling, currentStatus: researchPolling.currentStatus },
|
||||
{ isPolling: outlinePolling.isPolling, currentStatus: outlinePolling.currentStatus },
|
||||
{ isPolling: mediumPolling.isPolling, currentStatus: mediumPolling.currentStatus }
|
||||
);
|
||||
|
||||
// Add minimum display time for modal
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [modalStartTime, setModalStartTime] = useState<number | null>(null);
|
||||
const [isMediumGenerationStarting, setIsMediumGenerationStarting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ((mediumPolling.isPolling || isMediumGenerationStarting) && !showModal) {
|
||||
setShowModal(true);
|
||||
setModalStartTime(Date.now());
|
||||
} else if (!mediumPolling.isPolling && !isMediumGenerationStarting && showModal) {
|
||||
const elapsed = Date.now() - (modalStartTime || 0);
|
||||
const minDisplayTime = 2000; // 2 seconds minimum
|
||||
|
||||
if (elapsed < minDisplayTime) {
|
||||
setTimeout(() => {
|
||||
setShowModal(false);
|
||||
setModalStartTime(null);
|
||||
}, minDisplayTime - elapsed);
|
||||
} else {
|
||||
setShowModal(false);
|
||||
setModalStartTime(null);
|
||||
}
|
||||
}
|
||||
}, [mediumPolling.isPolling, isMediumGenerationStarting, showModal, modalStartTime]);
|
||||
|
||||
// Handle medium generation start from OutlineFeedbackForm
|
||||
const handleMediumGenerationStarted = (taskId: string) => {
|
||||
console.log('Starting medium generation polling for task:', taskId);
|
||||
setIsMediumGenerationStarting(false); // Clear the starting state
|
||||
mediumPolling.startPolling(taskId);
|
||||
};
|
||||
|
||||
// Show modal immediately when copilot action is triggered
|
||||
const handleMediumGenerationTriggered = () => {
|
||||
console.log('Medium generation triggered - showing modal immediately');
|
||||
setIsMediumGenerationStarting(true);
|
||||
};
|
||||
|
||||
// Debug medium polling state
|
||||
console.log('Medium polling state:', {
|
||||
isPolling: mediumPolling.isPolling,
|
||||
status: mediumPolling.currentStatus,
|
||||
progressCount: mediumPolling.progressMessages.length
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Outline Progress Modal */}
|
||||
<ResearchProgressModal
|
||||
open={Boolean(outlineTaskId && (outlinePolling.isPolling || outlinePolling.currentStatus === 'pending' || outlinePolling.currentStatus === 'running'))}
|
||||
title="Outline generation in progress"
|
||||
status={outlinePolling.currentStatus}
|
||||
messages={outlinePolling.progressMessages}
|
||||
error={outlinePolling.error}
|
||||
onClose={() => { /* informational while processing */ }}
|
||||
/>
|
||||
{/* Extracted Components */}
|
||||
<KeywordInputForm onResearchComplete={handleResearchComplete} />
|
||||
<KeywordInputForm
|
||||
onResearchComplete={handleResearchComplete}
|
||||
onTaskStart={(taskId) => researchPolling.startPolling(taskId)}
|
||||
/>
|
||||
<CustomOutlineForm onOutlineCreated={setOutline} />
|
||||
<ResearchAction onResearchComplete={handleResearchComplete} />
|
||||
<ResearchDataActions
|
||||
@@ -109,6 +184,14 @@ export const BlogWriter: React.FC = () => {
|
||||
outline={outline}
|
||||
onOutlineUpdated={setOutline}
|
||||
/>
|
||||
<OutlineFeedbackForm
|
||||
outline={outline}
|
||||
research={research!}
|
||||
onOutlineConfirmed={handleOutlineConfirmed}
|
||||
onOutlineRefined={handleOutlineRefined}
|
||||
onMediumGenerationStarted={handleMediumGenerationStarted}
|
||||
onMediumGenerationTriggered={handleMediumGenerationTriggered}
|
||||
/>
|
||||
|
||||
{/* New extracted functionality components */}
|
||||
<OutlineGenerator
|
||||
@@ -116,13 +199,6 @@ export const BlogWriter: React.FC = () => {
|
||||
onTaskStart={(taskId) => setOutlineTaskId(taskId)}
|
||||
onPollingStart={(taskId) => outlinePolling.startPolling(taskId)}
|
||||
/>
|
||||
<SectionGenerator
|
||||
outline={outline}
|
||||
research={research}
|
||||
genMode={genMode}
|
||||
onSectionGenerated={handleSectionGenerated}
|
||||
onContinuityRefresh={handleContinuityRefresh}
|
||||
/>
|
||||
<OutlineRefiner
|
||||
outline={outline}
|
||||
onOutlineUpdated={setOutline}
|
||||
@@ -161,57 +237,75 @@ export const BlogWriter: React.FC = () => {
|
||||
{research && outline.length === 0 && <ResearchResults research={research} />}
|
||||
{outline.length > 0 && (
|
||||
<div>
|
||||
{/* Enhanced Title Selection */}
|
||||
<EnhancedTitleSelector
|
||||
{outlineConfirmed ? (
|
||||
/* WYSIWYG Editor - Show when outline is confirmed */
|
||||
<BlogEditor
|
||||
outline={outline}
|
||||
research={research}
|
||||
initialTitle={selectedTitle}
|
||||
titleOptions={titleOptions}
|
||||
selectedTitle={selectedTitle}
|
||||
sections={outline}
|
||||
researchTitles={researchTitles}
|
||||
aiGeneratedTitles={aiGeneratedTitles}
|
||||
onTitleSelect={handleTitleSelect}
|
||||
onCustomTitle={handleCustomTitle}
|
||||
researchTitles={researchTitles}
|
||||
aiGeneratedTitles={aiGeneratedTitles}
|
||||
sections={sections}
|
||||
onContentUpdate={handleContentUpdate}
|
||||
onSave={handleContentSave}
|
||||
/>
|
||||
|
||||
) : (
|
||||
/* Outline Editor - Show when outline is not confirmed */
|
||||
<>
|
||||
{/* Enhanced Title Selection */}
|
||||
<EnhancedTitleSelector
|
||||
titleOptions={titleOptions}
|
||||
selectedTitle={selectedTitle}
|
||||
sections={outline}
|
||||
researchTitles={researchTitles}
|
||||
aiGeneratedTitles={aiGeneratedTitles}
|
||||
onTitleSelect={handleTitleSelect}
|
||||
onCustomTitle={handleCustomTitle}
|
||||
/>
|
||||
|
||||
|
||||
{/* Enhanced Outline Editor */}
|
||||
<EnhancedOutlineEditor
|
||||
outline={outline}
|
||||
research={research}
|
||||
sourceMappingStats={sourceMappingStats}
|
||||
groundingInsights={groundingInsights}
|
||||
optimizationResults={optimizationResults}
|
||||
researchCoverage={researchCoverage}
|
||||
onRefine={(op, id, payload) => blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then((res: any) => setOutline(res.outline))}
|
||||
/>
|
||||
{/* Enhanced Outline Editor */}
|
||||
<EnhancedOutlineEditor
|
||||
outline={outline}
|
||||
research={research}
|
||||
sourceMappingStats={sourceMappingStats}
|
||||
groundingInsights={groundingInsights}
|
||||
optimizationResults={optimizationResults}
|
||||
researchCoverage={researchCoverage}
|
||||
onRefine={(op, id, payload) => blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then((res: any) => setOutline(res.outline))}
|
||||
/>
|
||||
|
||||
{/* Draft/Polished Mode Toggle */}
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<label style={{ marginRight: 8 }}>Generation mode:</label>
|
||||
<select value={genMode} onChange={(e) => setGenMode(e.target.value as 'draft' | 'polished')}>
|
||||
<option value="draft">Draft (faster, lower cost)</option>
|
||||
<option value="polished">Polished (higher quality)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{outline.map(s => (
|
||||
<div key={s.id} style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<h4 style={{ margin: 0 }}>{s.heading}</h4>
|
||||
{/* Continuity badge */}
|
||||
{sections[s.id] && (
|
||||
<ContinuityBadge sectionId={s.id} refreshToken={continuityRefresh} />
|
||||
)}
|
||||
{/* Draft/Polished Mode Toggle */}
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<label style={{ marginRight: 8 }}>Generation mode:</label>
|
||||
<select value={genMode} onChange={(e) => setGenMode(e.target.value as 'draft' | 'polished')}>
|
||||
<option value="draft">Draft (faster, lower cost)</option>
|
||||
<option value="polished">Polished (higher quality)</option>
|
||||
</select>
|
||||
</div>
|
||||
{sections[s.id] ? (
|
||||
<>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{sections[s.id]}</pre>
|
||||
<SEOMiniPanel analysis={seoAnalysis} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ fontStyle: 'italic', color: '#666' }}>Ask the copilot to generate this section.</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{outline.map(s => (
|
||||
<div key={s.id} style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<h4 style={{ margin: 0 }}>{s.heading}</h4>
|
||||
{/* Continuity badge */}
|
||||
{sections[s.id] && (
|
||||
<ContinuityBadge sectionId={s.id} refreshToken={continuityRefresh} />
|
||||
)}
|
||||
</div>
|
||||
{sections[s.id] ? (
|
||||
<>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{sections[s.id]}</pre>
|
||||
<SEOMiniPanel analysis={seoAnalysis} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ fontStyle: 'italic', color: '#666' }}>Ask the copilot to generate this section.</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -231,6 +325,7 @@ export const BlogWriter: React.FC = () => {
|
||||
// Get current state information
|
||||
const hasResearch = research !== null;
|
||||
const hasOutline = outline.length > 0;
|
||||
const isOutlineConfirmed = outlineConfirmed;
|
||||
const researchInfo = hasResearch ? {
|
||||
sources: research.sources?.length || 0,
|
||||
queries: research.search_queries?.length || 0,
|
||||
@@ -239,6 +334,14 @@ export const BlogWriter: React.FC = () => {
|
||||
searchIntent: research.keyword_analysis?.search_intent || 'informational'
|
||||
} : null;
|
||||
|
||||
const outlineContext = hasOutline ? `
|
||||
OUTLINE DETAILS:
|
||||
- Total sections: ${outline.length}
|
||||
- Section headings: ${outline.map(s => s.heading).join(', ')}
|
||||
- Total target words: ${outline.reduce((sum, s) => sum + (s.target_words || 0), 0)}
|
||||
- Section breakdown: ${outline.map(s => `${s.heading} (${s.target_words || 0} words, ${s.subheadings?.length || 0} subheadings, ${s.key_points?.length || 0} key points)`).join('; ')}
|
||||
` : '';
|
||||
|
||||
const toolGuide = `
|
||||
You are the ALwrity Blog Writing Assistant. You MUST call the appropriate frontend actions (tools) to fulfill user requests.
|
||||
|
||||
@@ -252,7 +355,8 @@ ${hasResearch && researchInfo ? `
|
||||
- Search intent: ${researchInfo.searchIntent}
|
||||
` : '❌ No research completed yet'}
|
||||
|
||||
${hasOutline ? `✅ OUTLINE GENERATED: ${outline.length} sections created` : '❌ No outline generated yet'}
|
||||
${hasOutline ? `✅ OUTLINE GENERATED: ${outline.length} sections created${isOutlineConfirmed ? ' (CONFIRMED)' : ' (PENDING CONFIRMATION)'}` : '❌ No outline generated yet'}
|
||||
${outlineContext}
|
||||
|
||||
Available tools:
|
||||
- getResearchKeywords(prompt?: string) - Get keywords from user for research
|
||||
@@ -261,9 +365,12 @@ Available tools:
|
||||
- chatWithResearchData(question: string) - Chat with research data to explore insights and get recommendations
|
||||
- generateOutline()
|
||||
- createOutlineWithCustomInputs(customInstructions: string) - Create outline with user's custom instructions
|
||||
- refineOutline(prompt?: string) - Refine outline based on user feedback
|
||||
- chatWithOutline(question?: string) - Chat with outline to get insights and ask questions about content structure
|
||||
- confirmOutlineAndGenerateContent() - Confirm outline and mark as ready for content generation (does NOT auto-generate content)
|
||||
- generateSection(sectionId: string)
|
||||
- generateAllSections()
|
||||
- refineOutline(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object)
|
||||
- refineOutlineStructure(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object)
|
||||
- enhanceSection(sectionId: string, focus?: string) - Enhance a specific section with AI improvements
|
||||
- optimizeOutline(focus?: string) - Optimize entire outline for better flow, SEO, and engagement
|
||||
- rebalanceOutline(targetWords?: number) - Rebalance word count distribution across sections
|
||||
@@ -282,20 +389,48 @@ Available tools:
|
||||
- If user wants to explore research data, use chatWithResearchData() but then guide them to outline creation
|
||||
- If user has specific outline requirements, use createOutlineWithCustomInputs() with their instructions
|
||||
- When user asks for outline, call generateOutline() or createOutlineWithCustomInputs() based on their needs
|
||||
- When user asks to generate content, call generateSection or generateAllSections
|
||||
- After outline generation, ALWAYS guide user to review and confirm the outline
|
||||
- If user wants to discuss the outline, use chatWithOutline() to provide insights and answer questions
|
||||
- If user wants to refine the outline, use refineOutline() to collect their feedback and refine
|
||||
- When user clicks "Confirm & Generate Content", ONLY call confirmOutlineAndGenerateContent() - DO NOT automatically generate content
|
||||
- Only after outline confirmation, show content generation suggestions and wait for user to explicitly request content generation
|
||||
- When user asks to generate content before outline confirmation, remind them to confirm the outline first
|
||||
- Content generation should ONLY happen when user explicitly clicks "Generate all sections" or "Generate [specific section]"
|
||||
|
||||
ENGAGEMENT TACTICS:
|
||||
- DO NOT ask for clarification - take action immediately with the information provided
|
||||
- Always call the appropriate tool instead of just talking about what you could do
|
||||
- Be aware of the current state and reference research results when relevant
|
||||
- Guide users through the process: Research → Outline → Content → SEO → Publish
|
||||
- Guide users through the process: Research → Outline → Outline Review & Confirmation → Content → SEO → Publish
|
||||
- Use encouraging language and highlight progress made
|
||||
- If user seems lost, remind them of the current stage and suggest the next step
|
||||
- When research is complete, emphasize the value of the data found and guide to outline creation
|
||||
- When outline is generated, emphasize the importance of reviewing and confirming before content generation
|
||||
- Encourage users to make small manual edits to the outline UI before using AI for major changes
|
||||
`;
|
||||
return [toolGuide, additional].filter(Boolean).join('\n\n');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Outline Progress Modal */}
|
||||
{/* Outline modal */}
|
||||
<OutlineProgressModal
|
||||
isVisible={outlinePolling.isPolling}
|
||||
status={outlinePolling.currentStatus}
|
||||
progressMessages={outlinePolling.progressMessages.map(m => m.message)}
|
||||
latestMessage={outlinePolling.progressMessages.length > 0 ? outlinePolling.progressMessages[outlinePolling.progressMessages.length - 1].message : ''}
|
||||
error={outlinePolling.error}
|
||||
/>
|
||||
|
||||
{/* Medium generation modal */}
|
||||
<OutlineProgressModal
|
||||
isVisible={showModal}
|
||||
status={mediumPolling.currentStatus}
|
||||
progressMessages={mediumPolling.progressMessages.map(m => m.message)}
|
||||
latestMessage={mediumPolling.progressMessages.length > 0 ? mediumPolling.progressMessages[mediumPolling.progressMessages.length - 1].message : ''}
|
||||
error={mediumPolling.error}
|
||||
titleOverride={'📝 Generating Your Blog Content'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user