Added video studio router and endpoints. Added research router and endpoints. Added youtube router and endpoints. Added onboarding utils router and endpoints. Added onboarding utils service. Added onboarding utils models. Added onboarding utils routes. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils.

This commit is contained in:
ajaysi
2026-01-01 17:56:25 +05:30
parent 7512933c65
commit b134e9dc7e
252 changed files with 40333 additions and 2712 deletions

View File

@@ -0,0 +1,738 @@
/**
* IntentResearchWizard Component
*
* A new research experience that:
* 1. Understands what the user wants to accomplish
* 2. Shows quick options for confirmation
* 3. Executes targeted research
* 4. Displays results organized by deliverable type
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Chip,
CircularProgress,
Alert,
Collapse,
IconButton,
Tooltip,
Divider,
Card,
CardContent,
Grid,
Tabs,
Tab,
List,
ListItem,
ListItemIcon,
ListItemText,
Accordion,
AccordionSummary,
AccordionDetails,
Link,
} from '@mui/material';
import {
Search as SearchIcon,
Psychology as BrainIcon,
CheckCircle as CheckIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
TrendingUp as TrendIcon,
FormatQuote as QuoteIcon,
BarChart as StatsIcon,
School as CaseStudyIcon,
Compare as CompareIcon,
Lightbulb as IdeaIcon,
PlayArrow as PlayIcon,
Refresh as RefreshIcon,
OpenInNew as OpenIcon,
} from '@mui/icons-material';
import { useIntentResearch } from './hooks/useIntentResearch';
import {
ResearchIntent,
QuickOption,
IntentDrivenResearchResponse,
DELIVERABLE_DISPLAY,
PURPOSE_DISPLAY,
DEPTH_DISPLAY,
ExpectedDeliverable,
} from './types/intent.types';
interface IntentResearchWizardProps {
onComplete?: (result: IntentDrivenResearchResponse) => void;
onCancel?: () => void;
initialInput?: string;
showQuickMode?: boolean;
}
export const IntentResearchWizard: React.FC<IntentResearchWizardProps> = ({
onComplete,
onCancel,
initialInput = '',
showQuickMode = true,
}) => {
const [inputValue, setInputValue] = useState(initialInput);
const [resultTab, setResultTab] = useState(0);
const {
state,
isLoading,
hasIntent,
hasResults,
needsConfirmation,
confidence,
analyzeIntent,
updateQuickOption,
toggleQuerySelection,
confirmAndExecute,
quickResearch,
reset,
} = useIntentResearch({
usePersona: true,
useCompetitorData: true,
autoExecute: false,
});
// Handle result completion
useEffect(() => {
if (hasResults && state.result && onComplete) {
onComplete(state.result);
}
}, [hasResults, state.result, onComplete]);
// Handle input submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim()) return;
await analyzeIntent(inputValue);
};
// Handle quick research
const handleQuickResearch = async () => {
if (!inputValue.trim()) return;
await quickResearch(inputValue);
};
// Handle confirmation and execution
const handleConfirmAndExecute = async () => {
const result = await confirmAndExecute();
if (result && onComplete) {
onComplete(result);
}
};
// Render input form
const renderInputForm = () => (
<Paper
elevation={0}
sx={{
p: 3,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: 3,
color: 'white',
}}
>
<Typography variant="h5" fontWeight={600} mb={1}>
🔍 What do you want to research?
</Typography>
<Typography variant="body2" mb={3} sx={{ opacity: 0.9 }}>
Enter your topic, question, or describe what you need. AI will understand your intent
and find exactly what you need.
</Typography>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
multiline
rows={3}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder='Examples:&#10;• "AI trends in healthcare 2025"&#10;• "What are the best project management tools?"&#10;• "I need to write a blog about sustainable fashion for millennials"'
sx={{
mb: 2,
'& .MuiOutlinedInput-root': {
backgroundColor: 'rgba(255,255,255,0.95)',
borderRadius: 2,
},
}}
disabled={isLoading}
/>
<Box display="flex" gap={2} justifyContent="flex-end">
{showQuickMode && (
<Button
variant="outlined"
onClick={handleQuickResearch}
disabled={isLoading || !inputValue.trim()}
sx={{
color: 'white',
borderColor: 'rgba(255,255,255,0.5)',
'&:hover': { borderColor: 'white', backgroundColor: 'rgba(255,255,255,0.1)' },
}}
>
Quick Research
</Button>
)}
<Button
type="submit"
variant="contained"
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <BrainIcon />}
disabled={isLoading || !inputValue.trim()}
sx={{
backgroundColor: 'white',
color: '#667eea',
'&:hover': { backgroundColor: 'rgba(255,255,255,0.9)' },
}}
>
{state.isAnalyzing ? 'Analyzing...' : 'Analyze Intent'}
</Button>
</Box>
</form>
</Paper>
);
// Render intent confirmation
const renderIntentConfirmation = () => {
if (!state.intent) return null;
return (
<Paper elevation={0} sx={{ p: 3, mt: 3, borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<BrainIcon color="primary" />
<Typography variant="h6" fontWeight={600}>
AI Understood Your Research
</Typography>
<Chip
size="small"
label={`${Math.round(confidence * 100)}% confident`}
color={confidence > 0.8 ? 'success' : confidence > 0.6 ? 'warning' : 'error'}
/>
</Box>
{/* Analysis Summary */}
<Typography variant="body1" color="text.secondary" mb={3}>
{state.analysisSummary}
</Typography>
{/* Primary Question */}
<Alert severity="info" sx={{ mb: 3 }}>
<Typography fontWeight={500}>
Main Question: {state.intent.primary_question}
</Typography>
</Alert>
{/* Quick Options */}
<Grid container spacing={2} mb={3}>
{state.quickOptions.map((option) => (
<Grid item xs={12} sm={6} key={option.id}>
<Card variant="outlined">
<CardContent sx={{ py: 1.5 }}>
<Typography variant="caption" color="text.secondary">
{option.label}
</Typography>
<Typography variant="body1" fontWeight={500}>
{Array.isArray(option.display)
? option.display.slice(0, 3).join(', ')
: option.display}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
{/* Expected Deliverables */}
<Typography variant="subtitle2" gutterBottom>
What I'll find for you:
</Typography>
<Box display="flex" flexWrap="wrap" gap={1} mb={3}>
{state.intent.expected_deliverables.map((d) => (
<Chip
key={d}
label={DELIVERABLE_DISPLAY[d as ExpectedDeliverable] || d}
color="primary"
variant="outlined"
size="small"
icon={getDeliverableIcon(d)}
/>
))}
</Box>
{/* Suggested Queries (collapsible) */}
<Accordion elevation={0} sx={{ border: '1px solid', borderColor: 'divider' }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle2">
Research Queries ({state.suggestedQueries.length})
</Typography>
</AccordionSummary>
<AccordionDetails>
<List dense>
{state.suggestedQueries.map((query, idx) => (
<ListItem
key={idx}
button
onClick={() => toggleQuerySelection(query)}
selected={state.selectedQueries.some(q => q.query === query.query)}
>
<ListItemIcon>
<Chip
size="small"
label={query.provider.toUpperCase()}
color={query.provider === 'exa' ? 'primary' : 'secondary'}
/>
</ListItemIcon>
<ListItemText
primary={query.query}
secondary={`Finding: ${query.expected_results}`}
/>
</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
{/* Action Buttons */}
<Box display="flex" gap={2} justifyContent="flex-end" mt={3}>
<Button variant="outlined" onClick={reset}>
Start Over
</Button>
<Button
variant="contained"
startIcon={state.isResearching ? <CircularProgress size={20} color="inherit" /> : <PlayIcon />}
onClick={handleConfirmAndExecute}
disabled={state.isResearching}
>
{state.isResearching ? 'Researching...' : 'Start Research'}
</Button>
</Box>
</Paper>
);
};
// Render results
const renderResults = () => {
if (!state.result) return null;
const result = state.result;
// Available tabs based on what we have
const tabs = [
{ id: 'summary', label: 'Summary', count: 0 },
{ id: 'statistics', label: 'Statistics', count: result.statistics.length },
{ id: 'quotes', label: 'Expert Quotes', count: result.expert_quotes.length },
{ id: 'case_studies', label: 'Case Studies', count: result.case_studies.length },
{ id: 'trends', label: 'Trends', count: result.trends.length },
{ id: 'sources', label: 'Sources', count: result.sources.length },
].filter(t => t.id === 'summary' || t.id === 'sources' || t.count > 0);
return (
<Paper elevation={0} sx={{ mt: 3, borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
{/* Header */}
<Box sx={{ p: 3, borderBottom: '1px solid', borderColor: 'divider' }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<CheckIcon color="success" />
<Typography variant="h6" fontWeight={600}>
Research Complete
</Typography>
<Chip
size="small"
label={`${result.sources.length} sources`}
color="primary"
variant="outlined"
/>
</Box>
{/* Executive Summary */}
<Typography variant="body1" color="text.secondary">
{result.executive_summary}
</Typography>
</Box>
{/* Tabs */}
<Tabs
value={resultTab}
onChange={(_, v) => setResultTab(v)}
sx={{ px: 2, borderBottom: '1px solid', borderColor: 'divider' }}
>
{tabs.map((tab, idx) => (
<Tab
key={tab.id}
label={
<Box display="flex" alignItems="center" gap={0.5}>
{tab.label}
{tab.count > 0 && (
<Chip size="small" label={tab.count} color="primary" sx={{ height: 20 }} />
)}
</Box>
}
/>
))}
</Tabs>
{/* Tab Content */}
<Box sx={{ p: 3 }}>
{/* Summary Tab */}
{tabs[resultTab]?.id === 'summary' && (
<Box>
{/* Primary Answer */}
<Alert severity="success" sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Answer to your question:
</Typography>
<Typography>{result.primary_answer}</Typography>
</Alert>
{/* Key Takeaways */}
{result.key_takeaways.length > 0 && (
<Box mb={3}>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Key Takeaways
</Typography>
<List dense>
{result.key_takeaways.map((takeaway, idx) => (
<ListItem key={idx}>
<ListItemIcon>
<IdeaIcon color="primary" fontSize="small" />
</ListItemIcon>
<ListItemText primary={takeaway} />
</ListItem>
))}
</List>
</Box>
)}
{/* Best Practices */}
{result.best_practices.length > 0 && (
<Box mb={3}>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Best Practices
</Typography>
<List dense>
{result.best_practices.map((bp, idx) => (
<ListItem key={idx}>
<ListItemIcon>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary={bp} />
</ListItem>
))}
</List>
</Box>
)}
{/* Suggested Outline */}
{result.suggested_outline.length > 0 && (
<Box>
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Suggested Content Outline
</Typography>
<List dense>
{result.suggested_outline.map((item, idx) => (
<ListItem key={idx}>
<ListItemText primary={item} />
</ListItem>
))}
</List>
</Box>
)}
</Box>
)}
{/* Statistics Tab */}
{tabs[resultTab]?.id === 'statistics' && (
<Grid container spacing={2}>
{result.statistics.map((stat, idx) => (
<Grid item xs={12} md={6} key={idx}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="flex-start" gap={1}>
<StatsIcon color="primary" />
<Box flex={1}>
<Typography variant="body1" fontWeight={500}>
{stat.statistic}
</Typography>
<Typography variant="caption" color="text.secondary">
{stat.context}
</Typography>
<Box display="flex" alignItems="center" gap={1} mt={1}>
<Link href={stat.url} target="_blank" rel="noopener" variant="caption">
{stat.source} <OpenIcon sx={{ fontSize: 12 }} />
</Link>
<Chip
size="small"
label={`${Math.round(stat.credibility * 100)}% credible`}
color={stat.credibility > 0.8 ? 'success' : 'warning'}
/>
</Box>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Expert Quotes Tab */}
{tabs[resultTab]?.id === 'quotes' && (
<Grid container spacing={2}>
{result.expert_quotes.map((quote, idx) => (
<Grid item xs={12} key={idx}>
<Card variant="outlined">
<CardContent>
<Box display="flex" gap={2}>
<QuoteIcon color="primary" sx={{ fontSize: 40 }} />
<Box>
<Typography variant="body1" fontStyle="italic" mb={1}>
"{quote.quote}"
</Typography>
<Typography variant="subtitle2">
— {quote.speaker}
{quote.title && `, ${quote.title}`}
{quote.organization && ` at ${quote.organization}`}
</Typography>
<Link href={quote.url} target="_blank" rel="noopener" variant="caption">
Source: {quote.source} <OpenIcon sx={{ fontSize: 12 }} />
</Link>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Case Studies Tab */}
{tabs[resultTab]?.id === 'case_studies' && (
<Grid container spacing={2}>
{result.case_studies.map((cs, idx) => (
<Grid item xs={12} key={idx}>
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom>
{cs.title}
</Typography>
<Typography variant="subtitle2" color="primary" gutterBottom>
{cs.organization}
</Typography>
<Divider sx={{ my: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">
Challenge
</Typography>
<Typography variant="body2">{cs.challenge}</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">
Solution
</Typography>
<Typography variant="body2">{cs.solution}</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">
Outcome
</Typography>
<Typography variant="body2">{cs.outcome}</Typography>
</Grid>
</Grid>
{cs.key_metrics.length > 0 && (
<Box mt={2} display="flex" gap={1} flexWrap="wrap">
{cs.key_metrics.map((metric, i) => (
<Chip key={i} label={metric} size="small" color="success" variant="outlined" />
))}
</Box>
)}
<Box mt={2}>
<Link href={cs.url} target="_blank" rel="noopener" variant="caption">
Read full case study <OpenIcon sx={{ fontSize: 12 }} />
</Link>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Trends Tab */}
{tabs[resultTab]?.id === 'trends' && (
<Grid container spacing={2}>
{result.trends.map((trend, idx) => (
<Grid item xs={12} md={6} key={idx}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<TrendIcon
color={trend.direction === 'growing' ? 'success' : trend.direction === 'declining' ? 'error' : 'info'}
/>
<Typography variant="subtitle1" fontWeight={500}>
{trend.trend}
</Typography>
<Chip
size="small"
label={trend.direction}
color={trend.direction === 'growing' ? 'success' : trend.direction === 'declining' ? 'error' : 'info'}
/>
</Box>
<Typography variant="body2" color="text.secondary" mb={1}>
{trend.impact}
</Typography>
{trend.timeline && (
<Typography variant="caption" color="text.secondary">
Timeline: {trend.timeline}
</Typography>
)}
<Box mt={1}>
<Typography variant="caption" color="text.secondary">
Evidence:
</Typography>
<List dense>
{trend.evidence.slice(0, 3).map((e, i) => (
<ListItem key={i} sx={{ py: 0 }}>
<ListItemText primary={e} primaryTypographyProps={{ variant: 'caption' }} />
</ListItem>
))}
</List>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Sources Tab */}
{tabs[resultTab]?.id === 'sources' && (
<List>
{result.sources.map((source, idx) => (
<ListItem
key={idx}
component="a"
href={source.url}
target="_blank"
rel="noopener"
sx={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
<ListItemText
primary={source.title}
secondary={
<>
{source.excerpt && <Typography variant="caption">{source.excerpt}</Typography>}
<Box display="flex" gap={1} mt={0.5}>
{source.content_type && (
<Chip size="small" label={source.content_type} variant="outlined" />
)}
<Chip
size="small"
label={`${Math.round(source.relevance_score * 100)}% relevant`}
color="primary"
variant="outlined"
/>
<Chip
size="small"
label={`${Math.round(source.credibility_score * 100)}% credible`}
color={source.credibility_score > 0.8 ? 'success' : 'warning'}
variant="outlined"
/>
</Box>
</>
}
/>
<OpenIcon color="action" />
</ListItem>
))}
</List>
)}
</Box>
{/* Footer */}
<Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider', display: 'flex', justifyContent: 'space-between' }}>
<Button startIcon={<RefreshIcon />} onClick={reset}>
New Research
</Button>
{result.gaps_identified.length > 0 && (
<Tooltip
title={
<Box>
<Typography variant="caption" fontWeight={600}>Gaps Identified:</Typography>
<List dense>
{result.gaps_identified.map((gap, i) => (
<ListItem key={i} sx={{ py: 0 }}>
<ListItemText primary={gap} primaryTypographyProps={{ variant: 'caption' }} />
</ListItem>
))}
</List>
</Box>
}
>
<Chip
icon={<InfoIcon />}
label={`${result.gaps_identified.length} gaps identified`}
color="warning"
variant="outlined"
size="small"
/>
</Tooltip>
)}
</Box>
</Paper>
);
};
return (
<Box>
{/* Error display */}
{state.error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => reset()}>
{state.error}
</Alert>
)}
{/* Input Form (always visible unless we have results) */}
{!hasResults && renderInputForm()}
{/* Intent Confirmation */}
{hasIntent && !hasResults && !state.isResearching && renderIntentConfirmation()}
{/* Loading state during research */}
{state.isResearching && (
<Box display="flex" flexDirection="column" alignItems="center" py={4}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6">Executing Research...</Typography>
<Typography color="text.secondary">
Finding exactly what you need...
</Typography>
</Box>
)}
{/* Results */}
{hasResults && renderResults()}
</Box>
);
};
// Helper function to get icon for deliverable
const getDeliverableIcon = (deliverable: string): React.ReactElement | undefined => {
const iconMap: Record<string, React.ReactElement> = {
key_statistics: <StatsIcon fontSize="small" />,
expert_quotes: <QuoteIcon fontSize="small" />,
case_studies: <CaseStudyIcon fontSize="small" />,
trends: <TrendIcon fontSize="small" />,
comparisons: <CompareIcon fontSize="small" />,
best_practices: <CheckIcon fontSize="small" />,
step_by_step: <PlayIcon fontSize="small" />,
examples: <IdeaIcon fontSize="small" />,
predictions: <TrendIcon fontSize="small" />,
};
return iconMap[deliverable];
};
export default IntentResearchWizard;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
@@ -21,9 +21,10 @@ import {
Business as BusinessIcon,
Assessment as AssessmentIcon,
OpenInNew as OpenInNewIcon,
Link as LinkIcon
Link as LinkIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
import { CompetitorAnalysisResponse } from '../../api/researchConfig';
import { CompetitorAnalysisResponse, refreshCompetitorAnalysis } from '../../api/researchConfig';
interface OnboardingCompetitorModalProps {
open: boolean;
@@ -31,6 +32,7 @@ interface OnboardingCompetitorModalProps {
data: CompetitorAnalysisResponse | null;
loading?: boolean;
error?: string | null;
onRefresh?: (newData: CompetitorAnalysisResponse) => void;
}
export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps> = ({
@@ -38,8 +40,12 @@ export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps>
onClose,
data,
loading = false,
error = null
error = null,
onRefresh
}) => {
const [refreshing, setRefreshing] = useState(false);
const [refreshError, setRefreshError] = useState<string | null>(null);
if (!data && !loading && !error) {
return null;
}
@@ -48,6 +54,24 @@ export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps>
const socialMediaAccounts = data?.social_media_accounts || {};
const researchSummary = data?.research_summary || {};
const handleRefresh = async () => {
setRefreshing(true);
setRefreshError(null);
try {
const newData = await refreshCompetitorAnalysis();
if (newData.success && onRefresh) {
onRefresh(newData);
} else {
setRefreshError(newData.error || 'Failed to refresh competitor analysis');
}
} catch (err: any) {
setRefreshError(err.message || 'Failed to refresh competitor analysis');
} finally {
setRefreshing(false);
}
};
const avgScore = competitors.length > 0
? competitors.reduce((sum, c) => sum + (c.similarity_score || 0), 0) / competitors.length
: 0;
@@ -85,9 +109,33 @@ export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps>
</Typography>
</Box>
</Box>
<Button onClick={onClose} size="small" sx={{ minWidth: 'auto', p: 1 }}>
<CloseIcon />
</Button>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleRefresh}
disabled={refreshing}
startIcon={refreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
variant="outlined"
size="small"
sx={{
minWidth: 'auto',
borderColor: '#0ea5e9',
color: '#0ea5e9',
'&:hover': {
borderColor: '#0284c7',
backgroundColor: 'rgba(14, 165, 233, 0.08)'
},
'&:disabled': {
borderColor: '#cbd5e1',
color: '#94a3b8'
}
}}
>
{refreshing ? 'Refreshing...' : 'Refresh'}
</Button>
<Button onClick={onClose} size="small" sx={{ minWidth: 'auto', p: 1 }}>
<CloseIcon />
</Button>
</Box>
</DialogTitle>
<DialogContent sx={{ py: 3, overflowY: 'auto' }}>
@@ -100,9 +148,9 @@ export const OnboardingCompetitorModal: React.FC<OnboardingCompetitorModalProps>
</Box>
)}
{error && (
{(error || refreshError) && (
<Alert severity="error" sx={{ mb: 3 }}>
<Typography variant="body2">{error}</Typography>
<Typography variant="body2">{error || refreshError}</Typography>
</Alert>
)}

View File

@@ -100,13 +100,13 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
switch (wizard.state.currentStep) {
case 1:
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} />;
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} execution={execution} />;
case 2:
return <StepProgress {...stepProps} execution={execution} />;
case 3:
return <StepResults {...stepProps} />;
return <StepResults {...stepProps} execution={execution} />;
default:
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} />;
return <ResearchInput {...stepProps} advanced={advanced} onAdvancedChange={setAdvanced} execution={execution} />;
}
};
@@ -336,6 +336,51 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
Back
</button>
{/* Intent-Driven Research Button (Primary) - Only show on Step 1 */}
{wizard.state.currentStep === 1 && (
<button
onClick={async () => {
// Analyze intent and execute if successful
const analysis = await execution.analyzeIntent(wizard.state);
if (analysis?.success) {
// If high confidence, auto-execute
if (analysis.intent.confidence >= 0.8 && !analysis.intent.needs_clarification) {
const result = await execution.executeIntentResearch(wizard.state);
if (result?.success) {
wizard.updateState({ currentStep: 3 }); // Skip to results
}
}
}
}}
disabled={!wizard.canGoNext() || execution.isAnalyzingIntent || execution.isExecuting}
style={{
padding: '10px 24px',
background: wizard.canGoNext() && !execution.isAnalyzingIntent
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'rgba(100, 116, 139, 0.2)',
color: wizard.canGoNext() ? 'white' : '#94a3b8',
border: 'none',
borderRadius: '10px',
cursor: wizard.canGoNext() && !execution.isAnalyzingIntent ? 'pointer' : 'not-allowed',
fontSize: '13px',
fontWeight: '600',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
marginRight: '10px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
{execution.isAnalyzingIntent ? (
<>🧠 Analyzing...</>
) : execution.isExecuting ? (
<>🔍 Researching...</>
) : (
<>🧠 Smart Research</>
)}
</button>
)}
<button
onClick={wizard.nextStep}
disabled={!wizard.canGoNext()}

View File

@@ -0,0 +1,323 @@
/**
* useIntentResearch Hook
*
* React hook for managing intent-driven research flow:
* 1. Analyze user input to understand intent
* 2. Show quick options for user confirmation
* 3. Execute research with confirmed intent
* 4. Display results organized by deliverable type
*/
import { useState, useCallback } from 'react';
import { intentResearchApi } from '../../../api/intentResearchApi';
import {
ResearchIntent,
ResearchQuery,
QuickOption,
IntentDrivenResearchResponse,
IntentWizardState,
AnalyzeIntentResponse,
} from '../types/intent.types';
const initialState: IntentWizardState = {
userInput: '',
keywords: [],
intent: null,
suggestedQueries: [],
selectedQueries: [],
quickOptions: [],
analysisSummary: '',
suggestedKeywords: [],
suggestedAngles: [],
isAnalyzing: false,
isResearching: false,
hasConfirmedIntent: false,
result: null,
error: null,
};
export interface UseIntentResearchOptions {
usePersona?: boolean;
useCompetitorData?: boolean;
maxSources?: number;
includeDomains?: string[];
excludeDomains?: string[];
autoExecute?: boolean; // Auto-execute research after intent analysis (if high confidence)
autoExecuteThreshold?: number; // Confidence threshold for auto-execute (default: 0.85)
}
export const useIntentResearch = (options: UseIntentResearchOptions = {}) => {
const [state, setState] = useState<IntentWizardState>(initialState);
const {
usePersona = true,
useCompetitorData = true,
maxSources = 10,
includeDomains = [],
excludeDomains = [],
autoExecute = false,
autoExecuteThreshold = 0.85,
} = options;
/**
* Analyze user input to understand research intent.
*/
const analyzeIntent = useCallback(async (userInput: string) => {
setState(prev => ({
...prev,
userInput,
keywords: userInput.split(' ').filter(k => k.length > 2),
isAnalyzing: true,
error: null,
}));
try {
const response: AnalyzeIntentResponse = await intentResearchApi.analyzeIntent({
user_input: userInput,
keywords: userInput.split(' ').filter(k => k.length > 2),
use_persona: usePersona,
use_competitor_data: useCompetitorData,
});
if (!response.success) {
setState(prev => ({
...prev,
isAnalyzing: false,
error: response.error_message || 'Failed to analyze intent',
}));
return null;
}
const newState: Partial<IntentWizardState> = {
intent: response.intent,
suggestedQueries: response.suggested_queries,
selectedQueries: response.suggested_queries.slice(0, 5), // Select top 5 by default
quickOptions: response.quick_options,
analysisSummary: response.analysis_summary,
suggestedKeywords: response.suggested_keywords,
suggestedAngles: response.suggested_angles,
isAnalyzing: false,
};
setState(prev => ({ ...prev, ...newState }));
// Auto-execute if confidence is high enough
if (
autoExecute &&
response.intent.confidence >= autoExecuteThreshold &&
!response.intent.needs_clarification
) {
// Trigger research automatically
await executeResearchInternal(response.intent, response.suggested_queries.slice(0, 5));
}
return response;
} catch (error: any) {
setState(prev => ({
...prev,
isAnalyzing: false,
error: error.message || 'Failed to analyze intent',
}));
return null;
}
}, [usePersona, useCompetitorData, autoExecute, autoExecuteThreshold]);
/**
* Update a quick option value.
*/
const updateQuickOption = useCallback((optionId: string, newValue: any) => {
setState(prev => {
if (!prev.intent) return prev;
// Update intent based on option
const updatedIntent = { ...prev.intent };
switch (optionId) {
case 'purpose':
updatedIntent.purpose = newValue;
break;
case 'content_output':
updatedIntent.content_output = newValue;
break;
case 'deliverables':
updatedIntent.expected_deliverables = newValue;
break;
case 'depth':
updatedIntent.depth = newValue;
break;
}
return {
...prev,
intent: updatedIntent,
quickOptions: prev.quickOptions.map(opt =>
opt.id === optionId ? { ...opt, value: newValue } : opt
),
};
});
}, []);
/**
* Toggle a query selection.
*/
const toggleQuerySelection = useCallback((query: ResearchQuery) => {
setState(prev => {
const isSelected = prev.selectedQueries.some(q => q.query === query.query);
return {
...prev,
selectedQueries: isSelected
? prev.selectedQueries.filter(q => q.query !== query.query)
: [...prev.selectedQueries, query],
};
});
}, []);
/**
* Confirm intent and execute research.
*/
const confirmAndExecute = useCallback(async () => {
if (!state.intent) {
setState(prev => ({ ...prev, error: 'No intent to confirm' }));
return null;
}
return executeResearchInternal(state.intent, state.selectedQueries);
}, [state.intent, state.selectedQueries]);
/**
* Internal research execution.
*/
const executeResearchInternal = async (
intent: ResearchIntent,
queries: ResearchQuery[]
): Promise<IntentDrivenResearchResponse | null> => {
setState(prev => ({
...prev,
isResearching: true,
hasConfirmedIntent: true,
error: null,
}));
try {
const response = await intentResearchApi.executeIntentResearch({
user_input: state.userInput || intent.original_input,
confirmed_intent: intent,
selected_queries: queries,
max_sources: maxSources,
include_domains: includeDomains,
exclude_domains: excludeDomains,
skip_inference: true,
});
if (!response.success) {
setState(prev => ({
...prev,
isResearching: false,
error: response.error_message || 'Research failed',
}));
return null;
}
setState(prev => ({
...prev,
isResearching: false,
result: response,
}));
return response;
} catch (error: any) {
setState(prev => ({
...prev,
isResearching: false,
error: error.message || 'Research failed',
}));
return null;
}
};
/**
* Quick research - analyze and execute in one step.
* Skips user confirmation.
*/
const quickResearch = useCallback(async (userInput: string) => {
setState(prev => ({
...prev,
userInput,
isAnalyzing: true,
isResearching: true,
error: null,
}));
try {
const response = await intentResearchApi.quickIntentResearch(userInput, {
usePersona,
useCompetitorData,
maxSources,
includeDomains,
excludeDomains,
});
setState(prev => ({
...prev,
isAnalyzing: false,
isResearching: false,
result: response,
intent: response.intent,
hasConfirmedIntent: true,
error: response.success ? null : response.error_message,
}));
return response;
} catch (error: any) {
setState(prev => ({
...prev,
isAnalyzing: false,
isResearching: false,
error: error.message || 'Research failed',
}));
return null;
}
}, [usePersona, useCompetitorData, maxSources, includeDomains, excludeDomains]);
/**
* Reset to initial state.
*/
const reset = useCallback(() => {
setState(initialState);
}, []);
/**
* Clear just the results.
*/
const clearResults = useCallback(() => {
setState(prev => ({
...prev,
result: null,
hasConfirmedIntent: false,
}));
}, []);
return {
// State
state,
// Derived state
isLoading: state.isAnalyzing || state.isResearching,
hasIntent: state.intent !== null,
hasResults: state.result !== null,
needsConfirmation: state.intent?.needs_clarification || false,
confidence: state.intent?.confidence || 0,
// Actions
analyzeIntent,
updateQuickOption,
toggleQuerySelection,
confirmAndExecute,
quickResearch,
reset,
clearResults,
};
};
export default useIntentResearch;

View File

@@ -1,12 +1,26 @@
import { useState, useCallback } from 'react';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../../services/blogWriterApi';
import { useResearchPolling } from '../../../hooks/usePolling';
import { researchCache } from '../../../services/researchCache';
import { WizardState } from '../types/research.types';
import { researchEngineApi, ResearchEngineRequest } from '../../../services/researchEngineApi';
import { useResearchPolling } from '../../../hooks/usePolling';
import { intentResearchApi } from '../../../api/intentResearchApi';
import {
ResearchIntent,
IntentDrivenResearchResponse,
AnalyzeIntentResponse
} from '../types/intent.types';
export const useResearchExecution = () => {
const [isExecuting, setIsExecuting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<any>(null);
// Intent-driven research state
const [isAnalyzingIntent, setIsAnalyzingIntent] = useState(false);
const [intentAnalysis, setIntentAnalysis] = useState<AnalyzeIntentResponse | null>(null);
const [confirmedIntent, setConfirmedIntent] = useState<ResearchIntent | null>(null);
const [intentResult, setIntentResult] = useState<IntentDrivenResearchResponse | null>(null);
const [useIntentMode, setUseIntentMode] = useState(true); // Enable by default
const polling = useResearchPolling({
onComplete: (result) => {
@@ -19,6 +33,7 @@ export const useResearchExecution = () => {
);
}
setIsExecuting(false);
setResult(result);
},
onError: (error) => {
console.error('Research polling error:', error);
@@ -41,18 +56,55 @@ export const useResearchExecution = () => {
if (cachedResult) {
setIsExecuting(false);
setResult(cachedResult);
return 'cached';
}
const payload: BlogResearchRequest = {
// Build Research Engine request (tool-agnostic)
const payload: ResearchEngineRequest = {
query: state.keywords.join(' ') || 'research',
keywords: state.keywords,
goal: 'factual',
depth: state.researchMode === 'basic' ? 'standard' : state.researchMode === 'comprehensive' ? 'comprehensive' : 'standard',
provider: state.config.provider || 'auto',
content_type: 'blog',
industry: state.industry,
target_audience: state.targetAudience,
research_mode: state.researchMode,
config: state.config,
max_sources: state.config.max_sources,
recency: state.config.tavily_time_range,
include_domains: state.config.exa_include_domains || state.config.tavily_include_domains,
exclude_domains: state.config.exa_exclude_domains || state.config.tavily_exclude_domains,
advanced_mode: true, // expose raw params if provided
// Exa params
exa_category: state.config.exa_category,
exa_search_type: state.config.exa_search_type,
// Tavily params
tavily_topic: state.config.tavily_topic,
tavily_search_depth: state.config.tavily_search_depth,
tavily_include_answer: state.config.tavily_include_answer,
tavily_include_raw_content: state.config.tavily_include_raw_content,
tavily_time_range: state.config.tavily_time_range,
tavily_country: state.config.tavily_country,
config: state.config, // keep compatibility
};
const { task_id } = await blogWriterApi.startResearch(payload);
// For fast smoke tests: use synchronous path when basic mode
if (state.researchMode === 'basic') {
const syncResult = await researchEngineApi.execute(payload);
// Cache and surface immediately
researchCache.cacheResult(
state.keywords,
state.industry,
state.targetAudience,
syncResult
);
setResult(syncResult);
setIsExecuting(false);
return 'sync';
}
// Start async research to reuse existing progress step
const { task_id } = await researchEngineApi.start(payload);
polling.startPolling(task_id);
return task_id;
} catch (err) {
@@ -69,14 +121,173 @@ export const useResearchExecution = () => {
setError(null);
}, [polling]);
/**
* Analyze user input to understand research intent.
* Call this before executeResearch to show intent confirmation.
*/
const analyzeIntent = useCallback(async (state: WizardState): Promise<AnalyzeIntentResponse | null> => {
setIsAnalyzingIntent(true);
setError(null);
setIntentAnalysis(null);
setConfirmedIntent(null);
try {
const userInput = state.keywords.join(' ');
const response = await intentResearchApi.analyzeIntent({
user_input: userInput,
keywords: state.keywords,
use_persona: true,
use_competitor_data: true,
});
setIntentAnalysis(response);
// Auto-confirm if confidence is high and no clarification needed
if (response.success && response.intent.confidence >= 0.85 && !response.intent.needs_clarification) {
setConfirmedIntent(response.intent);
}
setIsAnalyzingIntent(false);
return response;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to analyze intent';
setError(errorMessage);
setIsAnalyzingIntent(false);
return null;
}
}, []);
/**
* Confirm the analyzed intent (possibly with user modifications).
*/
const confirmIntent = useCallback((intent: ResearchIntent) => {
setConfirmedIntent(intent);
}, []);
/**
* Update a specific field in the analyzed intent.
*/
const updateIntentField = useCallback(<K extends keyof ResearchIntent>(
field: K,
value: ResearchIntent[K]
) => {
if (intentAnalysis?.intent) {
const updatedIntent = { ...intentAnalysis.intent, [field]: value };
setIntentAnalysis({
...intentAnalysis,
intent: updatedIntent,
});
}
}, [intentAnalysis]);
/**
* Execute research using intent-driven approach.
*/
const executeIntentResearch = useCallback(async (state: WizardState): Promise<IntentDrivenResearchResponse | null> => {
// First analyze intent if not already done
let intent = confirmedIntent;
if (!intent) {
const analysis = await analyzeIntent(state);
if (!analysis?.success) {
return null;
}
intent = analysis.intent;
}
setIsExecuting(true);
setError(null);
try {
const response = await intentResearchApi.executeIntentResearch({
user_input: state.keywords.join(' '),
confirmed_intent: intent,
selected_queries: intentAnalysis?.suggested_queries?.slice(0, 5),
max_sources: state.config.max_sources || 10,
include_domains: state.config.exa_include_domains || state.config.tavily_include_domains || [],
exclude_domains: state.config.exa_exclude_domains || state.config.tavily_exclude_domains || [],
skip_inference: true,
});
if (!response.success) {
setError(response.error_message || 'Research failed');
setIsExecuting(false);
return null;
}
setIntentResult(response);
// Also set the legacy result for backward compatibility with StepResults
// Transform intent result to match the expected format
const legacyResult = {
success: true,
sources: response.sources.map(s => ({
title: s.title,
url: s.url,
excerpt: s.excerpt ?? undefined, // Convert null to undefined
credibility_score: s.credibility_score,
})),
keyword_analysis: {
primary_keywords: state.keywords,
secondary: response.suggested_outline,
},
competitor_analysis: {},
suggested_angles: response.key_takeaways,
search_queries: [],
// Add intent-specific data for enhanced display
intent_result: response,
};
setResult(legacyResult);
setIsExecuting(false);
// Cache the result
researchCache.cacheResult(
state.keywords,
state.industry,
state.targetAudience,
legacyResult
);
return response;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Research failed';
setError(errorMessage);
setIsExecuting(false);
return null;
}
}, [confirmedIntent, intentAnalysis, analyzeIntent]);
/**
* Clear intent analysis state.
*/
const clearIntent = useCallback(() => {
setIntentAnalysis(null);
setConfirmedIntent(null);
setIntentResult(null);
}, []);
return {
// Legacy API
executeResearch,
stopExecution,
isExecuting,
error,
progressMessages: polling.progressMessages,
currentStatus: polling.currentStatus,
result: polling.result,
result: result ?? polling.result,
// Intent-driven API
useIntentMode,
setUseIntentMode,
isAnalyzingIntent,
intentAnalysis,
confirmedIntent,
intentResult,
analyzeIntent,
confirmIntent,
updateIntentField,
executeIntentResearch,
clearIntent,
};
};

View File

@@ -5,15 +5,18 @@ import { ResearchMode, ResearchConfig, BlogResearchResponse } from '../../../ser
const WIZARD_STATE_KEY = 'alwrity_research_wizard_state';
const MAX_STEPS = 3; // Input (combined) -> Progress -> Results
// Default state: "General" is a placeholder that gets replaced by persona defaults on mount
// Phase 2: Backend never returns "General" - persona defaults are always hyper-personalized
// ResearchInput.tsx loads persona defaults and updates these values immediately
const defaultState: WizardState = {
currentStep: 1,
keywords: [],
industry: 'General',
targetAudience: 'General',
researchMode: 'basic' as ResearchMode,
industry: 'General', // Placeholder - replaced by persona defaults on mount
targetAudience: 'General', // Placeholder - replaced by persona defaults on mount
researchMode: 'comprehensive' as ResearchMode,
config: {
mode: 'basic',
provider: 'google',
mode: 'comprehensive',
provider: 'exa', // Phase 2: Default to Exa (primary provider)
max_sources: 10,
include_statistics: true,
include_expert_quotes: true,

View File

@@ -3,3 +3,7 @@ export { useResearchWizard } from './hooks/useResearchWizard';
export { useResearchExecution } from './hooks/useResearchExecution';
export * from './types/research.types';
// Intent-driven research exports
export { IntentResearchWizard } from './IntentResearchWizard';
export { useIntentResearch } from './hooks/useIntentResearch';
export * from './types/intent.types';

View File

@@ -7,7 +7,8 @@ import {
ResearchHistoryEntry
} from '../../../utils/researchHistory';
import {
expandKeywords
expandKeywords,
expandKeywordsWithPersona
} from '../../../utils/keywordExpansion';
import {
generateResearchAngles
@@ -28,13 +29,17 @@ import { CurrentKeywords } from './components/CurrentKeywords';
import { ResearchAngles } from './components/ResearchAngles';
import { TavilyOptions } from './components/TavilyOptions';
import { ExaOptions } from './components/ExaOptions';
import { PersonalizationIndicator, PersonalizationBadge } from './components/PersonalizationIndicator';
import { IntentConfirmationPanel } from './components/IntentConfirmationPanel';
import { ResearchExecution } from '../types/research.types';
interface ResearchInputProps extends WizardStepProps {
advanced?: boolean;
onAdvancedChange?: (advanced: boolean) => void;
execution?: ResearchExecution;
}
export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, advanced: advancedProp, onAdvancedChange }) => {
export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, onNext, advanced: advancedProp, onAdvancedChange, execution }) => {
const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability | null>(null);
const [loadingConfig, setLoadingConfig] = useState(true);
@@ -46,6 +51,18 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
suggestions: string[];
} | null>(null);
const [researchAngles, setResearchAngles] = useState<string[]>([]);
const [researchPersona, setResearchPersona] = useState<{
research_angles?: string[];
recommended_presets?: Array<{
name: string;
keywords: string | string[];
description?: string;
}>;
suggested_keywords?: string[];
keyword_expansion_patterns?: Record<string, string[]>;
industry?: string;
target_audience?: string;
} | null>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
// Use prop if provided, otherwise use local state
@@ -81,36 +98,165 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
exa_key_status: 'missing'
});
// Apply persona defaults if not already set (with null checks)
// Phase 2: Apply persona defaults from API
// Backend now returns hyper-personalized values (never "General")
// Always apply if we have values and user hasn't customized
if (config?.persona_defaults) {
if (config.persona_defaults.industry && state.industry === 'General') {
onUpdate({ industry: config.persona_defaults.industry });
const defaults = config.persona_defaults;
// Log whether research persona exists
console.log('[ResearchInput] Persona defaults loaded:', {
hasResearchPersona: defaults.has_research_persona,
industry: defaults.industry,
targetAudience: defaults.target_audience,
hasDomains: defaults.suggested_domains?.length > 0
});
// Apply industry if provided and user hasn't customized
// Phase 2: Backend never returns "General", so we apply unless user has real value
if (defaults.industry && (!state.industry || state.industry === 'General')) {
onUpdate({ industry: defaults.industry });
}
if (config.persona_defaults.target_audience && state.targetAudience === 'General') {
onUpdate({ targetAudience: config.persona_defaults.target_audience });
// Apply target audience if provided
if (defaults.target_audience && (!state.targetAudience || state.targetAudience === 'General')) {
onUpdate({ targetAudience: defaults.target_audience });
}
// Apply suggested Exa domains if Exa is available and not already set
if (config.provider_availability?.exa_available && config.persona_defaults.suggested_domains?.length > 0) {
if (config.provider_availability?.exa_available && defaults.suggested_domains?.length > 0) {
if (!state.config.exa_include_domains || state.config.exa_include_domains.length === 0) {
onUpdate({
config: {
...state.config,
exa_include_domains: config.persona_defaults.suggested_domains
exa_include_domains: defaults.suggested_domains
}
});
}
}
// Apply suggested Exa category if available
if (config.persona_defaults.suggested_exa_category && !state.config.exa_category) {
if (defaults.suggested_exa_category && !state.config.exa_category) {
onUpdate({
config: {
...state.config,
exa_category: config.persona_defaults.suggested_exa_category
exa_category: defaults.suggested_exa_category
}
});
}
// Phase 2+: Apply enhanced Exa defaults from research persona
if (defaults.suggested_exa_search_type && !state.config.exa_search_type) {
onUpdate({
config: {
...state.config,
exa_search_type: defaults.suggested_exa_search_type as 'auto' | 'keyword' | 'neural'
}
});
}
// Phase 2+: Apply Tavily defaults from research persona
if (defaults.suggested_tavily_topic && !state.config.tavily_topic) {
onUpdate({
config: {
...state.config,
tavily_topic: defaults.suggested_tavily_topic as 'general' | 'news' | 'finance'
}
});
}
if (defaults.suggested_tavily_search_depth && !state.config.tavily_search_depth) {
onUpdate({
config: {
...state.config,
tavily_search_depth: defaults.suggested_tavily_search_depth as 'basic' | 'advanced'
}
});
}
if (defaults.suggested_tavily_include_answer && !state.config.tavily_include_answer) {
const answerValue = defaults.suggested_tavily_include_answer === 'true' ? true :
defaults.suggested_tavily_include_answer === 'false' ? false :
defaults.suggested_tavily_include_answer as 'basic' | 'advanced';
onUpdate({
config: {
...state.config,
tavily_include_answer: answerValue
}
});
}
if (defaults.suggested_tavily_time_range && !state.config.tavily_time_range) {
onUpdate({
config: {
...state.config,
tavily_time_range: defaults.suggested_tavily_time_range as 'day' | 'week' | 'month' | 'year'
}
});
}
if (defaults.suggested_tavily_raw_content_format && !state.config.tavily_include_raw_content) {
const rawContentValue = defaults.suggested_tavily_raw_content_format === 'true' ? true :
defaults.suggested_tavily_raw_content_format === 'false' ? false :
defaults.suggested_tavily_raw_content_format as 'markdown' | 'text';
onUpdate({
config: {
...state.config,
tavily_include_raw_content: rawContentValue
}
});
}
// Phase 2: Apply additional hyper-personalization defaults from research persona
if (defaults.has_research_persona && config.research_persona) {
console.log('[ResearchInput] Applying research persona hyper-personalization:', {
researchMode: defaults.default_research_mode,
provider: defaults.default_provider,
suggestedKeywords: defaults.suggested_keywords?.length || 0,
researchAngles: defaults.research_angles?.length || 0,
recommendedPresets: config.research_persona.recommended_presets?.length || 0
});
// Store research persona data for personalized placeholders, keyword expansion, and research angles
setResearchPersona({
research_angles: config.research_persona.research_angles || defaults.research_angles,
recommended_presets: config.research_persona.recommended_presets || [],
suggested_keywords: config.research_persona.suggested_keywords || defaults.suggested_keywords,
keyword_expansion_patterns: config.research_persona.keyword_expansion_patterns,
industry: config.research_persona.default_industry || defaults.industry,
target_audience: config.research_persona.default_target_audience || defaults.target_audience
});
// Apply default research mode if not already customized
if (defaults.default_research_mode && state.researchMode === 'comprehensive') {
const validModes = ['basic', 'comprehensive', 'targeted'] as const;
if (validModes.includes(defaults.default_research_mode as typeof validModes[number])) {
onUpdate({ researchMode: defaults.default_research_mode as typeof validModes[number] });
}
}
// Apply default provider (only if it's available)
if (defaults.default_provider) {
const validProviders = ['exa', 'tavily', 'google'] as const;
type ValidProvider = typeof validProviders[number];
if (validProviders.includes(defaults.default_provider as ValidProvider)) {
const providerAvailable =
(defaults.default_provider === 'exa' && config.provider_availability?.exa_available) ||
(defaults.default_provider === 'tavily' && config.provider_availability?.tavily_available) ||
(defaults.default_provider === 'google' && config.provider_availability?.google_available);
if (providerAvailable && !state.config.provider) {
onUpdate({
config: {
...state.config,
provider: defaults.default_provider as ValidProvider
}
});
}
}
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -135,8 +281,8 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
loadConfig();
}, []); // Only run once on mount
// Get industry-specific placeholders
const placeholderExamples = getIndustryPlaceholders(state.industry);
// Get industry-specific placeholders, enhanced with research persona data
const placeholderExamples = getIndustryPlaceholders(state.industry, researchPersona || undefined);
// Rotate placeholder examples every 4 seconds
useEffect(() => {
@@ -151,41 +297,26 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
setCurrentPlaceholder(0);
}, [state.industry]);
// Auto-set provider based on research mode
// Auto-set provider based on availability
// Priority: Exa → Tavily → Google for ALL modes (including basic)
// This provides better semantic search results for content creators
useEffect(() => {
if (!providerAvailability) return;
// Priority: Exa → Tavily → Google for all modes
let newProvider: ResearchProvider = 'google';
switch (state.researchMode) {
case 'basic':
// Basic: Google only (fast, simple)
newProvider = 'google';
break;
case 'comprehensive':
// Comprehensive: Prefer Exa if available, then Tavily, fallback to Google
if (providerAvailability.exa_available) {
newProvider = 'exa';
} else if (providerAvailability.tavily_available) {
newProvider = 'tavily';
} else {
newProvider = 'google';
}
break;
case 'targeted':
// Targeted: Prefer Exa if available, then Tavily, fallback to Google
if (providerAvailability.exa_available) {
newProvider = 'exa';
} else if (providerAvailability.tavily_available) {
newProvider = 'tavily';
} else {
newProvider = 'google';
}
break;
if (providerAvailability.exa_available) {
newProvider = 'exa';
} else if (providerAvailability.tavily_available) {
newProvider = 'tavily';
} else {
newProvider = 'google';
}
// Only update if provider changed
if (state.config.provider !== newProvider) {
console.log('[ResearchInput] Auto-selecting provider:', newProvider, 'for mode:', state.researchMode);
onUpdate({ config: { ...state.config, provider: newProvider } });
}
}, [state.researchMode, providerAvailability]);
@@ -225,26 +356,70 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
}, [state.industry, providerAvailability]);
// Expand keywords when keywords or industry changes
// Enhanced to use research persona data if available
useEffect(() => {
if (state.keywords.length > 0 && state.industry !== 'General') {
const expansion = expandKeywords(state.keywords, state.industry);
if (state.keywords.length > 0) {
let expansion;
// If we have research persona with keyword expansion patterns, use them
if (researchPersona?.keyword_expansion_patterns && Object.keys(researchPersona.keyword_expansion_patterns).length > 0) {
expansion = expandKeywordsWithPersona(state.keywords, researchPersona.keyword_expansion_patterns, researchPersona.suggested_keywords);
} else if (state.industry !== 'General') {
// Fallback to industry-based expansion
expansion = expandKeywords(state.keywords, state.industry);
} else {
expansion = { original: state.keywords, expanded: state.keywords, suggestions: [] };
}
setKeywordExpansion(expansion);
} else {
setKeywordExpansion(null);
}
}, [state.keywords, state.industry]);
}, [state.keywords, state.industry, researchPersona]);
// Generate research angles when keywords change
// Enhanced to prioritize research persona angles if available
useEffect(() => {
if (state.keywords.length > 0) {
// Use the first keyword (or joined keywords) as the query
const query = state.keywords.join(' ');
const angles = generateResearchAngles(query, state.industry);
setResearchAngles(angles);
let angles: string[] = [];
// Priority 1: Use research persona angles if available and relevant
if (researchPersona?.research_angles && researchPersona.research_angles.length > 0) {
// Filter persona angles that are relevant to the current query
const relevantPersonaAngles = researchPersona.research_angles
.filter(angle => {
const angleLower = angle.toLowerCase();
const queryLower = query.toLowerCase();
// Check if angle contains any keyword from query or vice versa
return state.keywords.some(kw => angleLower.includes(kw.toLowerCase()) || queryLower.includes(kw.toLowerCase())) ||
angleLower.includes(queryLower) || queryLower.includes(angleLower);
})
.slice(0, 3); // Use top 3 relevant persona angles
angles.push(...relevantPersonaAngles);
}
// Priority 2: Generate additional angles using pattern matching
const generatedAngles = generateResearchAngles(query, state.industry);
// Merge and deduplicate, prioritizing persona angles
const allAngles = [...angles, ...generatedAngles];
const uniqueAngles = Array.from(new Set(allAngles.map(a => a.toLowerCase())))
.slice(0, 5) // Limit to 5 total
.map(a => {
// Find original casing from persona angles first, then generated
const personaMatch = angles.find(pa => pa.toLowerCase() === a);
if (personaMatch) return personaMatch;
const generatedMatch = generatedAngles.find(ga => ga.toLowerCase() === a);
return generatedMatch || a.charAt(0).toUpperCase() + a.slice(1);
});
setResearchAngles(uniqueAngles);
} else {
setResearchAngles([]);
}
}, [state.keywords, state.industry]);
}, [state.keywords, state.industry, researchPersona]);
// Event handlers
const handleKeywordsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -356,6 +531,11 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
fontSize: '20px',
}}>🔍</span>
Research Topic & Keywords
<PersonalizationIndicator
type="placeholder"
hasPersona={!!researchPersona}
source={researchPersona ? "from your research persona" : undefined}
/>
</label>
{/* Advanced Toggle and Upload Button */}
@@ -482,6 +662,26 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
{/* Smart Input Detection Indicator */}
<SmartInputIndicator keywords={state.keywords} />
{/* Intent Analysis Panel - Show when intent analysis is available */}
{execution && (execution.isAnalyzingIntent || execution.intentAnalysis) && (
<IntentConfirmationPanel
isAnalyzing={execution.isAnalyzingIntent}
intentAnalysis={execution.intentAnalysis}
confirmedIntent={execution.confirmedIntent}
onConfirm={execution.confirmIntent}
onUpdateField={execution.updateIntentField}
onExecute={async () => {
const result = await execution.executeIntentResearch(state);
if (result?.success) {
// Skip to results step
onUpdate({ currentStep: 3 });
}
}}
onDismiss={execution.clearIntent}
isExecuting={execution.isExecuting}
/>
)}
{/* Keyword Expansion Suggestions */}
{keywordExpansion && keywordExpansion.suggestions.length > 0 && (
<KeywordExpansion
@@ -502,6 +702,7 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, a
<ResearchAngles
angles={researchAngles}
onUseAngle={handleUseAngle}
hasPersona={!!researchPersona}
/>
</div>

View File

@@ -58,7 +58,12 @@ export const StepProgress: React.FC<WizardStepProps> = ({ state, onNext, onUpdat
return '#1976d2';
};
const providerName = state.config.provider === 'exa' ? 'Exa Neural' : 'Google Search';
const providerName =
state.config.provider === 'exa'
? 'Exa Neural'
: state.config.provider === 'tavily'
? 'Tavily AI Search'
: 'Google Search';
const modeName = state.researchMode === 'basic' ? 'Basic' : state.researchMode === 'comprehensive' ? 'Comprehensive' : 'Targeted';
return (

View File

@@ -1,9 +1,20 @@
import React from 'react';
import { WizardStepProps } from '../types/research.types';
import { WizardStepProps, ResearchExecution } from '../types/research.types';
import { ResearchResults } from '../../BlogWriter/ResearchResults';
import { BlogResearchResponse } from '../../../services/blogWriterApi';
import { IntentResultsDisplay } from './components/IntentResultsDisplay';
import { IntentDrivenResearchResponse } from '../types/intent.types';
export const StepResults: React.FC<WizardStepProps> = ({ state, onUpdate, onBack }) => {
interface StepResultsProps extends WizardStepProps {
execution?: ResearchExecution;
}
export const StepResults: React.FC<StepResultsProps> = ({ state, onUpdate, onBack, execution }) => {
// Check if we have intent-driven results
const intentResult: IntentDrivenResearchResponse | null =
execution?.intentResult ||
(state.results as any)?.intent_result ||
null;
if (!state.results) {
return (
<div style={{ padding: '24px', textAlign: 'center' }}>
@@ -100,8 +111,13 @@ export const StepResults: React.FC<WizardStepProps> = ({ state, onUpdate, onBack
borderRadius: '8px',
border: '1px solid #e0e0e0',
overflow: 'hidden',
padding: intentResult ? '16px' : '0',
}}>
<ResearchResults research={state.results} />
{intentResult ? (
<IntentResultsDisplay result={intentResult} />
) : (
<ResearchResults research={state.results} />
)}
</div>
{/* Action Section */}

View File

@@ -0,0 +1,381 @@
/**
* IntentConfirmationPanel Component
*
* Shows the AI-inferred research intent and allows user to confirm or modify.
* Embedded in the existing ResearchInput component.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
Paper,
Button,
Alert,
CircularProgress,
Collapse,
IconButton,
Tooltip,
Grid,
Card,
CardContent,
FormControl,
Select,
MenuItem,
InputLabel,
} from '@mui/material';
import {
Psychology as BrainIcon,
CheckCircle as CheckIcon,
Close as CloseIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
PlayArrow as PlayIcon,
Edit as EditIcon,
} from '@mui/icons-material';
import {
ResearchIntent,
AnalyzeIntentResponse,
ExpectedDeliverable,
ResearchPurpose,
ContentOutput,
ResearchDepthLevel,
DELIVERABLE_DISPLAY,
PURPOSE_DISPLAY,
DEPTH_DISPLAY,
CONTENT_OUTPUT_DISPLAY,
} from '../../types/intent.types';
interface IntentConfirmationPanelProps {
isAnalyzing: boolean;
intentAnalysis: AnalyzeIntentResponse | null;
confirmedIntent: ResearchIntent | null;
onConfirm: (intent: ResearchIntent) => void;
onUpdateField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
onExecute: () => void;
onDismiss: () => void;
isExecuting: boolean;
}
export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = ({
isAnalyzing,
intentAnalysis,
confirmedIntent,
onConfirm,
onUpdateField,
onExecute,
onDismiss,
isExecuting,
}) => {
const [showDetails, setShowDetails] = React.useState(false);
const [isEditing, setIsEditing] = React.useState(false);
// Loading state
if (isAnalyzing) {
return (
<Paper
elevation={0}
sx={{
p: 3,
mt: 2,
borderRadius: 2,
border: '1px solid',
borderColor: 'primary.light',
background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)',
}}
>
<Box display="flex" alignItems="center" gap={2}>
<CircularProgress size={24} />
<Box>
<Typography variant="subtitle1" fontWeight={600}>
🧠 Analyzing your research intent...
</Typography>
<Typography variant="body2" color="text.secondary">
AI is understanding what you want to accomplish
</Typography>
</Box>
</Box>
</Paper>
);
}
// No analysis yet
if (!intentAnalysis || !intentAnalysis.success) {
return null;
}
const intent = intentAnalysis.intent;
const confidence = intent.confidence;
const isHighConfidence = confidence >= 0.8;
return (
<Paper
elevation={0}
sx={{
mt: 2,
borderRadius: 2,
border: '1px solid',
borderColor: isHighConfidence ? 'success.light' : 'warning.light',
overflow: 'hidden',
}}
>
{/* Header */}
<Box
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: isHighConfidence
? 'linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(67, 160, 71, 0.1) 100%)'
: 'linear-gradient(135deg, rgba(255, 152, 0, 0.1) 0%, rgba(251, 140, 0, 0.1) 100%)',
}}
>
<Box display="flex" alignItems="center" gap={1.5}>
<BrainIcon color={isHighConfidence ? 'success' : 'warning'} />
<Box>
<Typography variant="subtitle1" fontWeight={600}>
AI Understood Your Research
</Typography>
<Typography variant="caption" color="text.secondary">
{intentAnalysis.analysis_summary}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Chip
size="small"
label={`${Math.round(confidence * 100)}% confident`}
color={isHighConfidence ? 'success' : 'warning'}
variant="outlined"
/>
<IconButton size="small" onClick={onDismiss}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{/* Main Content */}
<Box sx={{ p: 2 }}>
{/* Primary Question */}
<Alert
severity="info"
sx={{ mb: 2 }}
icon={<CheckIcon />}
>
<Typography variant="body2" fontWeight={500}>
<strong>Main Question:</strong> {intent.primary_question}
</Typography>
</Alert>
{/* Quick Summary Grid */}
<Grid container spacing={2} mb={2}>
{/* Purpose */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ py: 1, px: 1.5, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" color="text.secondary">
Purpose
</Typography>
{isEditing ? (
<FormControl size="small" fullWidth sx={{ mt: 0.5 }}>
<Select
value={intent.purpose}
onChange={(e) => onUpdateField('purpose', e.target.value as ResearchPurpose)}
>
{Object.entries(PURPOSE_DISPLAY).map(([key, label]) => (
<MenuItem key={key} value={key}>{label}</MenuItem>
))}
</Select>
</FormControl>
) : (
<Typography variant="body2" fontWeight={500}>
{PURPOSE_DISPLAY[intent.purpose as ResearchPurpose] || intent.purpose}
</Typography>
)}
</CardContent>
</Card>
</Grid>
{/* Content Type */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ py: 1, px: 1.5, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" color="text.secondary">
Creating
</Typography>
{isEditing ? (
<FormControl size="small" fullWidth sx={{ mt: 0.5 }}>
<Select
value={intent.content_output}
onChange={(e) => onUpdateField('content_output', e.target.value as ContentOutput)}
>
{Object.entries(CONTENT_OUTPUT_DISPLAY).map(([key, label]) => (
<MenuItem key={key} value={key}>{label}</MenuItem>
))}
</Select>
</FormControl>
) : (
<Typography variant="body2" fontWeight={500}>
{CONTENT_OUTPUT_DISPLAY[intent.content_output as ContentOutput] || intent.content_output}
</Typography>
)}
</CardContent>
</Card>
</Grid>
{/* Depth */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ py: 1, px: 1.5, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" color="text.secondary">
Depth
</Typography>
{isEditing ? (
<FormControl size="small" fullWidth sx={{ mt: 0.5 }}>
<Select
value={intent.depth}
onChange={(e) => onUpdateField('depth', e.target.value as ResearchDepthLevel)}
>
{Object.entries(DEPTH_DISPLAY).map(([key, label]) => (
<MenuItem key={key} value={key}>{label}</MenuItem>
))}
</Select>
</FormControl>
) : (
<Typography variant="body2" fontWeight={500}>
{DEPTH_DISPLAY[intent.depth as ResearchDepthLevel] || intent.depth}
</Typography>
)}
</CardContent>
</Card>
</Grid>
{/* Queries */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ py: 1, px: 1.5, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" color="text.secondary">
Queries
</Typography>
<Typography variant="body2" fontWeight={500}>
{intentAnalysis.suggested_queries?.length || 0} targeted
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* What we'll find */}
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom display="block">
What I'll find for you:
</Typography>
<Box display="flex" flexWrap="wrap" gap={0.5}>
{intent.expected_deliverables.slice(0, 5).map((d) => (
<Chip
key={d}
label={DELIVERABLE_DISPLAY[d as ExpectedDeliverable] || d}
size="small"
color="primary"
variant="outlined"
/>
))}
{intent.expected_deliverables.length > 5 && (
<Chip
label={`+${intent.expected_deliverables.length - 5} more`}
size="small"
variant="outlined"
/>
)}
</Box>
</Box>
{/* Expandable Details */}
<Collapse in={showDetails}>
<Box sx={{ pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
{/* Secondary Questions */}
{intent.secondary_questions.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom display="block">
Also answering:
</Typography>
{intent.secondary_questions.slice(0, 3).map((q, idx) => (
<Typography key={idx} variant="body2" sx={{ ml: 1 }}>
• {q}
</Typography>
))}
</Box>
)}
{/* Focus Areas */}
{intent.focus_areas.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom display="block">
Focus areas:
</Typography>
<Box display="flex" flexWrap="wrap" gap={0.5}>
{intent.focus_areas.map((area, idx) => (
<Chip key={idx} label={area} size="small" variant="outlined" />
))}
</Box>
</Box>
)}
{/* Research Angles */}
{intentAnalysis.suggested_angles?.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom display="block">
Research angles:
</Typography>
{intentAnalysis.suggested_angles.slice(0, 3).map((angle, idx) => (
<Typography key={idx} variant="body2" sx={{ ml: 1 }}>
• {angle}
</Typography>
))}
</Box>
)}
</Box>
</Collapse>
{/* Action Buttons */}
<Box display="flex" justifyContent="space-between" alignItems="center" mt={2}>
<Box>
<Button
size="small"
onClick={() => setShowDetails(!showDetails)}
endIcon={showDetails ? <ExpandLessIcon /> : <ExpandMoreIcon />}
>
{showDetails ? 'Less details' : 'More details'}
</Button>
<Button
size="small"
startIcon={<EditIcon />}
onClick={() => setIsEditing(!isEditing)}
sx={{ ml: 1 }}
>
{isEditing ? 'Done editing' : 'Edit'}
</Button>
</Box>
<Box display="flex" gap={1}>
<Button
variant="contained"
color="primary"
startIcon={isExecuting ? <CircularProgress size={16} color="inherit" /> : <PlayIcon />}
onClick={() => {
onConfirm(intent);
onExecute();
}}
disabled={isExecuting}
>
{isExecuting ? 'Researching...' : 'Start Research'}
</Button>
</Box>
</Box>
</Box>
</Paper>
);
};
export default IntentConfirmationPanel;

View File

@@ -0,0 +1,451 @@
/**
* IntentResultsDisplay Component
*
* Displays intent-driven research results organized by deliverable type.
* Shows statistics, quotes, case studies, trends, etc. in a structured format.
*/
import React, { useState } from 'react';
import {
Box,
Typography,
Tabs,
Tab,
Card,
CardContent,
Chip,
Alert,
List,
ListItem,
ListItemIcon,
ListItemText,
Grid,
Link,
Divider,
Accordion,
AccordionSummary,
AccordionDetails,
Paper,
} from '@mui/material';
import {
CheckCircle as CheckIcon,
TrendingUp as TrendIcon,
FormatQuote as QuoteIcon,
BarChart as StatsIcon,
School as CaseStudyIcon,
Lightbulb as IdeaIcon,
OpenInNew as OpenIcon,
ExpandMore as ExpandMoreIcon,
Warning as WarningIcon,
} from '@mui/icons-material';
import {
IntentDrivenResearchResponse,
DELIVERABLE_DISPLAY,
} from '../../types/intent.types';
interface IntentResultsDisplayProps {
result: IntentDrivenResearchResponse;
}
export const IntentResultsDisplay: React.FC<IntentResultsDisplayProps> = ({ result }) => {
const [tabIndex, setTabIndex] = useState(0);
// Build available tabs based on what we have
const tabs = [
{ id: 'summary', label: 'Summary', icon: <IdeaIcon />, count: 0 },
...(result.statistics.length > 0 ? [{ id: 'statistics', label: 'Statistics', icon: <StatsIcon />, count: result.statistics.length }] : []),
...(result.expert_quotes.length > 0 ? [{ id: 'quotes', label: 'Expert Quotes', icon: <QuoteIcon />, count: result.expert_quotes.length }] : []),
...(result.case_studies.length > 0 ? [{ id: 'case_studies', label: 'Case Studies', icon: <CaseStudyIcon />, count: result.case_studies.length }] : []),
...(result.trends.length > 0 ? [{ id: 'trends', label: 'Trends', icon: <TrendIcon />, count: result.trends.length }] : []),
{ id: 'sources', label: 'Sources', icon: <OpenIcon />, count: result.sources.length },
];
const currentTab = tabs[tabIndex]?.id || 'summary';
return (
<Box>
{/* Executive Summary Banner */}
{result.executive_summary && (
<Alert
severity="success"
icon={<CheckIcon />}
sx={{ mb: 3, borderRadius: 2 }}
>
<Typography variant="body1">{result.executive_summary}</Typography>
</Alert>
)}
{/* Primary Answer */}
{result.primary_answer && (
<Paper elevation={0} sx={{ p: 3, mb: 3, borderRadius: 2, bgcolor: 'primary.light', color: 'primary.contrastText' }}>
<Typography variant="subtitle2" gutterBottom>
Answer to Your Question:
</Typography>
<Typography variant="body1" fontWeight={500}>
{result.primary_answer}
</Typography>
</Paper>
)}
{/* Tabs */}
<Tabs
value={tabIndex}
onChange={(_, v) => setTabIndex(v)}
variant="scrollable"
scrollButtons="auto"
sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
>
{tabs.map((tab, idx) => (
<Tab
key={tab.id}
icon={tab.icon}
iconPosition="start"
label={
<Box display="flex" alignItems="center" gap={0.5}>
{tab.label}
{tab.count > 0 && (
<Chip size="small" label={tab.count} color="primary" sx={{ height: 20, fontSize: '0.7rem' }} />
)}
</Box>
}
sx={{ minHeight: 48, textTransform: 'none' }}
/>
))}
</Tabs>
{/* Tab Content */}
<Box sx={{ minHeight: 300 }}>
{/* Summary Tab */}
{currentTab === 'summary' && (
<Box>
{/* Key Takeaways */}
{result.key_takeaways.length > 0 && (
<Box mb={3}>
<Typography variant="h6" gutterBottom color="primary">
Key Takeaways
</Typography>
<List>
{result.key_takeaways.map((takeaway, idx) => (
<ListItem key={idx} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary={takeaway} />
</ListItem>
))}
</List>
</Box>
)}
{/* Best Practices */}
{result.best_practices.length > 0 && (
<Box mb={3}>
<Typography variant="h6" gutterBottom color="primary">
📋 Best Practices
</Typography>
<List>
{result.best_practices.map((practice, idx) => (
<ListItem key={idx} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<IdeaIcon color="info" fontSize="small" />
</ListItemIcon>
<ListItemText primary={practice} />
</ListItem>
))}
</List>
</Box>
)}
{/* Suggested Content Outline */}
{result.suggested_outline.length > 0 && (
<Box mb={3}>
<Typography variant="h6" gutterBottom color="primary">
📝 Suggested Content Outline
</Typography>
<Paper variant="outlined" sx={{ p: 2 }}>
<List dense>
{result.suggested_outline.map((item, idx) => (
<ListItem key={idx}>
<ListItemText primary={item} />
</ListItem>
))}
</List>
</Paper>
</Box>
)}
{/* Definitions */}
{Object.keys(result.definitions).length > 0 && (
<Box mb={3}>
<Typography variant="h6" gutterBottom color="primary">
📖 Key Definitions
</Typography>
<Grid container spacing={2}>
{Object.entries(result.definitions).map(([term, definition], idx) => (
<Grid item xs={12} md={6} key={idx}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" color="primary" gutterBottom>
{term}
</Typography>
<Typography variant="body2">{definition}</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
)}
</Box>
)}
{/* Statistics Tab */}
{currentTab === 'statistics' && (
<Grid container spacing={2}>
{result.statistics.map((stat, idx) => (
<Grid item xs={12} md={6} key={idx}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent>
<Box display="flex" alignItems="flex-start" gap={1}>
<StatsIcon color="primary" />
<Box flex={1}>
<Typography variant="body1" fontWeight={500}>
{stat.statistic}
</Typography>
{stat.value && (
<Chip label={stat.value} color="primary" size="small" sx={{ mt: 0.5 }} />
)}
<Typography variant="caption" color="text.secondary" display="block" mt={1}>
{stat.context}
</Typography>
<Box display="flex" alignItems="center" gap={1} mt={1}>
<Link href={stat.url} target="_blank" rel="noopener" variant="caption">
{stat.source} <OpenIcon sx={{ fontSize: 12 }} />
</Link>
<Chip
size="small"
label={`${Math.round(stat.credibility * 100)}% credible`}
color={stat.credibility > 0.8 ? 'success' : 'warning'}
variant="outlined"
/>
</Box>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Expert Quotes Tab */}
{currentTab === 'quotes' && (
<Box>
{result.expert_quotes.map((quote, idx) => (
<Card key={idx} variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Box display="flex" gap={2}>
<QuoteIcon color="primary" sx={{ fontSize: 40, opacity: 0.5 }} />
<Box>
<Typography variant="body1" fontStyle="italic" mb={1}>
"{quote.quote}"
</Typography>
<Typography variant="subtitle2" color="primary">
{quote.speaker}
{quote.title && `, ${quote.title}`}
{quote.organization && ` at ${quote.organization}`}
</Typography>
<Link href={quote.url} target="_blank" rel="noopener" variant="caption">
Source: {quote.source} <OpenIcon sx={{ fontSize: 12 }} />
</Link>
</Box>
</Box>
</CardContent>
</Card>
))}
</Box>
)}
{/* Case Studies Tab */}
{currentTab === 'case_studies' && (
<Box>
{result.case_studies.map((cs, idx) => (
<Accordion key={idx} defaultExpanded={idx === 0}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box>
<Typography variant="subtitle1" fontWeight={600}>
{cs.title}
</Typography>
<Typography variant="caption" color="primary">
{cs.organization}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">Challenge</Typography>
<Typography variant="body2">{cs.challenge}</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">Solution</Typography>
<Typography variant="body2">{cs.solution}</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Typography variant="caption" color="text.secondary">Outcome</Typography>
<Typography variant="body2">{cs.outcome}</Typography>
</Grid>
</Grid>
{cs.key_metrics.length > 0 && (
<Box mt={2} display="flex" gap={1} flexWrap="wrap">
{cs.key_metrics.map((metric, i) => (
<Chip key={i} label={metric} size="small" color="success" variant="outlined" />
))}
</Box>
)}
<Box mt={2}>
<Link href={cs.url} target="_blank" rel="noopener" variant="caption">
Read full case study <OpenIcon sx={{ fontSize: 12 }} />
</Link>
</Box>
</AccordionDetails>
</Accordion>
))}
</Box>
)}
{/* Trends Tab */}
{currentTab === 'trends' && (
<Grid container spacing={2}>
{result.trends.map((trend, idx) => (
<Grid item xs={12} md={6} key={idx}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<TrendIcon
color={trend.direction === 'growing' ? 'success' : trend.direction === 'declining' ? 'error' : 'info'}
/>
<Typography variant="subtitle1" fontWeight={500}>
{trend.trend}
</Typography>
<Chip
size="small"
label={trend.direction}
color={trend.direction === 'growing' ? 'success' : trend.direction === 'declining' ? 'error' : 'info'}
/>
</Box>
{trend.impact && (
<Typography variant="body2" color="text.secondary" mb={1}>
Impact: {trend.impact}
</Typography>
)}
{trend.timeline && (
<Typography variant="caption" color="text.secondary">
Timeline: {trend.timeline}
</Typography>
)}
<Box mt={1}>
<Typography variant="caption" color="text.secondary">Evidence:</Typography>
<List dense>
{trend.evidence.slice(0, 3).map((e, i) => (
<ListItem key={i} sx={{ py: 0, pl: 1 }}>
<ListItemText primary={`${e}`} primaryTypographyProps={{ variant: 'caption' }} />
</ListItem>
))}
</List>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
{/* Sources Tab */}
{currentTab === 'sources' && (
<List>
{result.sources.map((source, idx) => (
<ListItem
key={idx}
component="a"
href={source.url}
target="_blank"
rel="noopener"
sx={{
borderBottom: '1px solid',
borderColor: 'divider',
'&:hover': { bgcolor: 'action.hover' }
}}
>
<ListItemText
primary={source.title}
secondary={
<Box>
{source.excerpt && (
<Typography variant="caption" display="block" color="text.secondary">
{source.excerpt}
</Typography>
)}
<Box display="flex" gap={1} mt={0.5}>
{source.content_type && (
<Chip size="small" label={source.content_type} variant="outlined" />
)}
<Chip
size="small"
label={`${Math.round(source.relevance_score * 100)}% relevant`}
color="primary"
variant="outlined"
/>
<Chip
size="small"
label={`${Math.round(source.credibility_score * 100)}% credible`}
color={source.credibility_score > 0.8 ? 'success' : 'warning'}
variant="outlined"
/>
</Box>
</Box>
}
/>
<OpenIcon color="action" />
</ListItem>
))}
</List>
)}
</Box>
{/* Gaps Identified */}
{result.gaps_identified.length > 0 && (
<Alert severity="warning" icon={<WarningIcon />} sx={{ mt: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Gaps Identified:
</Typography>
<List dense>
{result.gaps_identified.map((gap, idx) => (
<ListItem key={idx} sx={{ py: 0 }}>
<ListItemText primary={`${gap}`} />
</ListItem>
))}
</List>
{result.follow_up_queries.length > 0 && (
<Box mt={1}>
<Typography variant="caption" color="text.secondary">
Suggested follow-up: {result.follow_up_queries.slice(0, 2).join(', ')}
</Typography>
</Box>
)}
</Alert>
)}
{/* Confidence */}
<Box mt={2} display="flex" justifyContent="flex-end">
<Chip
label={`Research confidence: ${Math.round(result.confidence * 100)}%`}
color={result.confidence > 0.8 ? 'success' : result.confidence > 0.6 ? 'warning' : 'error'}
variant="outlined"
/>
</Box>
</Box>
);
};
export default IntentResultsDisplay;

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { Tooltip } from '@mui/material';
import { InfoOutlined, AutoAwesome } from '@mui/icons-material';
interface PersonalizationIndicatorProps {
type: 'placeholder' | 'keywords' | 'presets' | 'angles' | 'provider' | 'mode';
hasPersona: boolean;
source?: string; // e.g., "from your website content", "from your writing style"
}
const PERSONALIZATION_TOOLTIPS = {
placeholder: {
title: 'Personalized Placeholders',
description: 'These placeholders are customized based on your research persona, including research angles and recommended presets from your website analysis.',
source: 'from your research persona'
},
keywords: {
title: 'Personalized Keywords',
description: 'Keywords are extracted from your actual website content and matched to your industry and audience preferences.',
source: 'from your website content'
},
presets: {
title: 'Personalized Presets',
description: 'Research presets are generated based on your content types, writing patterns, and website topics for maximum relevance.',
source: 'from your content strategy'
},
angles: {
title: 'Personalized Research Angles',
description: 'Research angles are derived from your writing patterns and style guidelines to match your content approach.',
source: 'from your writing patterns'
},
provider: {
title: 'Smart Provider Selection',
description: 'Research provider is automatically selected based on your writing style complexity and content type preferences.',
source: 'from your writing style'
},
mode: {
title: 'Optimized Research Depth',
description: 'Research depth is matched to your writing complexity level - high complexity gets comprehensive research, simple gets basic.',
source: 'from your writing complexity'
}
};
export const PersonalizationIndicator: React.FC<PersonalizationIndicatorProps> = ({
type,
hasPersona,
source
}) => {
if (!hasPersona) {
return null; // Don't show indicator if no persona
}
const tooltip = PERSONALIZATION_TOOLTIPS[type];
const displaySource = source || tooltip.source;
return (
<Tooltip
title={
<div style={{ padding: '4px 0' }}>
<div style={{ fontWeight: 600, marginBottom: '4px', fontSize: '13px' }}>
{tooltip.title}
</div>
<div style={{ fontSize: '12px', lineHeight: '1.5', marginBottom: '4px' }}>
{tooltip.description}
</div>
<div style={{ fontSize: '11px', color: 'rgba(255, 255, 255, 0.7)', fontStyle: 'italic' }}>
Personalized {displaySource}
</div>
</div>
}
arrow
placement="top"
>
<span
style={{
display: 'inline-flex',
alignItems: 'center',
cursor: 'help',
marginLeft: '6px',
color: '#0ea5e9',
}}
>
<AutoAwesome sx={{ fontSize: 14, color: '#0ea5e9' }} />
</span>
</Tooltip>
);
};
interface PersonalizationBadgeProps {
label: string;
source: string;
compact?: boolean;
}
export const PersonalizationBadge: React.FC<PersonalizationBadgeProps> = ({
label,
source,
compact = false
}) => {
return (
<Tooltip
title={`Personalized ${source} - This is customized based on your research persona and website analysis`}
arrow
placement="top"
>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
padding: compact ? '2px 6px' : '4px 8px',
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)',
border: '1px solid rgba(14, 165, 233, 0.2)',
borderRadius: '6px',
fontSize: compact ? '10px' : '11px',
color: '#0369a1',
fontWeight: 500,
cursor: 'help',
}}
>
<AutoAwesome sx={{ fontSize: compact ? 12 : 14, color: '#0ea5e9' }} />
<span>{label}</span>
</div>
</Tooltip>
);
};

View File

@@ -11,39 +11,23 @@ export const ProviderChips: React.FC<ProviderChipsProps> = ({ providerAvailabili
if (!providerAvailability) return null;
// Provider priority: Exa → Tavily → Google for all modes
// Status indicators show availability (green=configured, red=not configured)
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)
tooltip: 'Exa Neural Search (Primary). Advanced semantic search engine that understands context and meaning. Used by default when available.',
color: 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)
borderColor: 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',
textColor: providerAvailability.exa_available ? '#10b981' : '#ef4444',
},
{
id: 'tavily',
@@ -51,17 +35,29 @@ export const ProviderChips: React.FC<ProviderChipsProps> = ({ providerAvailabili
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%)'
tooltip: 'Tavily AI Research (Secondary). Specialized AI-powered research tool with real-time data and news. Used when Exa is unavailable.',
color: providerAvailability.tavily_available
? 'linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 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)'
borderColor: providerAvailability.tavily_available
? 'rgba(59, 130, 246, 0.3)'
: 'rgba(239, 68, 68, 0.2)',
textColor: (advanced && providerAvailability.tavily_available) ? '#10b981' : '#ef4444',
chipStatus: (advanced && providerAvailability.tavily_available) ? '#10b981' : '#ef4444',
textColor: providerAvailability.tavily_available ? '#3b82f6' : '#ef4444',
},
{
id: 'google',
name: 'Google',
available: providerAvailability.google_available,
status: providerAvailability.gemini_key_status,
icon: '🔍',
tooltip: 'Google Search (Fallback). Gemini-powered web search. Used when Exa and Tavily are unavailable.',
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',
},
];
@@ -111,8 +107,8 @@ export const ProviderChips: React.FC<ProviderChipsProps> = ({ providerAvailabili
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)
background: provider.available ? '#10b981' : '#ef4444',
boxShadow: provider.available
? '0 0 4px rgba(16, 185, 129, 0.4)'
: '0 0 4px rgba(239, 68, 68, 0.4)',
}} />

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { formatAngle } from '../../../../utils/researchAngles';
import { PersonalizationIndicator } from './PersonalizationIndicator';
interface ResearchAnglesProps {
angles: string[];
onUseAngle: (angle: string) => void;
hasPersona?: boolean;
}
export const ResearchAngles: React.FC<ResearchAnglesProps> = ({ angles, onUseAngle }) => {
export const ResearchAngles: React.FC<ResearchAnglesProps> = ({ angles, onUseAngle, hasPersona = false }) => {
if (angles.length === 0) return null;
return (
@@ -33,6 +35,13 @@ export const ResearchAngles: React.FC<ResearchAnglesProps> = ({ angles, onUseAng
}}>
Explore Alternative Research Angles
</span>
{hasPersona && (
<PersonalizationIndicator
type="angles"
hasPersona={hasPersona}
source="from your writing patterns"
/>
)}
</div>
<div style={{
display: 'grid',

View File

@@ -1,17 +1,20 @@
import React from 'react';
import { ProviderAvailability } from '../../../../api/researchConfig';
import { industries } from '../utils/constants';
import { PersonalizationIndicator } from './PersonalizationIndicator';
interface ResearchControlsBarProps {
industry: string;
providerAvailability: ProviderAvailability | null;
onIndustryChange: (industry: string) => void;
hasPersona?: boolean;
}
export const ResearchControlsBar: React.FC<ResearchControlsBarProps> = ({
industry,
providerAvailability,
onIndustryChange,
hasPersona = false,
}) => {
const dropdownStyle = {
minWidth: '130px',
@@ -83,21 +86,29 @@ export const ResearchControlsBar: React.FC<ResearchControlsBarProps> = ({
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 style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<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>
{hasPersona && industry !== 'General' && (
<PersonalizationIndicator
type="keywords"
hasPersona={hasPersona}
source="from your research persona"
/>
)}
</div>
</div>
</div>
);

View File

@@ -1,21 +1,31 @@
import React from 'react';
import { PersonalizationIndicator } from './PersonalizationIndicator';
interface TargetAudienceProps {
value: string;
onChange: (value: string) => void;
hasPersona?: boolean;
}
export const TargetAudience: React.FC<TargetAudienceProps> = ({ value, onChange }) => {
export const TargetAudience: React.FC<TargetAudienceProps> = ({ value, onChange, hasPersona = false }) => {
return (
<div>
<label style={{
display: 'block',
display: 'flex',
alignItems: 'center',
marginBottom: '8px',
fontSize: '13px',
fontWeight: '600',
color: '#0c4a6e',
}}>
Target Audience (Optional)
{hasPersona && (
<PersonalizationIndicator
type="keywords"
hasPersona={hasPersona}
source="from your research persona"
/>
)}
</label>
<input
type="text"

View File

@@ -1,58 +1,139 @@
/**
* Industry-specific placeholder examples for personalized experience
* Enhanced to use research persona data (research_angles and recommended_presets)
*/
export const getIndustryPlaceholders = (industry: string): string[] => {
export interface PersonaPlaceholderData {
research_angles?: string[];
recommended_presets?: Array<{
name: string;
keywords: string | string[];
description?: string;
}>;
industry?: string;
target_audience?: string;
}
export const getIndustryPlaceholders = (
industry: string,
personaData?: PersonaPlaceholderData
): string[] => {
// If we have research persona data, use it to generate personalized placeholders
if (personaData) {
const personalizedPlaceholders: string[] = [];
// Priority 1: Use recommended presets (most actionable)
if (personaData.recommended_presets && personaData.recommended_presets.length > 0) {
const presets = personaData.recommended_presets.slice(0, 4); // Use first 4 presets
presets.forEach((preset) => {
const keywords = typeof preset.keywords === 'string'
? preset.keywords
: Array.isArray(preset.keywords)
? preset.keywords.join(', ')
: '';
if (keywords && keywords.trim().length > 0) {
// Make placeholders concise and actionable
personalizedPlaceholders.push(keywords.trim());
}
});
}
// Priority 2: Use research angles (formatted as actionable queries)
if (personaData.research_angles && personaData.research_angles.length > 0 && personalizedPlaceholders.length < 4) {
const angles = personaData.research_angles.slice(0, 4 - personalizedPlaceholders.length);
angles.forEach((angle) => {
// Format angle as a concise research query
let placeholder = angle;
// Replace topic placeholders with industry if available
if (placeholder.includes('{topic}') || placeholder.includes('{{topic}}')) {
placeholder = placeholder.replace(/\{topic\}/g, industry || 'your topic')
.replace(/\{\{topic\}\}/g, industry || 'your topic');
}
// Make it concise - remove "Research:" prefix if present, keep it natural
placeholder = placeholder.replace(/^Research:\s*/i, '').trim();
if (placeholder && placeholder.length > 10) { // Only add meaningful angles
personalizedPlaceholders.push(placeholder);
}
});
}
// If we have personalized placeholders, return them (with fallback to industry defaults)
if (personalizedPlaceholders.length > 0) {
// Add 1-2 industry-specific ones as backup for variety
const industryDefaults = getIndustryDefaults(industry);
const needed = Math.max(0, 5 - personalizedPlaceholders.length);
return [...personalizedPlaceholders, ...industryDefaults.slice(0, needed)];
}
}
// Fallback to industry-specific defaults
return getIndustryDefaults(industry);
};
/**
* Get industry-specific default placeholders (original logic)
*/
const getIndustryDefaults = (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"
"AI diagnostic tools and clinical applications",
"Telemedicine adoption and patient outcomes",
"Personalized medicine and genomic testing",
"Healthcare automation and workflow optimization"
],
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"
"Edge computing and IoT deployment strategies",
"Cloud provider comparison and cost optimization",
"Quantum computing breakthroughs and applications",
"AI and machine learning industry trends"
],
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"
"DeFi regulations and compliance strategies",
"Digital banking and customer retention",
"ESG investing trends and performance",
"Fintech innovations and market analysis"
],
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"
"AI marketing automation and personalization",
"Influencer marketing ROI and best practices",
"Privacy-first marketing in cookieless world",
"Content marketing strategies and trends"
],
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"
"Remote work policies and hybrid models",
"Supply chain resilience and diversification",
"Sustainability initiatives and ESG programs",
"Business automation and efficiency"
],
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"
"EdTech tools and personalized learning",
"Microlearning and skill-based education",
"AI tutoring systems and student support",
"Online learning platforms and outcomes"
],
'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"
"PropTech innovations and property management",
"Virtual staging and 3D property tours",
"Real estate tokenization and fractional ownership",
"Smart building technologies and IoT"
],
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"
"Sustainable tourism and eco-travel trends",
"AI travel personalization and recommendations",
"Bleisure travel and workation destinations",
"Travel technology and booking platforms"
]
};
// Default placeholders - concise and actionable
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"
"Latest AI trends and innovations",
"Best practices and case studies",
"Market analysis and competitor insights",
"Emerging technologies and future predictions"
];
};

View File

@@ -0,0 +1,328 @@
/**
* Intent-Driven Research Types
*
* Types for the new intent-driven research system that:
* - Infers user intent from minimal input
* - Generates targeted queries
* - Analyzes results based on what user needs
*/
// ============================================================================
// Enums
// ============================================================================
export type ResearchPurpose =
| 'learn'
| 'create_content'
| 'make_decision'
| 'compare'
| 'solve_problem'
| 'find_data'
| 'explore_trends'
| 'validate'
| 'generate_ideas';
export type ContentOutput =
| 'blog'
| 'podcast'
| 'video'
| 'social_post'
| 'newsletter'
| 'presentation'
| 'report'
| 'whitepaper'
| 'email'
| 'general';
export type ExpectedDeliverable =
| 'key_statistics'
| 'expert_quotes'
| 'case_studies'
| 'comparisons'
| 'trends'
| 'best_practices'
| 'step_by_step'
| 'pros_cons'
| 'definitions'
| 'citations'
| 'examples'
| 'predictions';
export type ResearchDepthLevel = 'overview' | 'detailed' | 'expert';
export type InputType = 'keywords' | 'question' | 'goal' | 'mixed';
// ============================================================================
// Core Intent Types
// ============================================================================
export interface ResearchIntent {
primary_question: string;
secondary_questions: string[];
purpose: ResearchPurpose;
content_output: ContentOutput;
expected_deliverables: ExpectedDeliverable[];
depth: ResearchDepthLevel;
focus_areas: string[];
perspective: string | null;
time_sensitivity: string | null;
input_type: InputType;
original_input: string;
confidence: number;
needs_clarification: boolean;
clarifying_questions: string[];
}
export interface ResearchQuery {
query: string;
purpose: ExpectedDeliverable;
provider: 'exa' | 'tavily' | 'google';
priority: number;
expected_results: string;
}
// ============================================================================
// Deliverable Types
// ============================================================================
export interface StatisticWithCitation {
statistic: string;
value: string | null;
context: string;
source: string;
url: string;
credibility: number;
recency: string | null;
}
export interface ExpertQuote {
quote: string;
speaker: string;
title: string | null;
organization: string | null;
context: string | null;
source: string;
url: string;
}
export interface CaseStudySummary {
title: string;
organization: string;
challenge: string;
solution: string;
outcome: string;
key_metrics: string[];
source: string;
url: string;
}
export interface TrendAnalysis {
trend: string;
direction: 'growing' | 'declining' | 'emerging' | 'stable';
evidence: string[];
impact: string | null;
timeline: string | null;
sources: string[];
}
export interface ComparisonItem {
name: string;
description: string | null;
pros: string[];
cons: string[];
features: Record<string, string>;
rating: number | null;
source: string | null;
}
export interface ComparisonTable {
title: string;
criteria: string[];
items: ComparisonItem[];
winner: string | null;
verdict: string | null;
}
export interface ProsCons {
subject: string;
pros: string[];
cons: string[];
balanced_verdict: string;
}
export interface SourceWithRelevance {
title: string;
url: string;
excerpt: string | null;
relevance_score: number;
relevance_reason: string | null;
content_type: string | null;
published_date: string | null;
credibility_score: number;
}
// ============================================================================
// API Request/Response Types
// ============================================================================
export interface AnalyzeIntentRequest {
user_input: string;
keywords: string[];
use_persona: boolean;
use_competitor_data: boolean;
}
export interface AnalyzeIntentResponse {
success: boolean;
intent: ResearchIntent;
analysis_summary: string;
suggested_queries: ResearchQuery[];
suggested_keywords: string[];
suggested_angles: string[];
quick_options: QuickOption[];
error_message: string | null;
}
export interface QuickOption {
id: string;
label: string;
value: string | string[];
display: string | string[];
alternatives: string[];
confidence: number;
multi_select?: boolean;
}
export interface IntentDrivenResearchRequest {
user_input: string;
confirmed_intent?: ResearchIntent;
selected_queries?: ResearchQuery[];
max_sources: number;
include_domains: string[];
exclude_domains: string[];
skip_inference: boolean;
}
export interface IntentDrivenResearchResponse {
success: boolean;
// Direct answers
primary_answer: string;
secondary_answers: Record<string, string>;
// Deliverables
statistics: StatisticWithCitation[];
expert_quotes: ExpertQuote[];
case_studies: CaseStudySummary[];
trends: TrendAnalysis[];
comparisons: ComparisonTable[];
best_practices: string[];
step_by_step: string[];
pros_cons: ProsCons | null;
definitions: Record<string, string>;
examples: string[];
predictions: string[];
// Content-ready outputs
executive_summary: string;
key_takeaways: string[];
suggested_outline: string[];
// Sources and metadata
sources: SourceWithRelevance[];
confidence: number;
gaps_identified: string[];
follow_up_queries: string[];
// The intent used
intent: ResearchIntent | null;
// Error
error_message: string | null;
}
// ============================================================================
// UI State Types
// ============================================================================
export interface IntentWizardState {
// User input
userInput: string;
keywords: string[];
// Inferred/confirmed intent
intent: ResearchIntent | null;
// Suggested queries
suggestedQueries: ResearchQuery[];
selectedQueries: ResearchQuery[];
// Quick options for confirmation
quickOptions: QuickOption[];
// Analysis
analysisSummary: string;
suggestedKeywords: string[];
suggestedAngles: string[];
// State
isAnalyzing: boolean;
isResearching: boolean;
hasConfirmedIntent: boolean;
// Results
result: IntentDrivenResearchResponse | null;
// Errors
error: string | null;
}
// ============================================================================
// Display Helpers
// ============================================================================
export const PURPOSE_DISPLAY: Record<ResearchPurpose, string> = {
learn: 'Understand this topic',
create_content: 'Create content about this',
make_decision: 'Make a decision',
compare: 'Compare options',
solve_problem: 'Solve a problem',
find_data: 'Find specific data',
explore_trends: 'Explore trends',
validate: 'Validate information',
generate_ideas: 'Generate ideas',
};
export const CONTENT_OUTPUT_DISPLAY: Record<ContentOutput, string> = {
blog: 'Blog Post',
podcast: 'Podcast',
video: 'Video',
social_post: 'Social Post',
newsletter: 'Newsletter',
presentation: 'Presentation',
report: 'Report',
whitepaper: 'Whitepaper',
email: 'Email',
general: 'General Research',
};
export const DELIVERABLE_DISPLAY: Record<ExpectedDeliverable, string> = {
key_statistics: 'Key Statistics',
expert_quotes: 'Expert Quotes',
case_studies: 'Case Studies',
comparisons: 'Comparisons',
trends: 'Trends',
best_practices: 'Best Practices',
step_by_step: 'Step-by-Step Guide',
pros_cons: 'Pros & Cons',
definitions: 'Definitions',
citations: 'Citations',
examples: 'Examples',
predictions: 'Predictions',
};
export const DEPTH_DISPLAY: Record<ResearchDepthLevel, string> = {
overview: 'Quick Overview',
detailed: 'Detailed Analysis',
expert: 'Expert-Level Deep Dive',
};

View File

@@ -1,4 +1,9 @@
import { BlogResearchResponse, ResearchMode, ResearchConfig } from '../../../services/blogWriterApi';
import {
ResearchIntent,
AnalyzeIntentResponse,
IntentDrivenResearchResponse
} from './intent.types';
export interface WizardState {
currentStep: number;
@@ -11,6 +16,7 @@ export interface WizardState {
}
export interface ResearchExecution {
// Legacy API
executeResearch: (state: WizardState) => Promise<string | null>;
stopExecution: () => void;
isExecuting: boolean;
@@ -18,6 +24,19 @@ export interface ResearchExecution {
progressMessages: Array<{ timestamp: string; message: string }>;
currentStatus: string;
result: any;
// Intent-driven API
useIntentMode: boolean;
setUseIntentMode: (enabled: boolean) => void;
isAnalyzingIntent: boolean;
intentAnalysis: AnalyzeIntentResponse | null;
confirmedIntent: ResearchIntent | null;
intentResult: IntentDrivenResearchResponse | null;
analyzeIntent: (state: WizardState) => Promise<AnalyzeIntentResponse | null>;
confirmIntent: (intent: ResearchIntent) => void;
updateIntentField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
executeIntentResearch: (state: WizardState) => Promise<IntentDrivenResearchResponse | null>;
clearIntent: () => void;
}
export interface WizardStepProps {