Research component integration, Copilotkit implementation, SEO copilotkit implementation, Wix SEO metadata complete, Wix SEO metadata review

This commit is contained in:
ajaysi
2025-11-03 16:01:44 +05:30
parent de4328175d
commit e69107b07c
94 changed files with 9748 additions and 1565 deletions

View File

@@ -0,0 +1,216 @@
import React, { useEffect } from 'react';
import { useResearchWizard } from './hooks/useResearchWizard';
import { useResearchExecution } from './hooks/useResearchExecution';
import { StepKeyword } from './steps/StepKeyword';
import { StepOptions } from './steps/StepOptions';
import { StepProgress } from './steps/StepProgress';
import { StepResults } from './steps/StepResults';
import { ResearchWizardProps } from './types/research.types';
export const ResearchWizard: React.FC<ResearchWizardProps> = ({
onComplete,
onCancel,
initialKeywords,
initialIndustry,
}) => {
const wizard = useResearchWizard(initialKeywords, initialIndustry);
const execution = useResearchExecution();
// Handle results from execution
useEffect(() => {
if (execution.result && !execution.isExecuting) {
wizard.updateState({ results: execution.result });
if (wizard.state.currentStep === 3) {
wizard.nextStep();
}
}
}, [execution.result, execution.isExecuting]);
// Handle completion callback
useEffect(() => {
if (wizard.state.results && onComplete) {
onComplete(wizard.state.results);
}
}, [wizard.state.results, onComplete]);
const renderStep = () => {
const stepProps = {
state: wizard.state,
onUpdate: wizard.updateState,
onNext: wizard.nextStep,
onBack: wizard.prevStep,
};
switch (wizard.state.currentStep) {
case 1:
return <StepKeyword {...stepProps} />;
case 2:
return <StepOptions {...stepProps} />;
case 3:
return <StepProgress {...stepProps} />;
case 4:
return <StepResults {...stepProps} />;
default:
return <StepKeyword {...stepProps} />;
}
};
return (
<div style={{
minHeight: '100vh',
backgroundColor: '#f5f5f5',
padding: '20px',
}}>
{/* Wizard Container */}
<div style={{
maxWidth: '1200px',
margin: '0 auto',
backgroundColor: 'white',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
}}>
{/* Header */}
<div style={{
backgroundColor: '#1976d2',
color: 'white',
padding: '24px',
borderBottom: '1px solid #e0e0e0',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{ margin: 0, fontSize: '24px' }}>Research Wizard</h1>
<p style={{ margin: '8px 0 0 0', fontSize: '14px', opacity: 0.9 }}>
Step {wizard.state.currentStep} of {wizard.maxSteps}
</p>
</div>
{onCancel && (
<button
onClick={onCancel}
style={{
padding: '8px 16px',
backgroundColor: 'rgba(255,255,255,0.2)',
color: 'white',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Cancel
</button>
)}
</div>
</div>
{/* Progress Bar */}
<div style={{
backgroundColor: '#f0f0f0',
height: '4px',
position: 'relative',
}}>
<div
style={{
backgroundColor: '#1976d2',
height: '100%',
width: `${(wizard.state.currentStep / wizard.maxSteps) * 100}%`,
transition: 'width 0.3s ease',
}}
/>
</div>
{/* Step Indicators */}
<div style={{
display: 'flex',
justifyContent: 'space-around',
padding: '20px 40px',
borderBottom: '1px solid #e0e0e0',
}}>
{[1, 2, 3, 4].map(step => (
<div key={step} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
<div style={{
width: '40px',
height: '40px',
borderRadius: '50%',
backgroundColor: step <= wizard.state.currentStep ? '#1976d2' : '#e0e0e0',
color: step <= wizard.state.currentStep ? 'white' : '#999',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: '16px',
marginBottom: '8px',
transition: 'all 0.3s ease',
}}>
{step < wizard.state.currentStep ? '✓' : step}
</div>
<span style={{
fontSize: '12px',
color: step <= wizard.state.currentStep ? '#1976d2' : '#999',
fontWeight: step === wizard.state.currentStep ? '600' : 'normal',
}}>
{step === 1 && 'Setup'}
{step === 2 && 'Options'}
{step === 3 && 'Research'}
{step === 4 && 'Results'}
</span>
</div>
))}
</div>
{/* Content */}
<div style={{ padding: '24px' }}>
{renderStep()}
</div>
{/* Navigation Footer */}
{wizard.state.currentStep <= 2 && (
<div style={{
padding: '20px 24px',
borderTop: '1px solid #e0e0e0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#fafafa',
}}>
<button
onClick={wizard.prevStep}
disabled={wizard.isFirstStep}
style={{
padding: '10px 20px',
backgroundColor: wizard.isFirstStep ? '#f0f0f0' : 'white',
color: wizard.isFirstStep ? '#999' : '#333',
border: wizard.isFirstStep ? '1px solid #e0e0e0' : '1px solid #ddd',
borderRadius: '6px',
cursor: wizard.isFirstStep ? 'not-allowed' : 'pointer',
fontSize: '14px',
}}
>
Back
</button>
<button
onClick={wizard.nextStep}
disabled={!wizard.canGoNext()}
style={{
padding: '10px 24px',
backgroundColor: wizard.canGoNext() ? '#1976d2' : '#e0e0e0',
color: wizard.canGoNext() ? 'white' : '#999',
border: 'none',
borderRadius: '6px',
cursor: wizard.canGoNext() ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '600',
}}
>
{wizard.isLastStep ? 'Finish' : 'Next →'}
</button>
</div>
)}
</div>
</div>
);
};
export default ResearchWizard;

View File

@@ -0,0 +1,82 @@
import { useState, useCallback } from 'react';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../../services/blogWriterApi';
import { useResearchPolling } from '../../../hooks/usePolling';
import { researchCache } from '../../../services/researchCache';
import { WizardState } from '../types/research.types';
export const useResearchExecution = () => {
const [isExecuting, setIsExecuting] = useState(false);
const [error, setError] = useState<string | null>(null);
const polling = useResearchPolling({
onComplete: (result) => {
if (result && result.keywords) {
researchCache.cacheResult(
result.keywords,
'General',
'General',
result
);
}
setIsExecuting(false);
},
onError: (error) => {
console.error('Research polling error:', error);
setError(error);
setIsExecuting(false);
}
});
const executeResearch = useCallback(async (state: WizardState): Promise<string | null> => {
setIsExecuting(true);
setError(null);
try {
// Check cache first
const cachedResult = researchCache.getCachedResult(
state.keywords,
state.industry,
state.targetAudience
);
if (cachedResult) {
setIsExecuting(false);
return 'cached';
}
const payload: BlogResearchRequest = {
keywords: state.keywords,
industry: state.industry,
target_audience: state.targetAudience,
research_mode: state.researchMode,
config: state.config,
};
const { task_id } = await blogWriterApi.startResearch(payload);
polling.startPolling(task_id);
return task_id;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
setIsExecuting(false);
return null;
}
}, [polling]);
const stopExecution = useCallback(() => {
polling.stopPolling();
setIsExecuting(false);
setError(null);
}, [polling]);
return {
executeResearch,
stopExecution,
isExecuting,
error,
progressMessages: polling.progressMessages,
currentStatus: polling.currentStatus,
result: polling.result,
};
};

View File

@@ -0,0 +1,116 @@
import { useState, useCallback, useEffect } from 'react';
import { WizardState, WizardStepProps } from '../types/research.types';
import { ResearchMode, ResearchConfig, BlogResearchResponse } from '../../../services/blogWriterApi';
const WIZARD_STATE_KEY = 'alwrity_research_wizard_state';
const MAX_STEPS = 4;
const defaultState: WizardState = {
currentStep: 1,
keywords: [],
industry: 'General',
targetAudience: 'General',
researchMode: 'basic' as ResearchMode,
config: {
mode: 'basic',
provider: 'google',
max_sources: 10,
include_statistics: true,
include_expert_quotes: true,
include_competitors: true,
include_trends: true,
},
results: null,
};
export const useResearchWizard = (initialKeywords?: string[], initialIndustry?: string) => {
const [state, setState] = useState<WizardState>(() => {
// Try to load from localStorage first
const saved = localStorage.getItem(WIZARD_STATE_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
return parsed;
} catch (e) {
console.warn('Failed to parse saved wizard state', e);
}
}
// Use defaults or initial values
return {
...defaultState,
keywords: initialKeywords || [],
industry: initialIndustry || defaultState.industry,
};
});
// Persist state to localStorage
useEffect(() => {
if (state.currentStep > 1) {
localStorage.setItem(WIZARD_STATE_KEY, JSON.stringify(state));
}
}, [state]);
const updateState = useCallback((updates: Partial<WizardState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
const nextStep = useCallback(() => {
setState(prev => {
if (prev.currentStep >= MAX_STEPS) return prev;
return { ...prev, currentStep: prev.currentStep + 1 };
});
}, []);
const prevStep = useCallback(() => {
setState(prev => {
if (prev.currentStep <= 1) return prev;
return { ...prev, currentStep: prev.currentStep - 1 };
});
}, []);
const reset = useCallback(() => {
const resetState = {
...defaultState,
keywords: initialKeywords || [],
industry: initialIndustry || defaultState.industry,
};
setState(resetState);
localStorage.removeItem(WIZARD_STATE_KEY);
}, [initialKeywords, initialIndustry]);
const clearResults = useCallback(() => {
setState(prev => ({ ...prev, results: null }));
}, []);
const canGoNext = useCallback((): boolean => {
switch (state.currentStep) {
case 1:
return state.keywords.length > 0 && state.keywords.every(k => k.trim().length > 0);
case 2:
return true; // Mode selection always allowed
case 3:
return false; // Progress can't be skipped
case 4:
return false; // Results can't be skipped
default:
return false;
}
}, [state]);
return {
state,
updateState,
nextStep,
prevStep,
reset,
clearResults,
canGoNext,
isFirstStep: state.currentStep === 1,
isLastStep: state.currentStep === MAX_STEPS,
maxSteps: MAX_STEPS,
};
};
export type UseResearchWizardReturn = ReturnType<typeof useResearchWizard>;

View File

@@ -0,0 +1,5 @@
export { ResearchWizard } from './ResearchWizard';
export { useResearchWizard } from './hooks/useResearchWizard';
export { useResearchExecution } from './hooks/useResearchExecution';
export * from './types/research.types';

View File

@@ -0,0 +1,72 @@
/**
* Blog Writer Integration Adapter for Research Component
*
* This adapter provides a simple way to integrate the ResearchWizard
* into the BlogWriter's research phase.
*/
import React from 'react';
import { ResearchWizard } from '../ResearchWizard';
import { BlogResearchResponse } from '../../../services/blogWriterApi';
interface BlogWriterResearchAdapterProps {
onResearchComplete: (research: BlogResearchResponse) => void;
onCancel?: () => void;
initialKeywords?: string[];
initialIndustry?: string;
}
/**
* Adapter component that wraps ResearchWizard for BlogWriter integration.
* Provides a clean interface for switching between CopilotKit and wizard-based research.
*/
export const BlogWriterResearchAdapter: React.FC<BlogWriterResearchAdapterProps> = ({
onResearchComplete,
onCancel,
initialKeywords,
initialIndustry,
}) => {
return (
<div style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'white',
}}>
<ResearchWizard
onComplete={onResearchComplete}
onCancel={onCancel}
initialKeywords={initialKeywords}
initialIndustry={initialIndustry}
/>
</div>
);
};
export default BlogWriterResearchAdapter;
/**
* USAGE EXAMPLE:
*
* In BlogWriter.tsx, replace the research phase content with:
*
* {currentPhase === 'research' && !research && (
* <BlogWriterResearchAdapter
* onResearchComplete={(res) => {
* handleResearchComplete(res);
* // Optionally auto-advance to outline phase
* navigateToPhase('outline');
* }}
* onCancel={() => {
* // Navigate back to dashboard
* navigateToPhase('research');
* }}
* initialKeywords={[]}
* initialIndustry="General"
* />
* )}
*
* Note: This maintains backward compatibility. The existing CopilotKit/manual
* research flow continues to work. This provides an alternative UI option.
*/

View File

@@ -0,0 +1,133 @@
import React, { useEffect } from 'react';
import { WizardStepProps } from '../types/research.types';
const industries = [
'General',
'Technology',
'Business',
'Marketing',
'Finance',
'Healthcare',
'Education',
'Real Estate',
'Entertainment',
'Food & Beverage',
'Travel',
'Fashion',
'Sports',
'Science',
'Law',
'Other',
];
export const StepKeyword: React.FC<WizardStepProps> = ({ state, onUpdate }) => {
const handleKeywordsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const keywords = value.split(',').map(k => k.trim()).filter(Boolean);
onUpdate({ keywords });
};
const handleIndustryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onUpdate({ industry: e.target.value });
};
const handleAudienceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onUpdate({ targetAudience: e.target.value });
};
const keywordText = state.keywords.join(', ');
return (
<div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto' }}>
<h2 style={{ marginBottom: '8px', color: '#333' }}>🔍 Research Setup</h2>
<p style={{ marginBottom: '24px', color: '#666', fontSize: '15px' }}>
Enter your keywords, industry, and target audience to start research.
</p>
{/* Keywords Input */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: '600', color: '#555' }}>
Keywords *
</label>
<textarea
value={keywordText}
onChange={handleKeywordsChange}
placeholder="e.g., AI in marketing, automation tools, customer engagement"
rows={4}
style={{
width: '100%',
padding: '12px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'vertical',
boxSizing: 'border-box',
}}
/>
<p style={{ marginTop: '4px', fontSize: '12px', color: '#888' }}>
Separate multiple keywords with commas
</p>
</div>
{/* Industry Selection */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: '600', color: '#555' }}>
Industry
</label>
<select
value={state.industry}
onChange={handleIndustryChange}
style={{
width: '100%',
padding: '12px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
fontFamily: 'inherit',
backgroundColor: 'white',
cursor: 'pointer',
}}
>
{industries.map(ind => (
<option key={ind} value={ind}>{ind}</option>
))}
</select>
</div>
{/* Target Audience */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: '600', color: '#555' }}>
Target Audience
</label>
<input
type="text"
value={state.targetAudience}
onChange={handleAudienceChange}
placeholder="e.g., Digital marketers, Small business owners"
style={{
width: '100%',
padding: '12px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
fontFamily: 'inherit',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{
padding: '12px',
backgroundColor: '#f0f7ff',
borderRadius: '8px',
border: '1px solid #b3d9ff',
fontSize: '13px',
color: '#004085',
}}>
💡 <strong>Tip:</strong> Be specific with your keywords. The more precise your keywords, the better your research results.
</div>
</div>
);
};

View File

@@ -0,0 +1,182 @@
import React from 'react';
import { WizardStepProps, ModeCardInfo } from '../types/research.types';
import { ResearchProvider } from '../../../services/blogWriterApi';
const modeCards: ModeCardInfo[] = [
{
mode: 'basic',
title: 'Basic Research',
description: 'Quick keyword-focused analysis for fast results',
features: [
'Primary & secondary keywords',
'Current trends overview',
'Top 5 content angles',
'Key statistics',
],
icon: '⚡',
},
{
mode: 'comprehensive',
title: 'Comprehensive Research',
description: 'Deep analysis with full competitive intelligence',
features: [
'All basic features',
'Expert quotes & opinions',
'Competitor analysis',
'Market forecasts',
'Best practices & case studies',
'Content gaps identification',
],
icon: '📊',
},
{
mode: 'targeted',
title: 'Targeted Research',
description: 'Customize what you need most',
features: [
'Select specific components',
'Choose date ranges',
'Filter source types',
'Control depth & scope',
],
icon: '🎯',
},
];
export const StepOptions: React.FC<WizardStepProps> = ({ state, onUpdate }) => {
const handleModeChange = (mode: any) => {
onUpdate({ researchMode: mode });
};
const handleProviderChange = (provider: ResearchProvider) => {
onUpdate({ config: { ...state.config, provider } });
};
return (
<div style={{ padding: '24px', maxWidth: '1000px', margin: '0 auto' }}>
<h2 style={{ marginBottom: '8px', color: '#333' }}>Choose Research Mode</h2>
<p style={{ marginBottom: '24px', color: '#666', fontSize: '15px' }}>
Select the type of research that best fits your needs.
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
gap: '20px',
marginBottom: '24px',
}}>
{modeCards.map(card => (
<div
key={card.mode}
onClick={() => handleModeChange(card.mode)}
style={{
border: state.researchMode === card.mode ? '2px solid #1976d2' : '2px solid #e0e0e0',
borderRadius: '12px',
padding: '24px',
cursor: 'pointer',
transition: 'all 0.2s ease',
backgroundColor: state.researchMode === card.mode ? '#f0f7ff' : 'white',
}}
onMouseEnter={(e) => {
if (state.researchMode !== card.mode) {
e.currentTarget.style.borderColor = '#90caf9';
e.currentTarget.style.backgroundColor = '#fafafa';
}
}}
onMouseLeave={(e) => {
if (state.researchMode !== card.mode) {
e.currentTarget.style.borderColor = '#e0e0e0';
e.currentTarget.style.backgroundColor = 'white';
}
}}
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontSize: '32px', marginRight: '12px' }}>{card.icon}</span>
<h3 style={{ margin: 0, color: '#333', fontSize: '18px' }}>{card.title}</h3>
</div>
<p style={{ marginBottom: '16px', color: '#666', fontSize: '14px' }}>
{card.description}
</p>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '13px', color: '#555' }}>
{card.features.map((feature, idx) => (
<li key={idx} style={{ marginBottom: '4px' }}>{feature}</li>
))}
</ul>
{state.researchMode === card.mode && (
<div style={{
marginTop: '16px',
padding: '8px',
backgroundColor: '#1976d2',
color: 'white',
borderRadius: '6px',
textAlign: 'center',
fontSize: '13px',
fontWeight: '600',
}}>
Selected
</div>
)}
</div>
))}
</div>
{state.researchMode !== 'basic' && (
<div style={{ marginBottom: '24px', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '15px', backgroundColor: '#f9f9f9' }}>
<h3 style={{ marginTop: 0, marginBottom: '12px', color: '#555', fontSize: '16px' }}>
🔍 Research Provider
</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<div
onClick={() => handleProviderChange('google')}
style={{
flex: 1,
padding: '12px',
border: '2px solid',
borderColor: (state.config.provider === 'google' || !state.config.provider) ? '#1976d2' : '#ddd',
backgroundColor: (state.config.provider === 'google' || !state.config.provider) ? '#e3f2fd' : 'white',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Google Search</div>
<div style={{ fontSize: '11px', color: '#666' }}>
Fast, broad coverage, trending topics
</div>
</div>
<div
onClick={() => handleProviderChange('exa')}
style={{
flex: 1,
padding: '12px',
border: '2px solid',
borderColor: state.config.provider === 'exa' ? '#7c3aed' : '#ddd',
backgroundColor: state.config.provider === 'exa' ? '#f3e8ff' : 'white',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Exa Neural</div>
<div style={{ fontSize: '11px', color: '#666' }}>
Deep research, rich citations, semantic search
</div>
</div>
</div>
</div>
)}
<div style={{
padding: '12px',
backgroundColor: '#fff3e0',
borderRadius: '8px',
border: '1px solid #ffb74d',
fontSize: '13px',
color: '#e65100',
}}>
<strong>Note:</strong> You can always run additional research if you need more information later.
</div>
</div>
);
};

View File

@@ -0,0 +1,153 @@
import React, { useEffect } from 'react';
import { WizardStepProps } from '../types/research.types';
import { useResearchExecution } from '../hooks/useResearchExecution';
export const StepProgress: React.FC<WizardStepProps> = ({ state, onNext, onUpdate }) => {
const { executeResearch, stopExecution, isExecuting, error, progressMessages, currentStatus } = useResearchExecution();
useEffect(() => {
// Start research when this step is reached
const startResearch = async () => {
const taskId = await executeResearch(state);
if (taskId === 'cached') {
// If cached, move to results immediately
// The parent will handle this
}
};
startResearch();
return () => {
if (isExecuting) {
stopExecution();
}
};
}, []); // Run once on mount
// Move to next step when research completes
useEffect(() => {
if (!isExecuting && progressMessages.length > 0) {
// Small delay to show final message
const timer = setTimeout(() => {
onNext();
}, 1000);
return () => clearTimeout(timer);
}
}, [isExecuting, progressMessages.length, onNext]);
const getStatusIcon = () => {
if (error) return '❌';
if (!isExecuting && progressMessages.length > 0) return '✅';
if (currentStatus === 'completed') return '✅';
return '🔄';
};
const getStatusColor = () => {
if (error) return '#f44336';
if (!isExecuting && progressMessages.length > 0) return '#4caf50';
return '#1976d2';
};
return (
<div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto' }}>
<h2 style={{ marginBottom: '8px', color: '#333' }}>Researching...</h2>
<p style={{ marginBottom: '24px', color: '#666', fontSize: '15px' }}>
Gathering insights from Google Search grounding
</p>
{/* Status Display */}
<div style={{
backgroundColor: '#f5f5f5',
borderRadius: '12px',
padding: '24px',
marginBottom: '24px',
textAlign: 'center',
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>{getStatusIcon()}</div>
{error ? (
<>
<h3 style={{ color: getStatusColor(), marginBottom: '8px' }}>Error</h3>
<p style={{ color: '#666', fontSize: '14px' }}>{error}</p>
<button
onClick={() => window.location.reload()}
style={{
marginTop: '16px',
padding: '8px 16px',
backgroundColor: '#1976d2',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Retry
</button>
</>
) : (
<>
<h3 style={{ color: getStatusColor(), marginBottom: '8px' }}>
{currentStatus === 'completed' ? 'Complete!' : 'In Progress'}
</h3>
<p style={{ color: '#666', fontSize: '14px' }}>
{isExecuting ? 'Analyzing sources and generating insights...' : 'Finalizing results...'}
</p>
</>
)}
</div>
{/* Progress Messages */}
{progressMessages.length > 0 && (
<div style={{
backgroundColor: 'white',
border: '1px solid #e0e0e0',
borderRadius: '8px',
maxHeight: '300px',
overflow: 'auto',
}}>
<div style={{ padding: '16px', borderBottom: '1px solid #e0e0e0' }}>
<strong style={{ fontSize: '14px', color: '#333' }}>Progress Updates</strong>
</div>
{progressMessages.map((msg, idx) => (
<div
key={idx}
style={{
padding: '12px 16px',
borderBottom: idx < progressMessages.length - 1 ? '1px solid #f0f0f0' : 'none',
fontSize: '13px',
color: '#555',
}}
>
{idx === progressMessages.length - 1 && isExecuting && (
<span style={{ marginRight: '8px' }}>🔄</span>
)}
{msg.message}
</div>
))}
</div>
)}
{/* Cancel Button */}
{isExecuting && (
<div style={{ marginTop: '24px', textAlign: 'center' }}>
<button
onClick={stopExecution}
style={{
padding: '8px 16px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Cancel Research
</button>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { WizardStepProps } from '../types/research.types';
import { ResearchResults } from '../../BlogWriter/ResearchResults';
import { BlogResearchResponse } from '../../../services/blogWriterApi';
export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
if (!state.results) {
return (
<div style={{ padding: '24px', textAlign: 'center' }}>
<p style={{ color: '#666' }}>No results available</p>
</div>
);
}
const handleExport = () => {
const dataStr = JSON.stringify(state.results, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `research-${state.keywords.join('-')}-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '24px',
flexWrap: 'wrap',
gap: '16px',
}}>
<h2 style={{ margin: 0, color: '#333' }}>Research Results</h2>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
onClick={handleExport}
style={{
padding: '8px 16px',
backgroundColor: '#1976d2',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
📥 Export JSON
</button>
<button
onClick={onBack}
style={{
padding: '8px 16px',
backgroundColor: '#f5f5f5',
color: '#333',
border: '1px solid #ddd',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Start New Research
</button>
</div>
</div>
{/* Results Display */}
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
border: '1px solid #e0e0e0',
overflow: 'hidden',
}}>
<ResearchResults research={state.results} />
</div>
{/* Action Section */}
<div style={{
marginTop: '24px',
padding: '16px',
backgroundColor: '#f0f7ff',
borderRadius: '8px',
border: '1px solid #b3d9ff',
}}>
<h4 style={{ marginBottom: '8px', color: '#004085' }}>Next Steps</h4>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#004085', fontSize: '14px' }}>
<li>Review the research insights and sources</li>
<li>Explore content angles and competitor analysis</li>
<li>Use this research to create your blog outline</li>
<li>Export the data for reference</li>
</ul>
</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
import { BlogResearchResponse, ResearchMode, ResearchConfig } from '../../../services/blogWriterApi';
export interface WizardState {
currentStep: number;
keywords: string[];
industry: string;
targetAudience: string;
researchMode: ResearchMode;
config: ResearchConfig;
results: BlogResearchResponse | null;
}
export interface WizardStepProps {
state: WizardState;
onUpdate: (updates: Partial<WizardState>) => void;
onNext: () => void;
onBack: () => void;
}
export interface ResearchWizardProps {
onComplete?: (results: BlogResearchResponse) => void;
onCancel?: () => void;
initialKeywords?: string[];
initialIndustry?: string;
}
export interface ModeCardInfo {
mode: ResearchMode;
title: string;
description: string;
features: string[];
icon: string;
}

View File

@@ -0,0 +1,17 @@
// Utility functions for research component
export const formatKeywords = (keywords: string[]): string => {
return keywords.join(', ');
};
export const parseKeywords = (keywordsString: string): string[] => {
return keywordsString
.split(',')
.map(k => k.trim())
.filter(Boolean);
};
export const validateKeywords = (keywords: string[]): boolean => {
return keywords.length > 0 && keywords.every(k => k.trim().length > 0);
};