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:
738
frontend/src/components/Research/IntentResearchWizard.tsx
Normal file
738
frontend/src/components/Research/IntentResearchWizard.tsx
Normal 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: • "AI trends in healthcare 2025" • "What are the best project management tools?" • "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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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()}
|
||||
|
||||
323
frontend/src/components/Research/hooks/useIntentResearch.ts
Normal file
323
frontend/src/components/Research/hooks/useIntentResearch.ts
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)',
|
||||
}} />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
328
frontend/src/components/Research/types/intent.types.ts
Normal file
328
frontend/src/components/Research/types/intent.types.ts
Normal 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',
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user