AI Blog Writer - Implement modular architecture with research, outline, and core services
This commit is contained in:
@@ -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');
|
||||
}}
|
||||
|
||||
142
frontend/src/components/BlogWriter/CustomOutlineForm.tsx
Normal file
142
frontend/src/components/BlogWriter/CustomOutlineForm.tsx
Normal 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
|
||||
};
|
||||
200
frontend/src/components/BlogWriter/EnhancedOutlineActions.tsx
Normal file
200
frontend/src/components/BlogWriter/EnhancedOutlineActions.tsx
Normal 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
|
||||
};
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
179
frontend/src/components/BlogWriter/ResearchDataActions.tsx
Normal file
179
frontend/src/components/BlogWriter/ResearchDataActions.tsx
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user