AI Blog Writer - Implement modular architecture with research, outline, and core services

This commit is contained in:
ajaysi
2025-09-12 16:53:16 +05:30
parent c0a366269d
commit 2ae0c4a8b9
29 changed files with 3210 additions and 907 deletions

View File

@@ -10,6 +10,9 @@ import SEOMiniPanel from './SEOMiniPanel';
import ResearchResults from './ResearchResults';
import KeywordInputForm from './KeywordInputForm';
import ResearchAction from './ResearchAction';
import { CustomOutlineForm } from './CustomOutlineForm';
import { ResearchDataActions } from './ResearchDataActions';
import { EnhancedOutlineActions } from './EnhancedOutlineActions';
const useCopilotActionTyped = useCopilotAction as any;
@@ -141,9 +144,22 @@ export const BlogWriter: React.FC = () => {
} catch (error) {
console.error('Outline generation failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Provide more specific error messages based on the error type
let userMessage = '❌ Outline generation failed. ';
if (errorMessage.includes('503') || errorMessage.includes('overloaded')) {
userMessage += 'The AI service is temporarily overloaded. Please try again in a few minutes.';
} else if (errorMessage.includes('timeout')) {
userMessage += 'The request timed out. Please try again.';
} else if (errorMessage.includes('Invalid outline structure')) {
userMessage += 'The AI generated an invalid response. Please try again with different research data.';
} else {
userMessage += `${errorMessage}. Please try again or contact support if the problem persists.`;
}
return {
success: false,
message: `❌ Outline generation failed: ${errorMessage}. The AI system encountered an issue while creating your outline. Please try again or contact support if the problem persists.`
message: userMessage
};
}
return {
@@ -411,6 +427,9 @@ export const BlogWriter: React.FC = () => {
}
});
// Publish (convert markdown -> HTML rudimentary; TODO: replace with proper converter like marked)
useCopilotActionTyped({
name: 'publishToPlatform',
@@ -439,17 +458,36 @@ export const BlogWriter: React.FC = () => {
const suggestions = useMemo(() => {
const items = [] as { title: string; message: string }[];
if (!research) items.push({ title: '🔎 Start research', message: "I want to research a topic for my blog" });
if (research && outline.length === 0) items.push({ title: '🧩 Create Outline', message: 'Let\'s proceed to create an outline based on the research results' });
if (outline.length > 0) {
if (!research) {
items.push({ title: '🔎 Start research', message: "I want to research a topic for my blog" });
} else if (research && outline.length === 0) {
// Research completed, guide user to outline creation
items.push({
title: '🧩 Create Outline',
message: 'Let\'s proceed to create an outline based on the research results'
});
items.push({
title: '💬 Chat with Research Data',
message: 'I want to explore the research data and ask questions about the findings'
});
items.push({
title: '🎨 Create Custom Outline',
message: 'I want to create an outline with my own specific instructions and requirements'
});
} else if (outline.length > 0) {
// Outline created, focus on content generation
items.push({ title: '📝 Generate all sections', message: 'Generate all sections of my blog post' });
outline.forEach(s => items.push({ title: `✍️ Generate ${s.heading}`, message: `Generate the section: ${s.heading}` }));
items.push({ title: '🔧 Refine outline', message: 'Help me refine the outline structure' });
items.push({ title: '✨ Enhance outline', message: 'Optimize the entire outline for better flow and engagement' });
items.push({ title: '⚖️ Rebalance word counts', message: 'Rebalance word count distribution across sections' });
items.push({ title: '📈 Run SEO analysis', message: 'Analyze SEO for my blog post' });
items.push({ title: '🧾 Generate SEO metadata', message: 'Generate SEO metadata and title' });
items.push({ title: '🧪 Hallucination check', message: 'Check for any false claims in my content' });
items.push({ title: '🚀 Publish to WordPress', message: 'Publish my blog to WordPress' });
}
return items;
}, [research, outline]);
@@ -457,7 +495,17 @@ export const BlogWriter: React.FC = () => {
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Extracted Components */}
<KeywordInputForm onResearchComplete={handleResearchComplete} />
<CustomOutlineForm onOutlineCreated={setOutline} />
<ResearchAction onResearchComplete={handleResearchComplete} />
<ResearchDataActions
research={research}
onOutlineCreated={setOutline}
onTitleOptionsSet={setTitleOptions}
/>
<EnhancedOutlineActions
outline={outline}
onOutlineUpdated={setOutline}
/>
<div style={{ padding: 16, borderBottom: '1px solid #eee' }}>
<h2 style={{ margin: 0 }}>AI Blog Writer</h2>
@@ -551,25 +599,40 @@ Available tools:
- getResearchKeywords(prompt?: string) - Get keywords from user for research
- performResearch(formData: string) - Perform research with collected keywords (formData is JSON string with keywords and blogLength)
- researchTopic(keywords: string, industry?: string, target_audience?: string)
- chatWithResearchData(question: string) - Chat with research data to explore insights and get recommendations
- generateOutline()
- createOutlineWithCustomInputs(customInstructions: string) - Create outline with user's custom instructions
- generateSection(sectionId: string)
- generateAllSections()
- refineOutline(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
- runSEOAnalyze(keywords?: string)
- generateSEOMetadata(title?: string)
- runHallucinationCheck()
- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string)
CRITICAL BEHAVIOR:
CRITICAL BEHAVIOR & USER GUIDANCE:
- When user wants to research ANY topic, IMMEDIATELY call getResearchKeywords() to get their input
- When user asks to research something, call getResearchKeywords() first to collect their keywords
- After getResearchKeywords() completes, IMMEDIATELY call performResearch() with the collected data
- When user asks for outline, call generateOutline()
USER GUIDANCE STRATEGY:
- After research completion, ALWAYS guide user toward outline creation as the next step
- 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
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
- 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
`;
return [toolGuide, additional].filter(Boolean).join('\n\n');
}}

View File

@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
const useCopilotActionTyped = useCopilotAction as any;
interface CustomOutlineFormProps {
onOutlineCreated?: (outline: any) => void;
}
export const CustomOutlineForm: React.FC<CustomOutlineFormProps> = ({ onOutlineCreated }) => {
const [customInstructions, setCustomInstructions] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
useCopilotActionTyped({
name: 'getCustomOutlineInstructions',
description: 'Get custom instructions from user for outline generation',
parameters: [
{ name: 'prompt', type: 'string', description: 'Prompt to show user', required: false }
],
renderAndWaitForResponse: ({ respond, args, status }: { respond?: (value: string) => void; args: { prompt?: string }; status: string }) => {
if (status === 'complete') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f8ff',
borderRadius: '8px',
border: '1px solid #1976d2'
}}>
<p style={{ margin: 0, color: '#1976d2', fontWeight: '500' }}>
Custom outline instructions received! Creating your personalized outline...
</p>
</div>
);
}
return (
<div style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🎨 Create Custom Outline
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{args.prompt || 'Tell me your specific requirements for the blog outline. What should it focus on? What structure do you prefer?'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Custom Instructions *
</label>
<textarea
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
placeholder="e.g., Focus on beginner-friendly explanations, include case studies, emphasize practical applications, create a step-by-step guide format..."
style={{
width: '100%',
minHeight: '120px',
padding: '12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box',
resize: 'vertical',
fontFamily: 'inherit'
}}
autoFocus
autoComplete="off"
spellCheck="true"
/>
</div>
<div style={{
padding: '12px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
border: '1px solid #1976d2'
}}>
<h5 style={{ margin: '0 0 8px 0', color: '#1976d2', fontSize: '14px' }}>💡 Examples:</h5>
<ul style={{ margin: '0', paddingLeft: '20px', fontSize: '13px', color: '#333' }}>
<li>"Focus on beginner-friendly explanations with practical examples"</li>
<li>"Include case studies and real-world applications"</li>
<li>"Create a step-by-step tutorial format"</li>
<li>"Emphasize the business benefits and ROI"</li>
<li>"Make it more technical and detailed for developers"</li>
</ul>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
onClick={() => {
if (customInstructions.trim()) {
respond?.(customInstructions.trim());
} else {
window.alert('Please provide your custom instructions for the outline.');
}
}}
disabled={!customInstructions.trim() || isSubmitting}
style={{
backgroundColor: customInstructions.trim() ? '#1976d2' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: customInstructions.trim() ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
{isSubmitting ? '⏳ Creating...' : '🚀 Create Custom Outline'}
</button>
<button
onClick={() => respond?.('CANCEL')}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</div>
);
}
});
return null; // This component only provides the CopilotKit action, no UI
};

View File

@@ -0,0 +1,200 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogOutlineSection } from '../../services/blogWriterApi';
const useCopilotActionTyped = useCopilotAction as any;
interface EnhancedOutlineActionsProps {
outline: BlogOutlineSection[];
onOutlineUpdated: (outline: BlogOutlineSection[]) => void;
}
export const EnhancedOutlineActions: React.FC<EnhancedOutlineActionsProps> = ({
outline,
onOutlineUpdated
}) => {
// Enhanced Outline Actions
useCopilotActionTyped({
name: 'enhanceSection',
description: 'Enhance a specific outline section with AI improvements',
parameters: [
{ name: 'sectionId', type: 'string', description: 'ID of the section to enhance', required: true },
{ name: 'focus', type: 'string', description: 'Enhancement focus (SEO, engagement, depth, etc.)', required: false }
],
handler: async ({ sectionId, focus = 'general improvement' }: { sectionId: string; focus?: string }) => {
const section = outline.find(s => s.id === sectionId);
if (!section) return { success: false, message: 'Section not found' };
try {
const enhancedSection = await blogWriterApi.enhanceSection(section, focus);
onOutlineUpdated(outline.map(s => s.id === sectionId ? enhancedSection : s));
return {
success: true,
message: `Enhanced section "${section.heading}" with focus on ${focus}`,
enhanced_section: enhancedSection
};
} catch (error) {
return { success: false, message: `Enhancement failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #9c27b0',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#9c27b0' }}> Enhancing Section</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing section content and structure...</p>
<p style={{ margin: '0 0 8px 0' }}> Generating enhanced subheadings and key points...</p>
<p style={{ margin: '0' }}> Optimizing for better engagement and SEO...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
useCopilotActionTyped({
name: 'optimizeOutline',
description: 'Optimize entire outline for better flow, SEO, and engagement',
parameters: [
{ name: 'focus', type: 'string', description: 'Optimization focus (flow, SEO, engagement, etc.)', required: false }
],
handler: async ({ focus = 'general optimization' }: { focus?: string }) => {
if (outline.length === 0) return { success: false, message: 'No outline to optimize' };
try {
const optimizedOutline = await blogWriterApi.optimizeOutline({ outline }, focus);
onOutlineUpdated(optimizedOutline.outline);
return {
success: true,
message: `Optimized outline with focus on ${focus}`,
optimized_outline: optimizedOutline.outline
};
} catch (error) {
return { success: false, message: `Optimization failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #ff9800',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#ff9800' }}>🎯 Optimizing Outline</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing outline structure and flow...</p>
<p style={{ margin: '0 0 8px 0' }}> Optimizing headings for SEO and engagement...</p>
<p style={{ margin: '0' }}> Improving narrative progression and reader experience...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
useCopilotActionTyped({
name: 'rebalanceOutline',
description: 'Rebalance word count distribution across outline sections',
parameters: [
{ name: 'targetWords', type: 'number', description: 'Target total word count', required: false }
],
handler: async ({ targetWords = 1500 }: { targetWords?: number }) => {
if (outline.length === 0) return { success: false, message: 'No outline to rebalance' };
try {
const rebalancedOutline = await blogWriterApi.rebalanceOutline({ outline }, targetWords);
onOutlineUpdated(rebalancedOutline.outline);
return {
success: true,
message: `Rebalanced outline for ${targetWords} words`,
rebalanced_outline: rebalancedOutline.outline
};
} catch (error) {
return { success: false, message: `Rebalancing failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #4caf50',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#4caf50' }}> Rebalancing Word Counts</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Calculating optimal word distribution...</p>
<p style={{ margin: '0 0 8px 0' }}> Adjusting section word counts...</p>
<p style={{ margin: '0' }}> Ensuring balanced content structure...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
return null; // This component only provides the CopilotKit actions, no UI
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
@@ -9,23 +9,136 @@ interface KeywordInputFormProps {
onResearchComplete?: (researchData: BlogResearchResponse) => void;
}
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
// State for button enable/disable only
const [hasInput, setHasInput] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const selectRef = useRef<HTMLSelectElement>(null);
// Separate component to manage form state
const ResearchForm: React.FC<{
prompt?: string;
onSubmit: (data: { keywords: string; blogLength: string }) => void;
onCancel: () => void;
}> = ({ prompt, onSubmit, onCancel }) => {
const [keywords, setKeywords] = useState('');
const [blogLength, setBlogLength] = useState('1000');
const hasValidInput = keywords.trim().length > 0;
// Focus input when form appears
useEffect(() => {
if (inputRef.current) {
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 100);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (hasValidInput) {
onSubmit({ keywords: keywords.trim(), blogLength });
} else {
window.alert('Please enter keywords or a topic to start research.');
}
}, []);
};
return (
<form
onSubmit={handleSubmit}
style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}
>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🔍 Let's Research Your Blog Topic
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{prompt || 'Please provide the keywords or topic you want to research for your blog:'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Keywords or Topic *
</label>
<input
type="text"
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
onFocus={(e) => e.target.select()}
placeholder="e.g., artificial intelligence, machine learning, AI trends"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoFocus
autoComplete="off"
spellCheck="false"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Blog Length (words)
</label>
<select
value={blogLength}
onChange={(e) => setBlogLength(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
>
<option value="500">500 words (Short blog)</option>
<option value="1000">1000 words (Medium blog)</option>
<option value="1500">1500 words (Long blog)</option>
<option value="2000">2000+ words (Comprehensive guide)</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
type="submit"
disabled={!hasValidInput}
style={{
backgroundColor: hasValidInput ? '#1976d2' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: hasValidInput ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
🚀 Start Research {hasValidInput ? '(Enabled)' : '(Disabled)'}
</button>
<button
type="button"
onClick={onCancel}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</form>
);
};
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
// Keyword input action with Human-in-the-Loop
useCopilotActionTyped({
@@ -51,143 +164,14 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
}
return (
<form
key="keyword-input-form"
onSubmit={(e) => {
e.preventDefault();
<ResearchForm
prompt={args.prompt}
onSubmit={(formData) => {
onKeywordsReceived?.(formData);
respond?.(JSON.stringify(formData));
}}
style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}
>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🔍 Let's Research Your Blog Topic
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{args.prompt || 'Please provide the keywords or topic you want to research for your blog:'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Keywords or Topic *
</label>
<input
ref={inputRef}
type="text"
defaultValue=""
onChange={(e) => {
const value = e.target.value;
// Update state for button enable/disable
setHasInput(value.trim().length > 0);
}}
onFocus={(e) => {
e.target.select();
}}
placeholder="e.g., artificial intelligence, machine learning, AI trends"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoFocus
autoComplete="off"
spellCheck="false"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Blog Length (words)
</label>
<select
ref={selectRef}
defaultValue="1000"
onChange={(e) => {
// No state update needed for select
}}
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
>
<option value="500">500 words (Short blog)</option>
<option value="1000">1000 words (Medium blog)</option>
<option value="1500">1500 words (Long blog)</option>
<option value="2000">2000+ words (Comprehensive guide)</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const kw = (inputRef.current?.value || '').trim();
const len = (selectRef.current?.value || '1000');
if (kw) {
const formData = {
keywords: kw,
blogLength: len
};
// Notify parent component if callback provided
onKeywordsReceived?.(formData);
// Send to CopilotKit to trigger performResearch action
respond?.(JSON.stringify(formData));
}
}}
disabled={!hasInput}
style={{
backgroundColor: hasInput ? '#1976d2' : '#f5f5f5',
color: hasInput ? 'white' : '#999',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: hasInput ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
🚀 Start Research
</button>
<button
onClick={() => {
respond?.('CANCEL');
}}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</form>
onCancel={() => respond?.('CANCEL')}
/>
);
}
});
@@ -204,7 +188,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
const data = JSON.parse(formData);
const { keywords, blogLength } = data;
// If keywords is a topic description, extract keywords from it
const keywordList = keywords.includes(',')
? keywords.split(',').map((k: string) => k.trim())
: keywords.split(' ').filter((k: string) => k.length > 2).slice(0, 5);
@@ -217,8 +200,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
};
const res = await blogWriterApi.research(payload);
// Notify parent component
onResearchComplete?.(res);
const sourcesCount = res.sources?.length || 0;
@@ -246,7 +227,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
}
},
render: ({ status }: any) => {
console.log('performResearch render called with status:', status);
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{

View File

@@ -23,7 +23,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
// If keywords is a topic description, extract keywords from it
const keywordList = keywords.includes(',')
? keywords.split(',').map(k => k.trim())
: keywords.split(' ').filter(k => k.length > 2).slice(0, 5); // Extract up to 5 meaningful words
: keywords.split(' ').filter(k => k.length > 1).slice(0, 5); // Extract up to 5 meaningful words (including 2-char words like "AI")
const payload: BlogResearchRequest = {
keywords: keywordList,
@@ -34,6 +34,15 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
const res = await blogWriterApi.research(payload);
// Check if research failed gracefully
if (!res.success) {
return {
success: false,
message: `❌ Research failed: ${res.error_message || 'Unknown error occurred'}. Please try again with different keywords or contact support if the problem persists.`,
error_details: res.error_message
};
}
// Notify parent component
onResearchComplete?.(res);
@@ -63,7 +72,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
}
},
render: ({ status }: any) => {
if (status === 'inProgress') {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
@@ -84,10 +93,12 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
<h4 style={{ margin: 0, color: '#1976d2' }}>🔍 Researching Your Topic</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Starting research operation...</p>
<p style={{ margin: '0 0 8px 0' }}> Connecting to Google Search grounding...</p>
<p style={{ margin: '0 0 8px 0' }}> Analyzing keywords and search intent...</p>
<p style={{ margin: '0 0 8px 0' }}> Gathering relevant sources and statistics...</p>
<p style={{ margin: '0' }}> Generating content angles and search queries...</p>
<p style={{ margin: '0 0 8px 0' }}> Generating content angles and search queries...</p>
<p style={{ margin: '0', fontStyle: 'italic', color: '#888' }}> This may take 1-3 minutes. Please wait...</p>
</div>
<style>{`
@keyframes spin {

View File

@@ -0,0 +1,179 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchResponse, BlogOutlineSection } from '../../services/blogWriterApi';
const useCopilotActionTyped = useCopilotAction as any;
interface ResearchDataActionsProps {
research: BlogResearchResponse | null;
onOutlineCreated: (outline: BlogOutlineSection[]) => void;
onTitleOptionsSet: (titles: string[]) => void;
}
export const ResearchDataActions: React.FC<ResearchDataActionsProps> = ({
research,
onOutlineCreated,
onTitleOptionsSet
}) => {
// Chat with Research Data
useCopilotActionTyped({
name: 'chatWithResearchData',
description: 'Chat with the research data to explore insights, ask questions, and get recommendations',
parameters: [
{ name: 'question', type: 'string', description: 'Question or topic to explore in the research data', required: true }
],
handler: async ({ question }: { question: string }) => {
if (!research) {
return {
success: false,
message: 'No research data available. Please complete research first.',
suggestion: 'Try asking: "I want to research a topic for my blog"'
};
}
// Provide comprehensive research context for the copilot to answer intelligently
const researchContext = {
sources: research.sources.length,
primaryKeywords: research.keyword_analysis?.primary || [],
secondaryKeywords: research.keyword_analysis?.secondary || [],
longTailKeywords: research.keyword_analysis?.long_tail || [],
searchIntent: research.keyword_analysis?.search_intent || 'informational',
contentAngles: research.suggested_angles || [],
competitorAnalysis: research.competitor_analysis || {},
searchQueries: research.search_queries || [],
topSources: research.sources.slice(0, 5).map(s => ({
title: s.title,
credibility: s.credibility_score,
excerpt: s.excerpt?.substring(0, 200)
}))
};
return {
success: true,
message: `I can help you explore the research data! Here's what I found:`,
research_context: researchContext,
user_question: question,
next_step_suggestion: 'Ready to create an outline? Try: "Create outline with custom inputs" or "Let\'s proceed to create an outline"'
};
},
render: ({ status, result }: any) => {
if (status === 'complete' && result?.success) {
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f8ff',
borderRadius: '8px',
border: '1px solid #1976d2',
margin: '8px 0'
}}>
<h4 style={{ margin: '0 0 12px 0', color: '#1976d2' }}>🔍 Research Data Insights</h4>
<div style={{ fontSize: '14px', color: '#333', lineHeight: '1.5', marginBottom: '12px' }}>
<p style={{ margin: '0 0 8px 0' }}><strong>Your Question:</strong> {result.user_question}</p>
<p style={{ margin: '0 0 8px 0' }}><strong>Research Summary:</strong></p>
<ul style={{ margin: '0 0 8px 0', paddingLeft: '20px' }}>
<li>{result.research_context.sources} authoritative sources found</li>
<li>Primary keywords: {result.research_context.primaryKeywords.join(', ')}</li>
<li>Search intent: {result.research_context.searchIntent}</li>
<li>{result.research_context.contentAngles.length} content angles identified</li>
</ul>
</div>
<div style={{
padding: '12px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
border: '1px solid #1976d2'
}}>
<p style={{ margin: '0 0 8px 0', fontWeight: '500', color: '#1976d2' }}>💡 Next Step:</p>
<p style={{ margin: '0', fontSize: '14px', color: '#333' }}>{result.next_step_suggestion}</p>
</div>
</div>
);
}
return null;
}
});
// Create Outline with Custom Inputs
useCopilotActionTyped({
name: 'createOutlineWithCustomInputs',
description: 'Create an outline with custom instructions and requirements from the user',
parameters: [
{ name: 'customInstructions', type: 'string', description: 'Custom instructions for outline generation', required: true }
],
handler: async ({ customInstructions }: { customInstructions: string }) => {
if (!research) {
return {
success: false,
message: 'No research data available. Please complete research first.',
suggestion: 'Try asking: "I want to research a topic for my blog"'
};
}
try {
// Create a custom outline request with user instructions
const customOutlineRequest = {
research: research,
word_count: 1500,
custom_instructions: customInstructions
};
const outlineResponse = await blogWriterApi.generateOutline(customOutlineRequest);
onOutlineCreated(outlineResponse.outline);
onTitleOptionsSet(outlineResponse.title_options);
return {
success: true,
message: `Created custom outline with ${outlineResponse.outline.length} sections based on your instructions: "${customInstructions}"`,
outline_sections: outlineResponse.outline.length,
title_options: outlineResponse.title_options.length,
next_step_suggestion: 'Great! Now you can enhance sections, generate content, or refine the outline further.'
};
} catch (error) {
return {
success: false,
message: `Custom outline creation failed: ${error}`,
suggestion: 'Try providing more specific instructions or ask me to help refine your requirements.'
};
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #673ab7',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#673ab7' }}>🎨 Creating Custom Outline</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing your custom instructions...</p>
<p style={{ margin: '0 0 8px 0' }}> Applying requirements to research data...</p>
<p style={{ margin: '0' }}> Generating tailored outline structure...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
return null; // This component only provides the CopilotKit actions, no UI
};