Research Wizard and CopilotKit mitigation review
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
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 { ResearchInput } from './steps/ResearchInput';
|
||||
import { StepProgress } from './steps/StepProgress';
|
||||
import { StepResults } from './steps/StepResults';
|
||||
import { ResearchWizardProps } from './types/research.types';
|
||||
@@ -19,12 +18,17 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
// Handle results from execution
|
||||
useEffect(() => {
|
||||
if (execution.result && !execution.isExecuting) {
|
||||
console.log('[ResearchWizard] Results received, updating state and navigating:', {
|
||||
hasResults: !!execution.result,
|
||||
currentStep: wizard.state.currentStep,
|
||||
shouldNavigate: wizard.state.currentStep === 2
|
||||
});
|
||||
wizard.updateState({ results: execution.result });
|
||||
if (wizard.state.currentStep === 3) {
|
||||
if (wizard.state.currentStep === 2) {
|
||||
wizard.nextStep();
|
||||
}
|
||||
}
|
||||
}, [execution.result, execution.isExecuting]);
|
||||
}, [execution.result, execution.isExecuting]); // Don't depend on currentStep to avoid loops
|
||||
|
||||
// Handle completion callback
|
||||
useEffect(() => {
|
||||
@@ -43,61 +47,79 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
|
||||
switch (wizard.state.currentStep) {
|
||||
case 1:
|
||||
return <StepKeyword {...stepProps} />;
|
||||
return <ResearchInput {...stepProps} />;
|
||||
case 2:
|
||||
return <StepOptions {...stepProps} />;
|
||||
return <StepProgress {...stepProps} execution={execution} />;
|
||||
case 3:
|
||||
return <StepProgress {...stepProps} />;
|
||||
case 4:
|
||||
return <StepResults {...stepProps} />;
|
||||
default:
|
||||
return <StepKeyword {...stepProps} />;
|
||||
return <ResearchInput {...stepProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '20px',
|
||||
}}>
|
||||
<div>
|
||||
{/* Wizard Container */}
|
||||
<div style={{
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 4px 16px rgba(14, 165, 233, 0.1)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
padding: '24px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.08) 0%, rgba(56, 189, 248, 0.08) 100%)',
|
||||
borderBottom: '1px solid rgba(14, 165, 233, 0.15)',
|
||||
padding: '20px 28px',
|
||||
}}>
|
||||
<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}
|
||||
<h1 style={{
|
||||
margin: 0,
|
||||
fontSize: '24px',
|
||||
fontWeight: '700',
|
||||
color: '#0c4a6e',
|
||||
}}>
|
||||
Research Wizard
|
||||
</h1>
|
||||
<p style={{
|
||||
margin: '4px 0 0 0',
|
||||
fontSize: '13px',
|
||||
color: '#0369a1',
|
||||
fontWeight: '400',
|
||||
}}>
|
||||
Phase {wizard.state.currentStep} of {wizard.maxSteps} • AI-Powered Intelligence
|
||||
</p>
|
||||
</div>
|
||||
{onCancel && (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
onClick={() => {
|
||||
wizard.reset();
|
||||
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',
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
color: '#dc2626',
|
||||
border: '1px solid rgba(239, 68, 68, 0.25)',
|
||||
borderRadius: '10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
✕ Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -105,16 +127,18 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div style={{
|
||||
backgroundColor: '#f0f0f0',
|
||||
height: '4px',
|
||||
background: 'rgba(14, 165, 233, 0.1)',
|
||||
height: '5px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#1976d2',
|
||||
background: 'linear-gradient(90deg, #0ea5e9 0%, #38bdf8 100%)',
|
||||
height: '100%',
|
||||
width: `${(wizard.state.currentStep / wizard.maxSteps) * 100}%`,
|
||||
transition: 'width 0.3s ease',
|
||||
transition: 'width 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
boxShadow: '0 0 8px rgba(14, 165, 233, 0.4)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -123,67 +147,123 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
padding: '20px 40px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
padding: '24px 40px',
|
||||
borderBottom: '1px solid rgba(14, 165, 233, 0.15)',
|
||||
background: 'rgba(14, 165, 233, 0.03)',
|
||||
}}>
|
||||
{[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}
|
||||
{[1, 2, 3].map(step => {
|
||||
const isActive = step === wizard.state.currentStep;
|
||||
const isCompleted = step < wizard.state.currentStep;
|
||||
const isClickable = step <= wizard.state.currentStep;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
cursor: isClickable ? 'pointer' : 'default',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isClickable) {
|
||||
wizard.updateState({ currentStep: step });
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (isClickable) {
|
||||
e.currentTarget.style.transform = 'scale(1.05)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%',
|
||||
background: isActive
|
||||
? 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)'
|
||||
: isCompleted
|
||||
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
||||
: 'rgba(14, 165, 233, 0.1)',
|
||||
color: (isActive || isCompleted) ? 'white' : '#64748b',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '700',
|
||||
fontSize: '18px',
|
||||
marginBottom: '10px',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
border: isActive ? '2px solid rgba(14, 165, 233, 0.3)' : '2px solid rgba(14, 165, 233, 0.1)',
|
||||
boxShadow: isActive
|
||||
? '0 4px 16px rgba(14, 165, 233, 0.3)'
|
||||
: isCompleted
|
||||
? '0 2px 8px rgba(34, 197, 94, 0.2)'
|
||||
: 'none',
|
||||
}}>
|
||||
{isCompleted ? '✓' : step}
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: '13px',
|
||||
color: (isActive || isCompleted) ? '#0c4a6e' : '#64748b',
|
||||
fontWeight: isActive ? '600' : '400',
|
||||
letterSpacing: '0.01em',
|
||||
}}>
|
||||
{step === 1 && 'Configure'}
|
||||
{step === 2 && 'Execute'}
|
||||
{step === 3 && 'Analyze'}
|
||||
</span>
|
||||
</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' }}>
|
||||
<div style={{ padding: '20px' }}>
|
||||
{renderStep()}
|
||||
</div>
|
||||
|
||||
{/* Navigation Footer */}
|
||||
{wizard.state.currentStep <= 2 && (
|
||||
{wizard.state.currentStep < 3 && (
|
||||
<div style={{
|
||||
padding: '20px 24px',
|
||||
borderTop: '1px solid #e0e0e0',
|
||||
padding: '20px 28px',
|
||||
borderTop: '1px solid rgba(14, 165, 233, 0.15)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fafafa',
|
||||
background: 'rgba(14, 165, 233, 0.03)',
|
||||
}}>
|
||||
<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',
|
||||
padding: '10px 24px',
|
||||
background: wizard.isFirstStep ? 'rgba(100, 116, 139, 0.1)' : 'rgba(255, 255, 255, 0.8)',
|
||||
color: wizard.isFirstStep ? '#94a3b8' : '#0c4a6e',
|
||||
border: `1px solid ${wizard.isFirstStep ? 'rgba(100, 116, 139, 0.2)' : 'rgba(14, 165, 233, 0.2)'}`,
|
||||
borderRadius: '10px',
|
||||
cursor: wizard.isFirstStep ? 'not-allowed' : 'pointer',
|
||||
fontSize: '14px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!wizard.isFirstStep) {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 1)';
|
||||
e.currentTarget.style.transform = 'translateX(-4px)';
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!wizard.isFirstStep) {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.8)';
|
||||
e.currentTarget.style.transform = 'translateX(0)';
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
← Back
|
||||
@@ -194,16 +274,32 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
disabled={!wizard.canGoNext()}
|
||||
style={{
|
||||
padding: '10px 24px',
|
||||
backgroundColor: wizard.canGoNext() ? '#1976d2' : '#e0e0e0',
|
||||
color: wizard.canGoNext() ? 'white' : '#999',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
background: wizard.canGoNext()
|
||||
? 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)'
|
||||
: 'rgba(100, 116, 139, 0.2)',
|
||||
color: wizard.canGoNext() ? 'white' : '#94a3b8',
|
||||
border: wizard.canGoNext() ? 'none' : '1px solid rgba(100, 116, 139, 0.2)',
|
||||
borderRadius: '10px',
|
||||
cursor: wizard.canGoNext() ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: wizard.canGoNext() ? '0 2px 8px rgba(14, 165, 233, 0.3)' : 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (wizard.canGoNext()) {
|
||||
e.currentTarget.style.transform = 'translateX(4px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(14, 165, 233, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (wizard.canGoNext()) {
|
||||
e.currentTarget.style.transform = 'translateX(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{wizard.isLastStep ? 'Finish' : 'Next →'}
|
||||
{wizard.isLastStep ? 'Finish' : 'Continue →'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 MAX_STEPS = 3; // Input (combined) -> Progress -> Results
|
||||
|
||||
const defaultState: WizardState = {
|
||||
currentStep: 1,
|
||||
@@ -88,11 +88,9 @@ export const useResearchWizard = (initialKeywords?: string[], initialIndustry?:
|
||||
case 1:
|
||||
return state.keywords.length > 0 && state.keywords.every(k => k.trim().length > 0);
|
||||
case 2:
|
||||
return true; // Mode selection always allowed
|
||||
return !!state.results; // Can proceed if we have results
|
||||
case 3:
|
||||
return false; // Progress can't be skipped
|
||||
case 4:
|
||||
return false; // Results can't be skipped
|
||||
return false; // Results is the last step
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
571
frontend/src/components/Research/steps/ResearchInput.tsx
Normal file
571
frontend/src/components/Research/steps/ResearchInput.tsx
Normal file
@@ -0,0 +1,571 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { WizardStepProps } from '../types/research.types';
|
||||
import { ResearchProvider } from '../../../services/blogWriterApi';
|
||||
|
||||
const industries = [
|
||||
'General',
|
||||
'Technology',
|
||||
'Business',
|
||||
'Marketing',
|
||||
'Finance',
|
||||
'Healthcare',
|
||||
'Education',
|
||||
'Real Estate',
|
||||
'Entertainment',
|
||||
'Food & Beverage',
|
||||
'Travel',
|
||||
'Fashion',
|
||||
'Sports',
|
||||
'Science',
|
||||
'Law',
|
||||
'Other',
|
||||
];
|
||||
|
||||
const researchModes = [
|
||||
{ value: 'basic', label: 'Basic - Quick insights' },
|
||||
{ value: 'comprehensive', label: 'Comprehensive - In-depth analysis' },
|
||||
{ value: 'targeted', label: 'Targeted - Specific focus' },
|
||||
];
|
||||
|
||||
const providers = [
|
||||
{ value: 'google', label: '🔍 Google Search' },
|
||||
{ value: 'exa', label: '🧠 Exa Neural Search' },
|
||||
];
|
||||
|
||||
const exaCategories = [
|
||||
{ value: '', label: 'All Categories' },
|
||||
{ value: 'company', label: 'Company Profiles' },
|
||||
{ value: 'research paper', label: 'Research Papers' },
|
||||
{ value: 'news', label: 'News Articles' },
|
||||
{ value: 'linkedin profile', label: 'LinkedIn Profiles' },
|
||||
{ value: 'github', label: 'GitHub Repos' },
|
||||
{ value: 'tweet', label: 'Tweets' },
|
||||
{ value: 'movie', label: 'Movies' },
|
||||
{ value: 'song', label: 'Songs' },
|
||||
{ value: 'personal site', label: 'Personal Sites' },
|
||||
{ value: 'pdf', label: 'PDF Documents' },
|
||||
{ value: 'financial report', label: 'Financial Reports' },
|
||||
];
|
||||
|
||||
const exaSearchTypes = [
|
||||
{ value: 'auto', label: 'Auto - Let AI decide' },
|
||||
{ value: 'keyword', label: 'Keyword - Precise matching' },
|
||||
{ value: 'neural', label: 'Neural - Semantic search' },
|
||||
];
|
||||
|
||||
// Dynamic placeholder examples showcasing research capabilities
|
||||
const placeholderExamples = [
|
||||
"AI-powered content marketing strategies for SaaS startups\n\nExplores:\n• Latest automation tools and platforms\n• ROI optimization techniques\n• Multi-channel campaign orchestration\n• Data-driven personalization strategies",
|
||||
"Sustainable supply chain management in manufacturing\n\nCovers:\n• Green logistics and carbon footprint reduction\n• Blockchain for transparency and traceability\n• Circular economy implementation frameworks\n• Real-time inventory optimization with AI",
|
||||
"Emerging trends in telemedicine and remote patient monitoring\n\nIncludes:\n• Wearable device integration and IoT sensors\n• HIPAA-compliant data transmission protocols\n• AI-assisted diagnostic accuracy improvements\n• Patient engagement and adherence strategies",
|
||||
"Cryptocurrency regulation and institutional adoption\n\nAnalyzes:\n• Global regulatory frameworks and compliance\n• Institutional investment trends (2024-2025)\n• DeFi integration with traditional finance\n• Risk management and security best practices",
|
||||
"Voice search optimization and conversational AI for e-commerce\n\nFeatures:\n• Natural language processing advancements\n• Smart speaker integration strategies\n• Voice-enabled checkout experiences\n• Personalization through voice analytics"
|
||||
];
|
||||
|
||||
export const ResearchInput: React.FC<WizardStepProps> = ({ state, onUpdate }) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
|
||||
|
||||
// Rotate placeholder examples every 4 seconds
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentPlaceholder((prev) => (prev + 1) % placeholderExamples.length);
|
||||
}, 4000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
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 handleModeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const mode = e.target.value as any;
|
||||
onUpdate({ researchMode: mode });
|
||||
};
|
||||
|
||||
const handleProviderChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const provider = e.target.value as ResearchProvider;
|
||||
onUpdate({ config: { ...state.config, provider } });
|
||||
};
|
||||
|
||||
const handleExaCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
onUpdate({ config: { ...state.config, exa_category: value || undefined } });
|
||||
};
|
||||
|
||||
const handleExaSearchTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value as 'auto' | 'keyword' | 'neural';
|
||||
onUpdate({ config: { ...state.config, exa_search_type: value } });
|
||||
};
|
||||
|
||||
const handleIncludeDomainsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const domains = value.split(',').map(d => d.trim()).filter(Boolean);
|
||||
onUpdate({ config: { ...state.config, exa_include_domains: domains } });
|
||||
};
|
||||
|
||||
const handleExcludeDomainsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const domains = value.split(',').map(d => d.trim()).filter(Boolean);
|
||||
onUpdate({ config: { ...state.config, exa_exclude_domains: domains } });
|
||||
};
|
||||
|
||||
const handleFileUpload = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
console.log('File selected:', file.name);
|
||||
// TODO: Implement file upload logic
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
{/* Main Input Area */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
border: '2px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.4)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 20px rgba(14, 165, 233, 0.12)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<label style={{
|
||||
marginBottom: '12px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
color: '#0c4a6e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: '20px',
|
||||
}}>🔍</span>
|
||||
Research Topic & Keywords
|
||||
</label>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<textarea
|
||||
value={state.keywords.join(', ')}
|
||||
onChange={handleKeywordsChange}
|
||||
placeholder={placeholderExamples[currentPlaceholder]}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '160px',
|
||||
padding: '16px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
border: '1px solid rgba(14, 165, 233, 0.15)',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
resize: 'vertical',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: 'inset 0 1px 3px rgba(14, 165, 233, 0.05)',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.5)';
|
||||
e.currentTarget.style.boxShadow = 'inset 0 1px 3px rgba(14, 165, 233, 0.05), 0 0 0 3px rgba(14, 165, 233, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.15)';
|
||||
e.currentTarget.style.boxShadow = 'inset 0 1px 3px rgba(14, 165, 233, 0.05)';
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* File Upload Button */}
|
||||
<button
|
||||
onClick={handleFileUpload}
|
||||
type="button"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '12px',
|
||||
right: '12px',
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(14, 165, 233, 0.1)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
color: '#0369a1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(14, 165, 233, 0.15)';
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.3)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(14, 165, 233, 0.1)';
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
}}
|
||||
>
|
||||
📎 Upload Document
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept=".txt,.doc,.docx,.pdf"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
fontSize: '12px',
|
||||
color: '#64748b',
|
||||
lineHeight: '1.5',
|
||||
}}>
|
||||
💡 Tip: Describe your research topic in detail. Include specific keywords, questions, or aspects you want to explore. The AI will find relevant sources and insights.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Options */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '16px',
|
||||
marginBottom: '20px',
|
||||
}}>
|
||||
{/* Industry */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#0c4a6e',
|
||||
}}>
|
||||
Industry
|
||||
</label>
|
||||
<select
|
||||
value={state.industry}
|
||||
onChange={handleIndustryChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.5)';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{industries.map(ind => (
|
||||
<option key={ind} value={ind}>{ind}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Research Mode */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#0c4a6e',
|
||||
}}>
|
||||
Research Depth
|
||||
</label>
|
||||
<select
|
||||
value={state.researchMode}
|
||||
onChange={handleModeChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.5)';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{researchModes.map(mode => (
|
||||
<option key={mode.value} value={mode.value}>{mode.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Provider (only for Comprehensive/Targeted) */}
|
||||
{state.researchMode !== 'basic' && (
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#0c4a6e',
|
||||
}}>
|
||||
Search Provider
|
||||
</label>
|
||||
<select
|
||||
value={state.config.provider}
|
||||
onChange={handleProviderChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.5)';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{providers.map(prov => (
|
||||
<option key={prov.value} value={prov.value}>{prov.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Exa-Specific Options */}
|
||||
{state.config.provider === 'exa' && state.researchMode !== 'basic' && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%)',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '14px',
|
||||
padding: '16px',
|
||||
marginBottom: '20px',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '14px',
|
||||
}}>
|
||||
<span style={{ fontSize: '18px' }}>🧠</span>
|
||||
<strong style={{ color: '#6b21a8', fontSize: '13px' }}>Exa Neural Search Options</strong>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||||
gap: '12px',
|
||||
marginBottom: '12px',
|
||||
}}>
|
||||
{/* Exa Category */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Content Category
|
||||
</label>
|
||||
<select
|
||||
value={state.config.exa_category || ''}
|
||||
onChange={handleExaCategoryChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{exaCategories.map(cat => (
|
||||
<option key={cat.value} value={cat.value}>{cat.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Exa Search Type */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Search Algorithm
|
||||
</label>
|
||||
<select
|
||||
value={state.config.exa_search_type || 'auto'}
|
||||
onChange={handleExaSearchTypeChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{exaSearchTypes.map(type => (
|
||||
<option key={type.value} value={type.value}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain Filters */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Include Domains (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.config.exa_include_domains?.join(', ') || ''}
|
||||
onChange={handleIncludeDomainsChange}
|
||||
placeholder="e.g., nature.com, arxiv.org"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Exclude Domains (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.config.exa_exclude_domains?.join(', ') || ''}
|
||||
onChange={handleExcludeDomainsChange}
|
||||
placeholder="e.g., spam.com, ads.com"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Target Audience (Optional) */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
color: '#0c4a6e',
|
||||
}}>
|
||||
Target Audience (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.targetAudience}
|
||||
onChange={handleAudienceChange}
|
||||
placeholder="e.g., Marketing professionals, Tech enthusiasts, Business owners"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid rgba(14, 165, 233, 0.2)',
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.5)';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(14, 165, 233, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,20 @@
|
||||
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();
|
||||
export const StepProgress: React.FC<WizardStepProps> = ({ state, onNext, onUpdate, execution }) => {
|
||||
const { executeResearch, stopExecution, isExecuting, error, progressMessages, currentStatus } = execution || {
|
||||
executeResearch: async () => null,
|
||||
stopExecution: () => {},
|
||||
isExecuting: false,
|
||||
error: 'No execution provided',
|
||||
progressMessages: [],
|
||||
currentStatus: 'idle'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Only start research if execution is available
|
||||
if (!execution) return;
|
||||
|
||||
// Start research when this step is reached
|
||||
const startResearch = async () => {
|
||||
const taskId = await executeResearch(state);
|
||||
@@ -22,18 +31,19 @@ export const StepProgress: React.FC<WizardStepProps> = ({ state, onNext, onUpdat
|
||||
stopExecution();
|
||||
}
|
||||
};
|
||||
}, []); // Run once on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Run once on mount - stable references
|
||||
|
||||
// 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]);
|
||||
// Note: Navigation to next step is handled by ResearchWizard when results are received
|
||||
|
||||
// Handle missing execution gracefully
|
||||
if (!execution) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<p style={{ color: '#666' }}>Loading execution...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (error) return '❌';
|
||||
@@ -48,11 +58,14 @@ export const StepProgress: React.FC<WizardStepProps> = ({ state, onNext, onUpdat
|
||||
return '#1976d2';
|
||||
};
|
||||
|
||||
const providerName = state.config.provider === 'exa' ? 'Exa Neural' : 'Google Search';
|
||||
const modeName = state.researchMode === 'basic' ? 'Basic' : state.researchMode === 'comprehensive' ? 'Comprehensive' : 'Targeted';
|
||||
|
||||
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
|
||||
{modeName} research with {providerName}
|
||||
</p>
|
||||
|
||||
{/* Status Display */}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WizardStepProps } from '../types/research.types';
|
||||
import { ResearchResults } from '../../BlogWriter/ResearchResults';
|
||||
import { BlogResearchResponse } from '../../../services/blogWriterApi';
|
||||
|
||||
export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
|
||||
export const StepResults: React.FC<WizardStepProps> = ({ state, onUpdate, onBack }) => {
|
||||
if (!state.results) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
@@ -23,6 +23,14 @@ export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleStartNew = () => {
|
||||
// Reset to step 1 and clear results
|
||||
onUpdate({
|
||||
currentStep: 1,
|
||||
results: null
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
|
||||
<div style={{
|
||||
@@ -36,6 +44,21 @@ export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
|
||||
<h2 style={{ margin: 0, color: '#333' }}>Research Results</h2>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
color: '#333',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleExport}
|
||||
style={{
|
||||
@@ -55,7 +78,7 @@ export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onBack}
|
||||
onClick={handleStartNew}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
@@ -66,7 +89,7 @@ export const StepResults: React.FC<WizardStepProps> = ({ state, onBack }) => {
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
← Start New Research
|
||||
🔄 Start New Research
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,22 @@ export interface WizardState {
|
||||
results: BlogResearchResponse | null;
|
||||
}
|
||||
|
||||
export interface ResearchExecution {
|
||||
executeResearch: (state: WizardState) => Promise<string | null>;
|
||||
stopExecution: () => void;
|
||||
isExecuting: boolean;
|
||||
error: string | null;
|
||||
progressMessages: Array<{ timestamp: string; message: string }>;
|
||||
currentStatus: string;
|
||||
result: any;
|
||||
}
|
||||
|
||||
export interface WizardStepProps {
|
||||
state: WizardState;
|
||||
onUpdate: (updates: Partial<WizardState>) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
execution?: ResearchExecution;
|
||||
}
|
||||
|
||||
export interface ResearchWizardProps {
|
||||
|
||||
Reference in New Issue
Block a user