AI platform insights monitoring and website analysis monitoring services added

This commit is contained in:
ajaysi
2025-11-11 15:57:45 +05:30
parent d99c7c83a7
commit 7191c7e7f0
81 changed files with 10860 additions and 1567 deletions

View File

@@ -0,0 +1,317 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Grid,
Card,
CardContent,
Chip,
Avatar,
Divider,
Alert,
CircularProgress
} from '@mui/material';
import {
Close as CloseIcon,
Business as BusinessIcon,
Assessment as AssessmentIcon,
OpenInNew as OpenInNewIcon,
Link as LinkIcon
} from '@mui/icons-material';
import { CompetitorAnalysisResponse } from '../../api/researchConfig';
interface OnboardingCompetitorModalProps {
open: boolean;
onClose: () => void;
data: CompetitorAnalysisResponse | null;
loading?: boolean;
error?: string | null;
}
export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps> = ({
open,
onClose,
data,
loading = false,
error = null
}) => {
if (!data && !loading && !error) {
return null;
}
const competitors = data?.competitors || [];
const socialMediaAccounts = data?.social_media_accounts || {};
const researchSummary = data?.research_summary || {};
const avgScore = competitors.length > 0
? competitors.reduce((sum, c) => sum + (c.similarity_score || 0), 0) / competitors.length
: 0;
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
background: 'linear-gradient(135deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
maxHeight: '90vh'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pb: 2,
borderBottom: '2px solid #e5e7eb'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<BusinessIcon sx={{ fontSize: 32, color: '#0ea5e9' }} />
<Box>
<Typography variant="h5" sx={{ fontWeight: 600, color: '#0f172a' }}>
Competitive Analysis from Onboarding
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', mt: 0.5 }}>
{loading ? 'Loading...' : `${competitors.length} competitors analyzed`}
</Typography>
</Box>
</Box>
<Button onClick={onClose} size="small" sx={{ minWidth: 'auto', p: 1 }}>
<CloseIcon />
</Button>
</DialogTitle>
<DialogContent sx={{ py: 3, overflowY: 'auto' }}>
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
<CircularProgress />
<Typography variant="body2" sx={{ ml: 2, color: '#64748b' }}>
Loading competitor data...
</Typography>
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
<Typography variant="body2">{error}</Typography>
</Alert>
)}
{!loading && !error && data && (
<>
{researchSummary.industry_insights && (
<Alert
severity="info"
icon={<AssessmentIcon />}
sx={{ mb: 3, bgcolor: '#e0f2fe', borderLeft: '4px solid #0ea5e9' }}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Market Insights
</Typography>
<Typography variant="body2" sx={{ color: '#1e293b' }}>
{researchSummary.industry_insights}
</Typography>
</Alert>
)}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={4}>
<Card sx={{
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
borderLeft: '4px solid #0ea5e9'
}}>
<CardContent>
<Typography variant="caption" sx={{ color: '#0369a1', fontWeight: 600 }}>
Total Competitors
</Typography>
<Typography variant="h4" sx={{ color: '#0c4a6e', fontWeight: 700 }}>
{competitors.length}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card sx={{
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
borderLeft: '4px solid #22c55e'
}}>
<CardContent>
<Typography variant="caption" sx={{ color: '#15803d', fontWeight: 600 }}>
Avg Similarity
</Typography>
<Typography variant="h4" sx={{ color: '#166534', fontWeight: 700 }}>
{Math.round(avgScore * 100)}%
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card sx={{
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
borderLeft: '4px solid #f59e0b'
}}>
<CardContent>
<Typography variant="caption" sx={{ color: '#d97706', fontWeight: 600 }}>
Social Accounts Found
</Typography>
<Typography variant="h4" sx={{ color: '#92400e', fontWeight: 700 }}>
{Object.keys(socialMediaAccounts).length}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{Object.keys(socialMediaAccounts).length > 0 && (
<>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#0f172a' }}>
Social Media Accounts
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{Object.entries(socialMediaAccounts).map(([platform, url]) => (
<Chip
key={platform}
icon={<LinkIcon />}
label={`${platform}: ${url}`}
clickable
onClick={() => window.open(url, '_blank')}
sx={{
bgcolor: '#f8fafc',
border: '1px solid #e2e8f0',
'&:hover': {
bgcolor: '#f1f5f9',
borderColor: '#cbd5e1'
}
}}
/>
))}
</Box>
<Divider sx={{ my: 3 }} />
</>
)}
{competitors.length > 0 ? (
<>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600, color: '#0f172a' }}>
Competitors ({competitors.length})
</Typography>
<Grid container spacing={3}>
{competitors.map((competitor, index) => (
<Grid item xs={12} md={6} key={index}>
<Card sx={{
height: '100%',
'&:hover': { boxShadow: 4 },
transition: 'box-shadow 0.3s'
}}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, mb: 2 }}>
<Avatar sx={{ width: 40, height: 40, bgcolor: '#0ea5e9' }}>
<BusinessIcon />
</Avatar>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="h6"
sx={{
fontWeight: 600,
color: '#0f172a',
mb: 0.5,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{competitor.name || competitor.domain || 'Unknown Competitor'}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, flexWrap: 'wrap' }}>
{competitor.similarity_score !== undefined && (
<Chip
label={`Similarity: ${Math.round(competitor.similarity_score * 100)}%`}
size="small"
sx={{
bgcolor: competitor.similarity_score > 0.7
? '#dcfce7'
: competitor.similarity_score > 0.5
? '#fef3c7'
: '#fee2e2',
color: competitor.similarity_score > 0.7
? '#166534'
: competitor.similarity_score > 0.5
? '#92400e'
: '#991b1b',
fontWeight: 600
}}
/>
)}
{competitor.url && (
<Button
size="small"
endIcon={<OpenInNewIcon />}
href={competitor.url}
target="_blank"
sx={{ textTransform: 'none', fontSize: '0.75rem' }}
>
Visit
</Button>
)}
</Box>
</Box>
</Box>
{competitor.description && (
<Typography
variant="body2"
sx={{
color: '#64748b',
mb: 2,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{competitor.description}
</Typography>
)}
{competitor.domain && (
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block' }}>
{competitor.domain}
</Typography>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
</>
) : (
!loading && (
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
No competitor data available. Please complete onboarding step 3 to analyze competitors.
</Typography>
</Alert>
)
)}
</>
)}
</DialogContent>
<DialogActions sx={{ px: 4, py: 2, borderTop: '1px solid #e5e7eb' }}>
<Button onClick={onClose} variant="contained" sx={{ minWidth: 120 }}>
Close
</Button>
</DialogActions>
</Dialog>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useResearchWizard } from './hooks/useResearchWizard';
import { useResearchExecution } from './hooks/useResearchExecution';
import { ResearchInput } from './steps/ResearchInput';
@@ -6,6 +6,9 @@ import { StepProgress } from './steps/StepProgress';
import { StepResults } from './steps/StepResults';
import { ResearchWizardProps } from './types/research.types';
import { addResearchHistory } from '../../utils/researchHistory';
import { getResearchConfig, ProviderAvailability } from '../../api/researchConfig';
import { ProviderChips } from './steps/components/ProviderChips';
import { AdvancedChip } from './steps/components/AdvancedChip';
export const ResearchWizard: React.FC<ResearchWizardProps> = ({
onComplete,
@@ -24,6 +27,30 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
initialConfig
);
const execution = useResearchExecution();
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability | null>(null);
const [advanced, setAdvanced] = useState<boolean>(false);
// Load provider availability on mount
useEffect(() => {
const loadProviderAvailability = async () => {
try {
const config = await getResearchConfig();
setProviderAvailability(config?.provider_availability || null);
} catch (error) {
console.error('[ResearchWizard] Failed to load provider availability:', error);
// Set default availability on error
setProviderAvailability({
google_available: true,
exa_available: false,
tavily_available: false,
gemini_key_status: 'missing',
exa_key_status: 'missing',
tavily_key_status: 'missing',
});
}
};
loadProviderAvailability();
}, []);
// Handle results from execution
useEffect(() => {
@@ -73,13 +100,13 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
switch (wizard.state.currentStep) {
case 1:
return <ResearchInput {...stepProps} />;
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} />;
case 2:
return <StepProgress {...stepProps} execution={execution} />;
case 3:
return <StepResults {...stepProps} />;
default:
return <ResearchInput {...stepProps} />;
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} />;
}
};
@@ -96,31 +123,124 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
boxShadow: '0 4px 16px rgba(14, 165, 233, 0.1)',
overflow: 'hidden',
}}>
{/* Header */}
{/* Header with Compact Steps */}
<div style={{
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',
padding: '14px 24px',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{
margin: 0,
fontSize: '24px',
fontWeight: '700',
color: '#0c4a6e',
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '24px' }}>
{/* Title Section */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', flex: '1', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<h1 style={{
margin: 0,
fontSize: '20px',
fontWeight: '700',
color: '#0c4a6e',
letterSpacing: '-0.01em',
}}>
Research Wizard
</h1>
{/* Provider Status Chips */}
<ProviderChips providerAvailability={providerAvailability} advanced={advanced} />
{/* Advanced Chip */}
<AdvancedChip advanced={advanced} />
</div>
{/* Compact Step Indicators */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginLeft: '8px',
}}>
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>
{[1, 2, 3].map((step, index) => {
const isActive = step === wizard.state.currentStep;
const isCompleted = step < wizard.state.currentStep;
const isClickable = step <= wizard.state.currentStep;
return (
<React.Fragment key={step}>
{index > 0 && (
<div style={{
width: '20px',
height: '2px',
background: isCompleted || (step === wizard.state.currentStep)
? 'linear-gradient(90deg, #22c55e 0%, #16a34a 100%)'
: 'rgba(14, 165, 233, 0.2)',
transition: 'all 0.3s ease',
}} />
)}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
cursor: isClickable ? 'pointer' : 'default',
transition: 'all 0.2s ease',
}}
onClick={() => {
if (isClickable) {
wizard.updateState({ currentStep: step });
}
}}
onMouseEnter={(e) => {
if (isClickable) {
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<div style={{
width: '32px',
height: '32px',
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: '13px',
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 2px 8px rgba(14, 165, 233, 0.25)'
: isCompleted
? '0 1px 4px rgba(34, 197, 94, 0.2)'
: 'none',
}}>
{isCompleted ? '✓' : step}
</div>
<span style={{
fontSize: '11px',
color: (isActive || isCompleted) ? '#0c4a6e' : '#64748b',
fontWeight: isActive ? '600' : '400',
letterSpacing: '0.01em',
whiteSpace: 'nowrap',
}}>
{step === 1 && 'Configure'}
{step === 2 && 'Execute'}
{step === 3 && 'Analyze'}
</span>
</div>
</React.Fragment>
);
})}
</div>
</div>
{/* Cancel Button */}
{onCancel && (
<button
onClick={() => {
@@ -128,13 +248,13 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
onCancel();
}}
style={{
padding: '8px 16px',
padding: '6px 12px',
background: 'rgba(239, 68, 68, 0.1)',
color: '#dc2626',
border: '1px solid rgba(239, 68, 68, 0.25)',
borderRadius: '10px',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '13px',
fontSize: '12px',
fontWeight: '500',
transition: 'all 0.2s ease',
}}
@@ -154,7 +274,7 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
{/* Progress Bar */}
<div style={{
background: 'rgba(14, 165, 233, 0.1)',
height: '5px',
height: '3px',
position: 'relative',
overflow: 'hidden',
}}>
@@ -164,90 +284,11 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
height: '100%',
width: `${(wizard.state.currentStep / wizard.maxSteps) * 100}%`,
transition: 'width 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 0 8px rgba(14, 165, 233, 0.4)',
boxShadow: '0 0 6px rgba(14, 165, 233, 0.4)',
}}
/>
</div>
{/* Step Indicators */}
<div style={{
display: 'flex',
justifyContent: 'space-around',
padding: '24px 40px',
borderBottom: '1px solid rgba(14, 165, 233, 0.15)',
background: 'rgba(14, 165, 233, 0.03)',
}}>
{[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>
);
})}
</div>
{/* Content */}
<div style={{ padding: '20px' }}>
{renderStep()}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
import React, { useState } from 'react';
interface AdvancedChipProps {
advanced: boolean;
}
export const AdvancedChip: React.FC<AdvancedChipProps> = ({ advanced }) => {
const [hovered, setHovered] = useState(false);
return (
<div
style={{
position: 'relative',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Chip */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
background: advanced
? 'linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.15) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%)',
border: `1px solid ${advanced ? 'rgba(16, 185, 129, 0.3)' : 'rgba(239, 68, 68, 0.2)'}`,
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
color: advanced ? '#10b981' : '#ef4444',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'default',
boxShadow: hovered
? '0 2px 8px rgba(0, 0, 0, 0.12)'
: '0 1px 3px rgba(0, 0, 0, 0.08)',
transform: hovered ? 'translateY(-1px)' : 'translateY(0)',
letterSpacing: '-0.01em',
}}
>
<span style={{ fontSize: '13px' }}></span>
<span>Advanced</span>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: advanced ? '#10b981' : '#ef4444',
boxShadow: advanced
? '0 0 4px rgba(16, 185, 129, 0.4)'
: '0 0 4px rgba(239, 68, 68, 0.4)',
}} />
</div>
{/* Tooltip */}
{hovered && (
<div
style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginTop: '8px',
padding: '10px 12px',
background: 'rgba(15, 23, 42, 0.95)',
color: '#f8fafc',
fontSize: '11px',
lineHeight: '1.5',
borderRadius: '8px',
maxWidth: '240px',
zIndex: 1000,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)',
pointerEvents: 'none',
whiteSpace: 'normal',
wordWrap: 'break-word',
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
>
{advanced
? 'Advanced mode is ON. Exa and Tavily configuration options are available.'
: 'Advanced mode is OFF. Enable to access Exa and Tavily configuration options.'}
<div
style={{
position: 'absolute',
top: '-4px',
left: '50%',
transform: 'translateX(-50%) rotate(45deg)',
width: '8px',
height: '8px',
background: 'rgba(15, 23, 42, 0.95)',
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
}}
/>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { formatKeyword } from '../../../../utils/keywordExpansion';
interface CurrentKeywordsProps {
keywords: string[];
onRemoveKeyword: (keyword: string) => void;
}
export const CurrentKeywords: React.FC<CurrentKeywordsProps> = ({ keywords, onRemoveKeyword }) => {
if (keywords.length === 0) return null;
return (
<div style={{
marginTop: '12px',
padding: '10px',
background: 'rgba(241, 245, 249, 0.5)',
border: '1px solid rgba(203, 213, 225, 0.3)',
borderRadius: '8px',
}}>
<div style={{
fontSize: '12px',
fontWeight: '600',
color: '#475569',
marginBottom: '8px',
}}>
Current Keywords ({keywords.length})
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
}}>
{keywords.map((keyword, idx) => (
<div
key={idx}
style={{
padding: '5px 10px',
background: 'white',
border: '1px solid rgba(203, 213, 225, 0.5)',
borderRadius: '6px',
fontSize: '12px',
color: '#334155',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<span>{formatKeyword(keyword)}</span>
<button
onClick={() => onRemoveKeyword(keyword)}
style={{
background: 'none',
border: 'none',
color: '#ef4444',
cursor: 'pointer',
fontSize: '14px',
padding: '0',
width: '16px',
height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'none';
}}
title="Remove keyword"
>
×
</button>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,183 @@
import React from 'react';
import { ResearchConfig } from '../../../../services/blogWriterApi';
import { exaCategories, exaSearchTypes } from '../utils/constants';
interface ExaOptionsProps {
config: ResearchConfig;
onConfigUpdate: (updates: Partial<ResearchConfig>) => void;
}
export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }) => {
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
onConfigUpdate({ exa_category: value || undefined });
};
const handleSearchTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as 'auto' | 'keyword' | 'neural';
onConfigUpdate({ 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);
onConfigUpdate({ 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);
onConfigUpdate({ exa_exclude_domains: domains });
};
return (
<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={config.exa_category || ''}
onChange={handleCategoryChange}
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={config.exa_search_type || 'auto'}
onChange={handleSearchTypeChange}
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={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={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>
);
};

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { formatKeyword } from '../../../../utils/keywordExpansion';
interface KeywordExpansionProps {
suggestions: string[];
currentKeywords: string[];
industry: string;
onAddSuggestion: (suggestion: string) => void;
}
export const KeywordExpansion: React.FC<KeywordExpansionProps> = ({
suggestions,
currentKeywords,
industry,
onAddSuggestion,
}) => {
if (suggestions.length === 0 || industry === 'General') return null;
return (
<div style={{
marginTop: '12px',
padding: '12px',
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(147, 197, 253, 0.05) 100%)',
border: '1px solid rgba(59, 130, 246, 0.15)',
borderRadius: '8px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '10px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '13px',
fontWeight: '600',
color: '#1e40af',
}}>
<span>💡</span>
<span>Suggested Keywords for {industry}</span>
</div>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
}}>
{suggestions.map((suggestion, idx) => {
const isAlreadyAdded = currentKeywords.some(k => k.toLowerCase() === suggestion.toLowerCase());
return (
<button
key={idx}
onClick={() => !isAlreadyAdded && onAddSuggestion(suggestion)}
disabled={isAlreadyAdded}
style={{
padding: '6px 12px',
background: isAlreadyAdded
? 'rgba(203, 213, 225, 0.3)'
: 'rgba(59, 130, 246, 0.1)',
border: `1px solid ${isAlreadyAdded ? 'rgba(148, 163, 184, 0.3)' : 'rgba(59, 130, 246, 0.2)'}`,
borderRadius: '6px',
fontSize: '12px',
fontWeight: '500',
color: isAlreadyAdded ? '#64748b' : '#1e40af',
cursor: isAlreadyAdded ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
onMouseEnter={(e) => {
if (!isAlreadyAdded) {
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.15)';
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.3)';
}
}}
onMouseLeave={(e) => {
if (!isAlreadyAdded) {
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.1)';
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.2)';
}
}}
>
{isAlreadyAdded ? (
<>
<span></span>
<span>{formatKeyword(suggestion)}</span>
</>
) : (
<>
<span>+</span>
<span>{formatKeyword(suggestion)}</span>
</>
)}
</button>
);
})}
</div>
<div style={{
marginTop: '8px',
fontSize: '11px',
color: '#64748b',
fontStyle: 'italic',
}}>
Click to add suggested keywords to your research query
</div>
</div>
);
};

View File

@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import { ProviderAvailability } from '../../../../api/researchConfig';
interface ProviderChipsProps {
providerAvailability: ProviderAvailability | null;
advanced?: boolean;
}
export const ProviderChips: React.FC<ProviderChipsProps> = ({ providerAvailability, advanced = false }) => {
const [hoveredChip, setHoveredChip] = useState<string | null>(null);
if (!providerAvailability) return null;
const providers = [
{
id: 'google',
name: 'Google',
available: providerAvailability.google_available,
status: providerAvailability.gemini_key_status,
icon: '🔍',
tooltip: 'Google Search powered by Gemini AI. Provides comprehensive web search results with semantic understanding and real-time information from across the web.',
color: providerAvailability.google_available
? 'linear-gradient(135deg, rgba(66, 133, 244, 0.15) 0%, rgba(52, 168, 83, 0.15) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%)',
borderColor: providerAvailability.google_available
? 'rgba(66, 133, 244, 0.3)'
: 'rgba(239, 68, 68, 0.2)',
textColor: providerAvailability.google_available ? '#4285f4' : '#ef4444',
},
{
id: 'exa',
name: 'Exa',
available: providerAvailability.exa_available,
status: providerAvailability.exa_key_status,
icon: '🧠',
tooltip: 'Exa Neural Search. Advanced semantic search engine that understands context and meaning, providing highly relevant results through neural network-powered query understanding.',
// Show green when advanced is ON and available, red when advanced is OFF or not available
isAdvanced: true,
color: (advanced && providerAvailability.exa_available)
? 'linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.15) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%)',
borderColor: (advanced && providerAvailability.exa_available)
? 'rgba(16, 185, 129, 0.3)'
: 'rgba(239, 68, 68, 0.2)',
textColor: (advanced && providerAvailability.exa_available) ? '#10b981' : '#ef4444',
chipStatus: (advanced && providerAvailability.exa_available) ? '#10b981' : '#ef4444',
},
{
id: 'tavily',
name: 'Tavily',
available: providerAvailability.tavily_available,
status: providerAvailability.tavily_key_status,
icon: '🤖',
tooltip: 'Tavily AI Research Engine. Specialized AI-powered research tool designed for comprehensive content discovery, providing deep insights and structured research data from multiple sources.',
// Show green when advanced is ON and available, red when advanced is OFF or not available
isAdvanced: true,
color: (advanced && providerAvailability.tavily_available)
? 'linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.15) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%)',
borderColor: (advanced && providerAvailability.tavily_available)
? 'rgba(16, 185, 129, 0.3)'
: 'rgba(239, 68, 68, 0.2)',
textColor: (advanced && providerAvailability.tavily_available) ? '#10b981' : '#ef4444',
chipStatus: (advanced && providerAvailability.tavily_available) ? '#10b981' : '#ef4444',
},
];
return (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginLeft: '16px',
}}>
{providers.map((provider) => {
const isHovered = hoveredChip === provider.id;
return (
<div
key={provider.id}
style={{
position: 'relative',
}}
onMouseEnter={() => setHoveredChip(provider.id)}
onMouseLeave={() => setHoveredChip(null)}
>
{/* Chip */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
background: provider.color,
border: `1px solid ${provider.borderColor}`,
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
color: provider.textColor,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'default',
boxShadow: isHovered
? '0 2px 8px rgba(0, 0, 0, 0.12)'
: '0 1px 3px rgba(0, 0, 0, 0.08)',
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
letterSpacing: '-0.01em',
}}
>
<span style={{ fontSize: '13px' }}>{provider.icon}</span>
<span>{provider.name}</span>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: (provider as any).chipStatus || (provider.available ? '#10b981' : '#ef4444'),
boxShadow: ((provider as any).chipStatus === '#10b981') || (provider.available && !(provider as any).isAdvanced)
? '0 0 4px rgba(16, 185, 129, 0.4)'
: '0 0 4px rgba(239, 68, 68, 0.4)',
}} />
</div>
{/* Tooltip */}
{isHovered && (
<div
style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginTop: '8px',
padding: '10px 12px',
background: 'rgba(15, 23, 42, 0.95)',
color: '#f8fafc',
fontSize: '11px',
lineHeight: '1.5',
borderRadius: '8px',
maxWidth: '280px',
zIndex: 1000,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)',
pointerEvents: 'none',
whiteSpace: 'normal',
wordWrap: 'break-word',
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
>
{provider.tooltip}
<div
style={{
position: 'absolute',
top: '-4px',
left: '50%',
transform: 'translateX(-50%) rotate(45deg)',
width: '8px',
height: '8px',
background: 'rgba(15, 23, 42, 0.95)',
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
}}
/>
</div>
)}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { ProviderAvailability } from '../../../../api/researchConfig';
interface ProviderStatusProps {
providerAvailability: ProviderAvailability | null;
}
export const ProviderStatus: React.FC<ProviderStatusProps> = ({ providerAvailability }) => {
if (!providerAvailability) return null;
return (
<div style={{
marginBottom: '20px',
padding: '10px 14px',
background: 'rgba(241, 245, 249, 0.5)',
border: '1px solid rgba(203, 213, 225, 0.3)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '16px',
fontSize: '11px',
color: '#64748b',
flexWrap: 'wrap',
}}>
<span style={{ fontWeight: '600', color: '#475569' }}>Provider Status:</span>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
}}>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: providerAvailability.google_available ? '#10b981' : '#ef4444',
}} />
<span>Google: {providerAvailability.gemini_key_status}</span>
</span>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
}}>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: providerAvailability.exa_available ? '#10b981' : '#ef4444',
}} />
<span>Exa: {providerAvailability.exa_key_status}</span>
</span>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
}}>
<span style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: providerAvailability.tavily_available ? '#10b981' : '#ef4444',
}} />
<span>Tavily: {providerAvailability.tavily_key_status}</span>
</span>
</div>
);
};

View File

@@ -0,0 +1,98 @@
import React from 'react';
import { formatAngle } from '../../../../utils/researchAngles';
interface ResearchAnglesProps {
angles: string[];
onUseAngle: (angle: string) => void;
}
export const ResearchAngles: React.FC<ResearchAnglesProps> = ({ angles, onUseAngle }) => {
if (angles.length === 0) return null;
return (
<div style={{
marginTop: '12px',
padding: '12px',
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%)',
border: '1px solid rgba(168, 85, 247, 0.15)',
borderRadius: '8px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
marginBottom: '10px',
}}>
<span style={{
fontSize: '16px',
}}>💡</span>
<span style={{
fontSize: '13px',
fontWeight: '600',
color: '#7c3aed',
}}>
Explore Alternative Research Angles
</span>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '10px',
}}>
{angles.map((angle, idx) => (
<button
key={idx}
onClick={() => onUseAngle(angle)}
style={{
padding: '10px 14px',
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(168, 85, 247, 0.2)',
borderRadius: '8px',
fontSize: '12px',
fontWeight: '500',
color: '#6b21a8',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.2s ease',
display: 'flex',
flexDirection: 'column',
gap: '4px',
boxShadow: '0 1px 3px rgba(168, 85, 247, 0.1)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(168, 85, 247, 0.1)';
e.currentTarget.style.borderColor = 'rgba(168, 85, 247, 0.4)';
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(168, 85, 247, 0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.9)';
e.currentTarget.style.borderColor = 'rgba(168, 85, 247, 0.2)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 1px 3px rgba(168, 85, 247, 0.1)';
}}
title={`Click to research: ${angle}`}
>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span style={{ fontSize: '14px' }}>🔍</span>
<span>{formatAngle(angle)}</span>
</span>
</button>
))}
</div>
<div style={{
marginTop: '8px',
fontSize: '11px',
color: '#64748b',
fontStyle: 'italic',
}}>
Click any angle to explore a different research focus
</div>
</div>
);
};

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { ProviderAvailability } from '../../../../api/researchConfig';
import { industries } from '../utils/constants';
interface ResearchControlsBarProps {
industry: string;
providerAvailability: ProviderAvailability | null;
onIndustryChange: (industry: string) => void;
}
export const ResearchControlsBar: React.FC<ResearchControlsBarProps> = ({
industry,
providerAvailability,
onIndustryChange,
}) => {
const dropdownStyle = {
minWidth: '130px',
padding: '7px 28px 7px 10px',
fontSize: '12px',
border: '1px solid rgba(15, 23, 42, 0.1)',
borderRadius: '8px',
background: '#ffffff',
color: '#0f172a',
cursor: 'pointer',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif',
fontWeight: '500',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.04)',
appearance: 'none' as const,
WebkitAppearance: 'none' as const,
MozAppearance: 'none' as const,
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%23475569' d='M5 7L1 3h8z'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat' as const,
backgroundPosition: 'right 9px center',
backgroundSize: '10px 10px',
};
const handleFocus = (e: React.FocusEvent<HTMLSelectElement>) => {
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.4)';
e.currentTarget.style.boxShadow = '0 0 0 2px rgba(14, 165, 233, 0.08), 0 1px 3px rgba(0, 0, 0, 0.08)';
e.currentTarget.style.background = '#ffffff';
e.currentTarget.style.backgroundImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%230ea5e9' d='M5 7L1 3h8z'/%3E%3C/svg%3E")`;
e.currentTarget.style.backgroundSize = '10px 10px';
};
const handleBlur = (e: React.FocusEvent<HTMLSelectElement>) => {
e.currentTarget.style.borderColor = 'rgba(15, 23, 42, 0.1)';
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.04)';
e.currentTarget.style.background = '#ffffff';
e.currentTarget.style.backgroundImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%23475569' d='M5 7L1 3h8z'/%3E%3C/svg%3E")`;
e.currentTarget.style.backgroundSize = '10px 10px';
};
const handleMouseEnter = (e: React.MouseEvent<HTMLSelectElement>) => {
e.currentTarget.style.borderColor = 'rgba(15, 23, 42, 0.15)';
e.currentTarget.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.06)';
};
const handleMouseLeave = (e: React.MouseEvent<HTMLSelectElement>) => {
if (document.activeElement !== e.currentTarget) {
e.currentTarget.style.borderColor = 'rgba(15, 23, 42, 0.1)';
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.04)';
}
};
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '10px',
paddingTop: '16px',
marginTop: '16px',
borderTop: '1px solid rgba(14, 165, 233, 0.15)',
flexWrap: 'wrap',
}}>
{/* Compact Dropdowns - Stacked Horizontally */}
<div style={{
display: 'flex',
flexDirection: 'row',
gap: '10px',
alignItems: 'center',
flexWrap: 'wrap',
}}>
{/* Industry Dropdown */}
<select
value={industry}
onChange={(e) => onIndustryChange(e.target.value)}
title="Select industry for targeted research"
style={dropdownStyle}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{industries.map(ind => (
<option key={ind} value={ind}>{ind}</option>
))}
</select>
</div>
</div>
);
};

View File

@@ -0,0 +1,146 @@
import React from 'react';
import {
clearResearchHistory,
formatHistoryTimestamp,
getHistorySummary,
ResearchHistoryEntry
} from '../../../../utils/researchHistory';
import { WizardState } from '../../types/research.types';
interface ResearchHistoryProps {
history: ResearchHistoryEntry[];
onLoadHistory: (entry: Partial<WizardState>) => void;
onHistoryCleared: () => void;
}
export const ResearchHistory: React.FC<ResearchHistoryProps> = ({
history,
onLoadHistory,
onHistoryCleared
}) => {
if (history.length === 0) return null;
const handleClear = () => {
clearResearchHistory();
onHistoryCleared();
};
return (
<div style={{
marginBottom: '12px',
padding: '12px',
background: 'rgba(14, 165, 233, 0.03)',
border: '1px solid rgba(14, 165, 233, 0.1)',
borderRadius: '10px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '10px',
}}>
<span style={{
fontSize: '12px',
fontWeight: '600',
color: '#0369a1',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span>🕐</span>
Recently Researched
</span>
<button
onClick={handleClear}
style={{
padding: '4px 10px',
fontSize: '11px',
color: '#64748b',
background: 'transparent',
border: '1px solid rgba(100, 116, 139, 0.2)',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
e.currentTarget.style.color = '#dc2626';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = 'rgba(100, 116, 139, 0.2)';
e.currentTarget.style.color = '#64748b';
}}
>
Clear
</button>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
}}>
{history.map((entry) => (
<button
key={entry.timestamp}
onClick={() => {
onLoadHistory({
keywords: entry.keywords,
industry: entry.industry,
targetAudience: entry.targetAudience,
researchMode: entry.researchMode,
});
}}
title={`Industry: ${entry.industry} | Audience: ${entry.targetAudience} | Mode: ${entry.researchMode} | ${formatHistoryTimestamp(entry.timestamp)}`}
style={{
padding: '8px 14px',
fontSize: '12px',
color: '#0369a1',
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '6px',
maxWidth: '100%',
textAlign: 'left',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(14, 165, 233, 0.1)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.4)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.9)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<span style={{ fontSize: '14px' }}>🔍</span>
<span style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
}}>
{getHistorySummary(entry)}
</span>
<span style={{
fontSize: '10px',
color: '#64748b',
marginLeft: '4px',
}}>
{formatHistoryTimestamp(entry.timestamp)}
</span>
</button>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
interface ResearchInputContainerProps {
keywords: string[];
placeholder: string;
onKeywordsChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
keywords,
placeholder,
onKeywordsChange,
}) => {
const [inputValue, setInputValue] = useState('');
const [wordCount, setWordCount] = useState(0);
const MAX_WORDS = 1000;
// Initialize input value from keywords only on mount or when keywords are cleared
useEffect(() => {
const keywordValue = keywords.length > 0 ? keywords.join(', ') : '';
// Only update if the input is empty or if keywords were cleared
if (inputValue === '' || (keywords.length === 0 && inputValue !== '')) {
setInputValue(keywordValue);
const words = keywordValue.trim().split(/\s+/).filter(w => w.length > 0);
setWordCount(words.length);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [keywords.length]); // Only reinitialize if keywords array length changes
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const words = value.trim().split(/\s+/).filter(w => w.length > 0);
const currentWordCount = words.length;
// Only update if within word limit
if (currentWordCount <= MAX_WORDS) {
setInputValue(value);
setWordCount(currentWordCount);
// Create a new event with the current value for the parent handler
const syntheticEvent = {
...e,
target: {
...e.target,
value: value,
},
} as React.ChangeEvent<HTMLTextAreaElement>;
onKeywordsChange(syntheticEvent);
} else {
// Truncate to last valid word boundary
const truncatedWords = words.slice(0, MAX_WORDS);
const truncatedValue = truncatedWords.join(' ');
setInputValue(truncatedValue);
setWordCount(MAX_WORDS);
// Create synthetic event with truncated value
const syntheticEvent = {
...e,
target: {
...e.target,
value: truncatedValue,
},
} as React.ChangeEvent<HTMLTextAreaElement>;
onKeywordsChange(syntheticEvent);
}
};
return (
<div style={{
position: 'relative',
minHeight: '227px', // Reduced by 35% from 350px
width: '65%', // Reduced by 35% from 100%
padding: '20px',
display: 'flex',
flexDirection: 'column',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '16px',
background: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(249, 250, 251, 0.95) 100%)',
boxShadow: 'inset 0 2px 8px rgba(14, 165, 233, 0.06), 0 1px 2px rgba(0, 0, 0, 0.05)',
overflow: 'hidden',
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.3)';
e.currentTarget.style.boxShadow = 'inset 0 2px 8px rgba(14, 165, 233, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)';
e.currentTarget.style.boxShadow = 'inset 0 2px 8px rgba(14, 165, 233, 0.06), 0 1px 2px rgba(0, 0, 0, 0.05)';
}}
>
{/* Textarea for input - takes full space */}
<textarea
value={inputValue}
onChange={handleInputChange}
placeholder={placeholder}
style={{
width: '100%',
flex: '1',
minHeight: '195px', // Reduced by 35% from 300px
padding: '12px',
fontSize: '15px',
lineHeight: '1.7',
border: 'none',
background: 'transparent',
color: '#1e293b',
resize: 'vertical',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif',
outline: 'none',
fontWeight: '400',
letterSpacing: '-0.01em',
overflowWrap: 'break-word',
wordWrap: 'break-word',
boxSizing: 'border-box',
}}
/>
{/* Word count indicator */}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
paddingTop: '8px',
fontSize: '12px',
color: wordCount >= MAX_WORDS ? '#ef4444' : '#64748b',
fontWeight: wordCount >= MAX_WORDS ? '600' : '400',
}}>
{wordCount} / {MAX_WORDS} words
</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
import React from 'react';
interface SmartInputIndicatorProps {
keywords: string[];
}
export const SmartInputIndicator: React.FC<SmartInputIndicatorProps> = ({ keywords }) => {
if (keywords.length === 0) return null;
return (
<div style={{
marginTop: '10px',
padding: '8px 12px',
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, rgba(16, 185, 129, 0.1) 100%)',
border: '1px solid rgba(34, 197, 94, 0.2)',
borderRadius: '8px',
fontSize: '12px',
color: '#059669',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<span></span>
{keywords[0]?.startsWith('http') ? (
<span>URL detected - will extract and analyze content</span>
) : keywords.length === 1 && keywords[0]?.split(/\s+/).length > 5 ? (
<span>Research topic detected - will conduct comprehensive analysis</span>
) : (
<span>{keywords.length} keyword{keywords.length > 1 ? 's' : ''} identified</span>
)}
</div>
);
};

View File

@@ -0,0 +1,47 @@
import React from 'react';
interface TargetAudienceProps {
value: string;
onChange: (value: string) => void;
}
export const TargetAudience: React.FC<TargetAudienceProps> = ({ value, onChange }) => {
return (
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontSize: '13px',
fontWeight: '600',
color: '#0c4a6e',
}}>
Target Audience (Optional)
</label>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
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>
);
};

View File

@@ -0,0 +1,570 @@
import React from 'react';
import { ResearchConfig } from '../../../../services/blogWriterApi';
import {
tavilyTopics,
tavilySearchDepths,
tavilyTimeRanges,
tavilyAnswerOptions,
tavilyRawContentOptions
} from '../utils/constants';
interface TavilyOptionsProps {
config: ResearchConfig;
onConfigUpdate: (updates: Partial<ResearchConfig>) => void;
}
export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUpdate }) => {
const handleTopicChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as 'general' | 'news' | 'finance';
onConfigUpdate({ tavily_topic: value || 'general' });
};
const handleSearchDepthChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as 'basic' | 'advanced';
onConfigUpdate({ tavily_search_depth: value });
};
const handleIncludeDomainsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const domains = value.split(',').map(d => d.trim()).filter(Boolean);
onConfigUpdate({ tavily_include_domains: domains });
};
const handleExcludeDomainsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const domains = value.split(',').map(d => d.trim()).filter(Boolean);
onConfigUpdate({ tavily_exclude_domains: domains });
};
const handleIncludeAnswerChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
let answerValue: boolean | 'basic' | 'advanced';
if (value === 'true') {
answerValue = true;
} else if (value === 'false') {
answerValue = false;
} else {
answerValue = value as 'basic' | 'advanced';
}
onConfigUpdate({ tavily_include_answer: answerValue });
};
const handleTimeRangeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
const timeRangeValue = value ? (value as 'day' | 'week' | 'month' | 'year' | 'd' | 'w' | 'm' | 'y') : undefined;
onConfigUpdate({ tavily_time_range: timeRangeValue });
};
const handleIncludeRawContentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
let rawContentValue: boolean | 'markdown' | 'text';
if (value === 'true') {
rawContentValue = true;
} else if (value === 'false') {
rawContentValue = false;
} else {
rawContentValue = value as 'markdown' | 'text';
}
onConfigUpdate({ tavily_include_raw_content: rawContentValue });
};
const handleIncludeImagesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_include_images: e.target.checked });
};
const handleIncludeImageDescriptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_include_image_descriptions: e.target.checked });
};
const handleIncludeFaviconChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_include_favicon: e.target.checked });
};
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_start_date: e.target.value || undefined });
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_end_date: e.target.value || undefined });
};
const handleCountryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_country: e.target.value || undefined });
};
const handleChunksPerSourceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value >= 1 && value <= 3) {
onConfigUpdate({ tavily_chunks_per_source: value });
}
};
const handleAutoParametersChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onConfigUpdate({ tavily_auto_parameters: e.target.checked });
};
return (
<div style={{
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '2px solid rgba(14, 165, 233, 0.3)',
borderRadius: '12px',
padding: '16px',
marginBottom: '14px',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '14px',
}}>
<span style={{ fontSize: '18px' }}>🤖</span>
<strong style={{ color: '#0ea5e9', fontSize: '13px' }}>Tavily AI Search Options</strong>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
gap: '12px',
marginBottom: '12px',
}}>
{/* Tavily Topic */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Search Topic
</label>
<select
value={config.tavily_topic || 'general'}
onChange={handleTopicChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
cursor: 'pointer',
}}
>
{tavilyTopics.map(topic => (
<option key={topic.value} value={topic.value}>{topic.label}</option>
))}
</select>
</div>
{/* Tavily Search Depth */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Search Depth
</label>
<select
value={config.tavily_search_depth || 'basic'}
onChange={handleSearchDepthChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
cursor: 'pointer',
}}
>
{tavilySearchDepths.map(depth => (
<option key={depth.value} value={depth.value}>{depth.label}</option>
))}
</select>
</div>
{/* Tavily Include Answer */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
AI Answer
</label>
<select
value={config.tavily_include_answer === true ? 'true' : typeof config.tavily_include_answer === 'string' ? config.tavily_include_answer : 'false'}
onChange={handleIncludeAnswerChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
cursor: 'pointer',
}}
>
{tavilyAnswerOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
{/* Tavily Time Range */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Time Range
</label>
<select
value={config.tavily_time_range || ''}
onChange={handleTimeRangeChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
cursor: 'pointer',
}}
>
{tavilyTimeRanges.map(range => (
<option key={range.value} value={range.value}>{range.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: '#0ea5e9',
}}>
Include Domains (optional)
</label>
<input
type="text"
value={config.tavily_include_domains?.join(', ') || ''}
onChange={handleIncludeDomainsChange}
placeholder="e.g., nature.com, arxiv.org"
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 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: '#0ea5e9',
}}>
Exclude Domains (optional)
</label>
<input
type="text"
value={config.tavily_exclude_domains?.join(', ') || ''}
onChange={handleExcludeDomainsChange}
placeholder="e.g., spam.com, ads.com"
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
}}
/>
</div>
</div>
{/* Additional Tavily Options */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '12px',
marginTop: '12px',
}}>
{/* Include Raw Content */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Raw Content Format
</label>
<select
value={config.tavily_include_raw_content === true ? 'true' : typeof config.tavily_include_raw_content === 'string' ? config.tavily_include_raw_content : 'false'}
onChange={handleIncludeRawContentChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
cursor: 'pointer',
}}
>
{tavilyRawContentOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
{/* Country */}
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Country Code (optional)
</label>
<input
type="text"
value={config.tavily_country || ''}
onChange={handleCountryChange}
placeholder="e.g., US, GB, IN"
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
}}
/>
</div>
{/* Chunks Per Source (only for advanced) */}
{config.tavily_search_depth === 'advanced' && (
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Chunks Per Source (1-3)
</label>
<input
type="number"
min="1"
max="3"
value={config.tavily_chunks_per_source || 3}
onChange={handleChunksPerSourceChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
}}
/>
</div>
)}
</div>
{/* Date Range */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
marginTop: '12px',
}}>
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '12px',
fontWeight: '600',
color: '#0ea5e9',
}}>
Start Date (YYYY-MM-DD)
</label>
<input
type="date"
value={config.tavily_start_date || ''}
onChange={handleStartDateChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 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: '#0ea5e9',
}}>
End Date (YYYY-MM-DD)
</label>
<input
type="date"
value={config.tavily_end_date || ''}
onChange={handleEndDateChange}
style={{
width: '100%',
padding: '8px 10px',
fontSize: '12px',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '8px',
background: 'rgba(255, 255, 255, 0.9)',
color: '#0f172a',
}}
/>
</div>
</div>
{/* Checkboxes */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '12px',
marginTop: '12px',
}}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: '#0ea5e9',
cursor: 'pointer',
}}>
<input
type="checkbox"
checked={config.tavily_include_images || false}
onChange={handleIncludeImagesChange}
style={{
width: '16px',
height: '16px',
cursor: 'pointer',
}}
/>
<span>Include Images</span>
</label>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: '#0ea5e9',
cursor: 'pointer',
}}>
<input
type="checkbox"
checked={config.tavily_include_image_descriptions || false}
onChange={handleIncludeImageDescriptionsChange}
disabled={!config.tavily_include_images}
style={{
width: '16px',
height: '16px',
cursor: config.tavily_include_images ? 'pointer' : 'not-allowed',
opacity: config.tavily_include_images ? 1 : 0.5,
}}
/>
<span>Include Image Descriptions</span>
</label>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: '#0ea5e9',
cursor: 'pointer',
}}>
<input
type="checkbox"
checked={config.tavily_include_favicon || false}
onChange={handleIncludeFaviconChange}
style={{
width: '16px',
height: '16px',
cursor: 'pointer',
}}
/>
<span>Include Favicon URLs</span>
</label>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: '#0ea5e9',
cursor: 'pointer',
}}>
<input
type="checkbox"
checked={config.tavily_auto_parameters || false}
onChange={handleAutoParametersChange}
style={{
width: '16px',
height: '16px',
cursor: 'pointer',
}}
/>
<span>Auto-Configure Parameters</span>
</label>
</div>
</div>
);
};

View File

@@ -0,0 +1,83 @@
export const industries = [
'General',
'Technology',
'Business',
'Marketing',
'Finance',
'Healthcare',
'Education',
'Real Estate',
'Entertainment',
'Food & Beverage',
'Travel',
'Fashion',
'Sports',
'Science',
'Law',
'Other',
];
export const researchModes = [
{ value: 'basic', label: 'Basic - Quick insights' },
{ value: 'comprehensive', label: 'Comprehensive - In-depth analysis' },
{ value: 'targeted', label: 'Targeted - Specific focus' },
];
export const providers = [
{ value: 'google', label: '🔍 Google Search' },
{ value: 'exa', label: '🧠 Exa Neural Search' },
{ value: 'tavily', label: '🤖 Tavily AI Search' },
];
export 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' },
];
export const exaSearchTypes = [
{ value: 'auto', label: 'Auto - Let AI decide' },
{ value: 'keyword', label: 'Keyword - Precise matching' },
{ value: 'neural', label: 'Neural - Semantic search' },
];
export const tavilyTopics = [
{ value: 'general', label: 'General' },
{ value: 'news', label: 'News' },
{ value: 'finance', label: 'Finance' },
];
export const tavilySearchDepths = [
{ value: 'basic', label: 'Basic (1 credit) - Fast search' },
{ value: 'advanced', label: 'Advanced (2 credits) - Deep analysis' },
];
export const tavilyTimeRanges = [
{ value: '', label: 'No time filter' },
{ value: 'day', label: 'Last 24 hours' },
{ value: 'week', label: 'Last week' },
{ value: 'month', label: 'Last month' },
{ value: 'year', label: 'Last year' },
];
export const tavilyAnswerOptions = [
{ value: 'false', label: 'No answer' },
{ value: 'basic', label: 'Basic answer' },
{ value: 'advanced', label: 'Advanced answer' },
];
export const tavilyRawContentOptions = [
{ value: 'false', label: 'No raw content' },
{ value: 'markdown', label: 'Markdown format' },
{ value: 'text', label: 'Plain text' },
];

View File

@@ -0,0 +1,38 @@
/**
* Industry-specific domain suggestions and Exa category mappings
*/
export const getIndustryDomainSuggestions = (industry: string): string[] => {
const domainMap: Record<string, string[]> = {
'Healthcare': ['pubmed.gov', 'nejm.org', 'thelancet.com', 'nih.gov'],
'Technology': ['techcrunch.com', 'wired.com', 'arstechnica.com', 'theverge.com'],
'Finance': ['wsj.com', 'bloomberg.com', 'ft.com', 'reuters.com'],
'Science': ['nature.com', 'sciencemag.org', 'cell.com', 'pnas.org'],
'Business': ['hbr.org', 'forbes.com', 'businessinsider.com', 'mckinsey.com'],
'Marketing': ['marketingland.com', 'adweek.com', 'hubspot.com', 'moz.com'],
'Education': ['edutopia.org', 'chronicle.com', 'insidehighered.com'],
'Real Estate': ['realtor.com', 'zillow.com', 'forbes.com'],
'Entertainment': ['variety.com', 'hollywoodreporter.com', 'deadline.com'],
'Travel': ['lonelyplanet.com', 'nationalgeographic.com', 'travelandleisure.com'],
'Fashion': ['vogue.com', 'elle.com', 'wwd.com'],
'Sports': ['espn.com', 'si.com', 'bleacherreport.com'],
'Law': ['law.com', 'abajournal.com', 'scotusblog.com'],
};
return domainMap[industry] || [];
};
export const getIndustryExaCategory = (industry: string): string | undefined => {
const categoryMap: Record<string, string> = {
'Healthcare': 'research paper',
'Science': 'research paper',
'Finance': 'financial report',
'Technology': 'company',
'Business': 'company',
'Marketing': 'company',
'Education': 'research paper',
'Law': 'pdf',
};
return categoryMap[industry];
};

View File

@@ -0,0 +1,32 @@
/**
* Intelligent input parser - handles sentences, keywords, URLs
*/
export const parseIntelligentInput = (value: string): string[] => {
// If empty, return empty array
if (!value.trim()) return [];
// Detect if input contains URLs
const urlPattern = /(https?:\/\/[^\s,]+)/g;
const urls = value.match(urlPattern) || [];
// Check if input looks like a sentence/paragraph (contains multiple words without commas)
const hasCommas = value.includes(',');
const wordCount = value.trim().split(/\s+/).length;
if (urls.length > 0) {
// User provided URLs - extract them as separate keywords
const textWithoutUrls = value.replace(urlPattern, '').trim();
const textKeywords = textWithoutUrls ? [textWithoutUrls] : [];
return [...urls, ...textKeywords];
} else if (!hasCommas && wordCount > 5) {
// Looks like a sentence/paragraph - treat entire input as single research topic
return [value.trim()];
} else if (hasCommas) {
// Traditional comma-separated keywords
return value.split(',').map(k => k.trim()).filter(Boolean);
} else {
// Short phrase or single keyword
return [value.trim()];
}
};

View File

@@ -0,0 +1,58 @@
/**
* Industry-specific placeholder examples for personalized experience
*/
export const getIndustryPlaceholders = (industry: string): string[] => {
const industryExamples: Record<string, string[]> = {
Healthcare: [
"Research: AI-powered diagnostic tools in clinical practice\n\n💡 What you'll get:\n• FDA-approved AI medical devices\n• Clinical accuracy and patient outcomes\n• Implementation costs and ROI",
"Analyze: Telemedicine adoption trends and patient satisfaction\n\n💡 Research includes:\n• Post-pandemic telehealth growth\n• Remote patient monitoring technologies\n• Insurance coverage and reimbursement",
"Investigate: Personalized medicine and genomic testing advances\n\n💡 You'll discover:\n• Latest genomic sequencing technologies\n• Precision therapy success rates\n• Ethical considerations and regulations"
],
Technology: [
"Investigate: Latest developments in edge computing and IoT\n\n💡 What you'll get:\n• Edge AI deployment strategies\n• 5G integration and performance\n• Industry use cases and benchmarks",
"Compare: Cloud providers for enterprise SaaS applications\n\n💡 Research includes:\n• AWS vs Azure vs GCP feature comparison\n• Cost optimization strategies\n• Security and compliance certifications",
"Analyze: Quantum computing breakthroughs and commercial applications\n\n💡 You'll discover:\n• Latest quantum hardware developments\n• Real-world problem solving examples\n• Investment landscape and timeline"
],
Finance: [
"Research: DeFi regulatory landscape and compliance challenges\n\n💡 What you'll get:\n• Global regulatory frameworks\n• Compliance best practices\n• Risk management strategies",
"Analyze: Digital banking customer retention strategies\n\n💡 Research includes:\n• Neobank growth and market share\n• Customer acquisition costs and LTV\n• Personalization and UX innovations",
"Investigate: ESG investing trends and impact measurement\n\n💡 You'll discover:\n• ESG rating methodologies\n• Fund performance and returns\n• Regulatory requirements and reporting"
],
Marketing: [
"Research: AI-powered marketing automation and personalization\n\n💡 What you'll get:\n• Top marketing AI platforms and features\n• ROI and conversion rate improvements\n• Implementation case studies",
"Analyze: Influencer marketing ROI and authenticity trends\n\n💡 Research includes:\n• Micro vs macro influencer effectiveness\n• Platform-specific engagement rates\n• Brand partnership best practices",
"Investigate: Privacy-first marketing in a cookieless world\n\n💡 You'll discover:\n• First-party data strategies\n• Contextual targeting innovations\n• Compliance with privacy regulations"
],
Business: [
"Research: Remote work policies and hybrid workplace models\n\n💡 What you'll get:\n• Productivity metrics and employee satisfaction\n• Technology infrastructure requirements\n• Cultural impact and change management",
"Analyze: Supply chain resilience and diversification strategies\n\n💡 Research includes:\n• Nearshoring and reshoring trends\n• Technology solutions for visibility\n• Risk mitigation frameworks",
"Investigate: Sustainability initiatives and corporate ESG programs\n\n💡 You'll discover:\n• Industry-specific sustainability benchmarks\n• Cost-benefit analysis of green initiatives\n• Stakeholder communication strategies"
],
Education: [
"Research: EdTech tools for personalized learning experiences\n\n💡 What you'll get:\n• Adaptive learning platform comparisons\n• Student engagement and outcomes data\n• Implementation costs and training needs",
"Analyze: Microlearning and skill-based education trends\n\n💡 Research includes:\n• Corporate training effectiveness\n• Platform and content recommendations\n• ROI and completion rates",
"Investigate: AI tutoring systems and student support tools\n\n💡 You'll discover:\n• Natural language processing advances\n• Student performance improvements\n• Accessibility and inclusion features"
],
'Real Estate': [
"Research: PropTech innovations transforming property management\n\n💡 What you'll get:\n• Smart building technologies and IoT\n• Tenant experience platforms\n• Operational efficiency gains",
"Analyze: Virtual staging and 3D property tours adoption\n\n💡 Research includes:\n• Technology provider comparisons\n• Impact on sales velocity and pricing\n• Cost vs traditional staging",
"Investigate: Real estate tokenization and fractional ownership\n\n💡 You'll discover:\n• Blockchain platforms and regulations\n• Investor demographics and demand\n• Liquidity and exit strategies"
],
Travel: [
"Research: Sustainable tourism trends and eco-travel preferences\n\n💡 What you'll get:\n• Green certification programs\n• Traveler willingness to pay premium\n• Destination best practices",
"Analyze: AI-powered travel personalization and recommendations\n\n💡 Research includes:\n• Recommendation engine technologies\n• Booking conversion rate improvements\n• Customer lifetime value impact",
"Investigate: Bleisure travel and workation destination trends\n\n💡 You'll discover:\n• Remote work-friendly destinations\n• Co-working and accommodation options\n• Digital nomad demographics"
]
};
return industryExamples[industry] || [
"Research: Latest AI advancements in your industry\n\n💡 What you'll get:\n• Recent breakthroughs and innovations\n• Key companies and technologies\n• Expert insights and market trends",
"Write a blog on: Emerging trends shaping your industry in 2025\n\n💡 This will research:\n• Technology disruptions and innovations\n• Regulatory changes and compliance\n• Consumer behavior shifts",
"Analyze: Best practices and success stories in your field\n\n💡 Research includes:\n• Industry leader strategies\n• Implementation case studies\n• ROI and performance metrics",
"https://example.com/article\n\n💡 URL detected! Research will:\n• Extract key insights from the article\n• Find related sources and updates\n• Provide comprehensive context"
];
};

View File

@@ -0,0 +1,25 @@
import { ResearchMode } from '../../../../services/blogWriterApi';
/**
* Smart mode suggestion based on query complexity
*/
export const suggestResearchMode = (keywords: string[]): ResearchMode => {
if (keywords.length === 0) return 'basic';
const totalText = keywords.join(' ');
const totalWords = totalText.split(/\s+/).length;
const hasURL = keywords.some(k => k.startsWith('http'));
// URL detected → comprehensive research
if (hasURL) return 'comprehensive';
// Long detailed query → comprehensive
if (totalWords > 20) return 'comprehensive';
// Medium complexity → targeted
if (totalWords > 10 || keywords.length > 3) return 'targeted';
// Simple query → basic
return 'basic';
};