AI Researcher and Video Studio implementation complete

This commit is contained in:
ajaysi
2026-01-05 15:49:51 +05:30
parent b134e9dc7e
commit 0b63ae7fc1
200 changed files with 39535 additions and 1375 deletions

View File

@@ -9,6 +9,7 @@ import { addResearchHistory } from '../../utils/researchHistory';
import { getResearchConfig, ProviderAvailability } from '../../api/researchConfig';
import { ProviderChips } from './steps/components/ProviderChips';
import { AdvancedChip } from './steps/components/AdvancedChip';
import { SmartResearchInfo } from './steps/components/SmartResearchInfo';
export const ResearchWizard: React.FC<ResearchWizardProps> = ({
onComplete,
@@ -336,82 +337,105 @@ 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);
{/* Research Button (Unified - enabled only after intent analysis on Step 1) */}
<button
onClick={() => {
if (wizard.state.currentStep === 1) {
// On Step 1: If intent is analyzed with high confidence, execute directly
if (execution.intentAnalysis?.success &&
execution.intentAnalysis.intent.confidence >= 0.7) {
const queriesToUse = execution.intentAnalysis.suggested_queries?.slice(0, 5) || [];
execution.executeIntentResearch(wizard.state, queriesToUse).then(result => {
if (result?.success) {
wizard.updateState({ currentStep: 3 }); // Skip to results
}
}
});
} else {
// No intent or low confidence - go to progress step for traditional research
wizard.nextStep();
}
}}
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()}
} else {
wizard.nextStep();
}
}}
disabled={
wizard.state.currentStep === 1
? !wizard.canGoNext() || !execution.intentAnalysis || execution.isExecuting
: !wizard.canGoNext()
}
style={{
padding: '10px 24px',
background: wizard.canGoNext()
? 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)'
: 'rgba(100, 116, 139, 0.2)',
color: wizard.canGoNext() ? 'white' : '#94a3b8',
border: wizard.canGoNext() ? 'none' : '1px solid rgba(100, 116, 139, 0.2)',
background: (() => {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
return canProceed
? 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)'
: 'rgba(100, 116, 139, 0.2)';
})(),
color: (() => {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
return canProceed ? 'white' : '#94a3b8';
})(),
border: 'none',
borderRadius: '10px',
cursor: wizard.canGoNext() ? 'pointer' : 'not-allowed',
cursor: (() => {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
return canProceed ? 'pointer' : 'not-allowed';
})(),
fontSize: '13px',
fontWeight: '600',
transition: 'all 0.2s ease',
boxShadow: wizard.canGoNext() ? '0 2px 8px rgba(14, 165, 233, 0.3)' : 'none',
boxShadow: (() => {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
return canProceed ? '0 2px 8px rgba(14, 165, 233, 0.3)' : 'none';
})(),
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
onMouseEnter={(e) => {
if (wizard.canGoNext()) {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
if (canProceed) {
e.currentTarget.style.transform = 'translateX(4px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(14, 165, 233, 0.4)';
}
}}
onMouseLeave={(e) => {
if (wizard.canGoNext()) {
const canProceed = wizard.state.currentStep === 1
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
: wizard.canGoNext();
if (canProceed) {
e.currentTarget.style.transform = 'translateX(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
}
}}
title={
wizard.state.currentStep === 1 && !execution.intentAnalysis
? 'Click "Intent & Options" in the text area to analyze your research first'
: wizard.isLastStep ? 'Complete research' : 'Start research'
}
>
{wizard.isLastStep ? 'Finish' : 'Continue →'}
{execution.isExecuting ? (
<>
<span style={{ animation: 'pulse 1.5s ease-in-out infinite' }}>🔍</span>
Researching...
</>
) : wizard.isLastStep ? (
'Finish'
) : (
<>
🚀 Research
</>
)}
</button>
</div>
)}

View File

@@ -4,10 +4,11 @@ import { WizardState } from '../types/research.types';
import { researchEngineApi, ResearchEngineRequest } from '../../../services/researchEngineApi';
import { useResearchPolling } from '../../../hooks/usePolling';
import { intentResearchApi } from '../../../api/intentResearchApi';
import {
import {
ResearchIntent,
IntentDrivenResearchResponse,
AnalyzeIntentResponse
AnalyzeIntentResponse,
ResearchQuery,
} from '../types/intent.types';
export const useResearchExecution = () => {
@@ -133,6 +134,12 @@ export const useResearchExecution = () => {
try {
const userInput = state.keywords.join(' ');
if (!userInput.trim()) {
setError('Please enter keywords or a research topic');
setIsAnalyzingIntent(false);
return null;
}
const response = await intentResearchApi.analyzeIntent({
user_input: userInput,
keywords: state.keywords,
@@ -140,20 +147,73 @@ export const useResearchExecution = () => {
use_competitor_data: true,
});
if (!response.success) {
const errorMsg = response.error_message || 'Failed to analyze intent';
setError(errorMsg);
setIsAnalyzingIntent(false);
return response; // Return response even if failed so UI can show error
}
setIntentAnalysis(response);
// Auto-confirm if confidence is high and no clarification needed
if (response.success && response.intent.confidence >= 0.85 && !response.intent.needs_clarification) {
if (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';
} catch (err: any) {
console.error('[useResearchExecution] analyzeIntent error:', err);
let errorMessage = 'Failed to analyze intent';
if (err.response) {
// HTTP error response
if (err.response.status === 404) {
errorMessage = 'Smart Research endpoint not found. The feature may not be available yet. Please use the regular research flow.';
} else if (err.response.status === 401) {
errorMessage = 'Authentication required. Please log in again.';
} else if (err.response.status >= 500) {
errorMessage = 'Server error. Please try again later.';
} else {
errorMessage = err.response.data?.detail || err.response.data?.error_message || `Server error: ${err.response.status}`;
}
} else if (err.request) {
// Network error
errorMessage = 'Network error. Please check your connection and try again.';
} else {
errorMessage = err.message || 'Unknown error occurred';
}
setError(errorMessage);
setIsAnalyzingIntent(false);
return null;
// Return a failed response so UI can show the error
return {
success: false,
intent: {
primary_question: state.keywords.join(' '),
secondary_questions: [],
purpose: 'learn',
content_output: 'general',
expected_deliverables: ['key_statistics'],
depth: 'detailed',
focus_areas: [],
perspective: null,
time_sensitivity: null,
input_type: 'keywords',
original_input: state.keywords.join(' '),
confidence: 0,
needs_clarification: true,
clarifying_questions: [],
},
analysis_summary: '',
suggested_queries: [],
suggested_keywords: [],
suggested_angles: [],
quick_options: [],
error_message: errorMessage,
};
}
}, []);
@@ -183,7 +243,10 @@ export const useResearchExecution = () => {
/**
* Execute research using intent-driven approach.
*/
const executeIntentResearch = useCallback(async (state: WizardState): Promise<IntentDrivenResearchResponse | null> => {
const executeIntentResearch = useCallback(async (
state: WizardState,
selectedQueries?: ResearchQuery[]
): Promise<IntentDrivenResearchResponse | null> => {
// First analyze intent if not already done
let intent = confirmedIntent;
if (!intent) {
@@ -198,13 +261,23 @@ export const useResearchExecution = () => {
setError(null);
try {
// Use provided queries or fall back to intent analysis queries
const queriesToUse = selectedQueries || intentAnalysis?.suggested_queries?.slice(0, 5) || [];
const response = await intentResearchApi.executeIntentResearch({
user_input: state.keywords.join(' '),
confirmed_intent: intent,
selected_queries: intentAnalysis?.suggested_queries?.slice(0, 5),
selected_queries: queriesToUse.map(q => ({
query: q.query,
purpose: q.purpose,
provider: q.provider,
priority: q.priority,
expected_results: q.expected_results,
})),
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 || [],
trends_config: intentAnalysis?.trends_config, // Include Google Trends configuration
skip_inference: true,
});

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { WizardStepProps } from '../types/research.types';
import { ResearchProvider, ResearchMode } from '../../../services/blogWriterApi';
import { getResearchConfig, ProviderAvailability } from '../../../api/researchConfig';
@@ -6,13 +6,6 @@ import {
getResearchHistory,
ResearchHistoryEntry
} from '../../../utils/researchHistory';
import {
expandKeywords,
expandKeywordsWithPersona
} from '../../../utils/keywordExpansion';
import {
generateResearchAngles
} from '../../../utils/researchAngles';
// Utilities
import { parseIntelligentInput } from './utils/inputParser';
@@ -27,12 +20,15 @@ import { SmartInputIndicator } from './components/SmartInputIndicator';
import { KeywordExpansion } from './components/KeywordExpansion';
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 { ResearchInputHeader } from './components/ResearchInputHeader';
import { AdvancedOptionsSection } from './components/AdvancedOptionsSection';
import { IntentConfirmationPanel } from './components/IntentConfirmationPanel';
import { ResearchExecution } from '../types/research.types';
// Hooks
import { useKeywordExpansion } from './hooks/useKeywordExpansion';
import { useResearchAngles } from './hooks/useResearchAngles';
interface ResearchInputProps extends WizardStepProps {
advanced?: boolean;
onAdvancedChange?: (advanced: boolean) => void;
@@ -45,12 +41,6 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
const [loadingConfig, setLoadingConfig] = useState(true);
const [suggestedMode, setSuggestedMode] = useState<ResearchMode | null>(null);
const [researchHistory, setResearchHistory] = useState<ResearchHistoryEntry[]>([]);
const [keywordExpansion, setKeywordExpansion] = useState<{
original: string[];
expanded: string[];
suggestions: string[];
} | null>(null);
const [researchAngles, setResearchAngles] = useState<string[]>([]);
const [researchPersona, setResearchPersona] = useState<{
research_angles?: string[];
recommended_presets?: Array<{
@@ -355,71 +345,11 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
}
}, [state.industry, providerAvailability]);
// Expand keywords when keywords or industry changes
// Enhanced to use research persona data if available
useEffect(() => {
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, researchPersona]);
// Use keyword expansion hook
const keywordExpansion = useKeywordExpansion(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) {
const query = state.keywords.join(' ');
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, researchPersona]);
// Use research angles hook
const researchAngles = useResearchAngles(state.keywords, state.industry, researchPersona);
// Event handlers
const handleKeywordsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -511,130 +441,13 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
e.currentTarget.style.boxShadow = 'none';
}}
>
<div style={{
marginBottom: '12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
}}>
<label style={{
fontSize: '15px',
fontWeight: '600',
color: '#0c4a6e',
display: 'flex',
alignItems: 'center',
gap: '8px',
flex: '1',
}}>
<span style={{
fontSize: '20px',
}}>🔍</span>
Research Topic & Keywords
<PersonalizationIndicator
type="placeholder"
hasPersona={!!researchPersona}
source={researchPersona ? "from your research persona" : undefined}
/>
</label>
{/* Advanced Toggle and Upload Button */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
{/* Advanced Toggle */}
<label
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer',
padding: '6px 10px',
borderRadius: '8px',
border: `1px solid ${advanced ? 'rgba(14, 165, 233, 0.3)' : 'rgba(15, 23, 42, 0.1)'}`,
background: advanced
? 'linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)'
: '#ffffff',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
fontSize: '11px',
fontWeight: '600',
color: advanced ? '#0369a1' : '#475569',
boxShadow: advanced ? '0 1px 3px rgba(14, 165, 233, 0.12)' : '0 1px 2px rgba(0, 0, 0, 0.04)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = advanced ? 'rgba(14, 165, 233, 0.4)' : 'rgba(15, 23, 42, 0.15)';
e.currentTarget.style.boxShadow = advanced
? '0 2px 4px rgba(14, 165, 233, 0.18)'
: '0 1px 3px rgba(0, 0, 0, 0.06)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = advanced ? 'rgba(14, 165, 233, 0.3)' : 'rgba(15, 23, 42, 0.1)';
e.currentTarget.style.boxShadow = advanced
? '0 1px 3px rgba(14, 165, 233, 0.12)'
: '0 1px 2px rgba(0, 0, 0, 0.04)';
}}
title="Enable advanced research options (Exa and Tavily configurations)"
>
<input
type="checkbox"
checked={advanced}
onChange={(e) => {
if (onAdvancedChange) {
onAdvancedChange(e.target.checked);
} else {
setLocalAdvanced(e.target.checked);
}
}}
style={{
width: '14px',
height: '14px',
cursor: 'pointer',
accentColor: '#0ea5e9',
}}
/>
<span>Advanced</span>
</label>
{/* Upload Button */}
<button
onClick={handleFileUpload}
type="button"
style={{
padding: '6px 10px',
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.25)',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '11px',
fontWeight: '600',
color: '#0369a1',
display: 'flex',
alignItems: 'center',
gap: '5px',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 1px 2px rgba(14, 165, 233, 0.12)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.35)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 2px 4px rgba(14, 165, 233, 0.18)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.25)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 1px 2px rgba(14, 165, 233, 0.12)';
}}
title="Upload Document"
>
<span style={{ fontSize: '13px' }}>📎</span>
<span>Upload</span>
</button>
</div>
</div>
{/* Header */}
<ResearchInputHeader
hasPersona={!!researchPersona}
advanced={advanced}
onAdvancedChange={setAdvanced}
onFileUpload={handleFileUpload}
/>
{/* Research History */}
<ResearchHistory
@@ -643,11 +456,55 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
onHistoryCleared={handleHistoryCleared}
/>
{/* Main Input Container */}
{/* Main Input Container with Intent & Options button */}
<ResearchInputContainer
keywords={state.keywords}
placeholder={placeholderExamples[currentPlaceholder]}
onKeywordsChange={handleKeywordsChange}
onIntentAndOptions={async () => {
if (execution?.analyzeIntent) {
try {
const response = await execution.analyzeIntent(state);
// Apply optimized config from intent analysis (if available)
if (response?.success && response.optimized_config) {
const optConfig = response.optimized_config;
const configUpdates: any = {};
// Apply recommended provider
if (response.recommended_provider) {
configUpdates.provider = response.recommended_provider;
}
// Apply Exa settings (note: backend uses exa_type, but frontend state uses exa_search_type)
if (optConfig.exa_category) configUpdates.exa_category = optConfig.exa_category;
if (optConfig.exa_type) configUpdates.exa_search_type = optConfig.exa_type as 'auto' | 'keyword' | 'neural';
if (optConfig.exa_include_domains) configUpdates.exa_include_domains = optConfig.exa_include_domains;
if (optConfig.exa_num_results) configUpdates.exa_num_results = optConfig.exa_num_results;
// Apply Tavily settings
if (optConfig.tavily_topic) configUpdates.tavily_topic = optConfig.tavily_topic;
if (optConfig.tavily_search_depth) configUpdates.tavily_search_depth = optConfig.tavily_search_depth;
if (optConfig.tavily_include_answer !== undefined) configUpdates.tavily_include_answer = optConfig.tavily_include_answer;
if (optConfig.tavily_time_range) configUpdates.tavily_time_range = optConfig.tavily_time_range;
// Update state with optimized config
if (Object.keys(configUpdates).length > 0) {
console.log('[ResearchInput] Applying optimized config from intent:', configUpdates);
onUpdate({ config: { ...state.config, ...configUpdates } });
}
}
// After analysis, show advanced options
setAdvanced(true);
} catch (error) {
console.error('[ResearchInput] Intent analysis error:', error);
}
}
}}
isAnalyzingIntent={execution?.isAnalyzingIntent}
hasIntentAnalysis={!!execution?.intentAnalysis}
intentConfidence={execution?.intentAnalysis?.intent?.confidence || 0}
/>
{/* Hidden File Input */}
@@ -662,24 +519,71 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
{/* 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}
/>
{/* Error Display */}
{execution && execution.error && (
<div style={{
padding: '16px',
marginTop: '16px',
backgroundColor: '#fee2e2',
border: '1px solid #fca5a5',
borderRadius: '8px',
color: '#991b1b',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}></span>
<strong>Smart Research Error</strong>
</div>
<p style={{ margin: 0, fontSize: '14px' }}>{execution.error}</p>
<button
onClick={() => {
if (execution.clearIntent) {
execution.clearIntent();
}
}}
style={{
marginTop: '12px',
padding: '6px 12px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '12px',
}}
>
Dismiss
</button>
</div>
)}
{/* Intent Analysis Panel - Always inline when available (Unified Design) */}
{execution && execution.intentAnalysis && (
<div style={{
marginTop: '20px',
animation: 'fadeIn 0.3s ease-out',
}}>
<IntentConfirmationPanel
isAnalyzing={execution.isAnalyzingIntent}
intentAnalysis={execution.intentAnalysis}
confirmedIntent={execution.confirmedIntent}
onConfirm={execution.confirmIntent}
onUpdateField={execution.updateIntentField}
onExecute={async (selectedQueries) => {
const result = await execution.executeIntentResearch(state, selectedQueries);
if (result?.success) {
// Skip to results step
onUpdate({ currentStep: 3 });
}
}}
onDismiss={execution.clearIntent}
isExecuting={execution.isExecuting}
showAdvancedOptions={advanced}
onAdvancedOptionsChange={setAdvanced}
providerAvailability={providerAvailability}
config={state.config}
onConfigUpdate={handleConfigUpdate}
/>
</div>
)}
{/* Keyword Expansion Suggestions */}
@@ -708,26 +612,13 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
</div>
{/* Advanced Options - Show when Advanced toggle is ON */}
{advanced && (
<>
{/* Tavily-Specific Options */}
{providerAvailability?.tavily_available && (
<TavilyOptions
config={state.config}
onConfigUpdate={handleConfigUpdate}
/>
)}
{/* Exa-Specific Options */}
{providerAvailability?.exa_available && (
<ExaOptions
config={state.config}
onConfigUpdate={handleConfigUpdate}
/>
)}
</>
)}
{/* Advanced Options Section */}
<AdvancedOptionsSection
advanced={advanced}
providerAvailability={providerAvailability}
config={state.config}
onConfigUpdate={handleConfigUpdate}
/>
</div>
);

View File

@@ -1,27 +1,54 @@
import React from 'react';
import React, { useState } from 'react';
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';
type ResultTab = 'summary' | 'deliverables' | 'sources' | 'analysis';
interface StepResultsProps extends WizardStepProps {
execution?: ResearchExecution;
}
export const StepResults: React.FC<StepResultsProps> = ({ state, onUpdate, onBack, execution }) => {
const [activeTab, setActiveTab] = useState<ResultTab>('summary');
// Check if we have intent-driven results
const intentResult: IntentDrivenResearchResponse | null =
execution?.intentResult ||
(state.results as any)?.intent_result ||
null;
if (!state.results) {
// Determine if we have both types of results
const hasIntentResults = !!intentResult;
const hasTraditionalResults = !!state.results && !intentResult;
const hasAnyResults = hasIntentResults || hasTraditionalResults;
if (!hasAnyResults) {
return (
<div style={{ padding: '24px', textAlign: 'center' }}>
<p style={{ color: '#666' }}>No results available</p>
</div>
);
}
// Get counts for tab badges
const getTabBadge = (tab: ResultTab): number | undefined => {
if (!intentResult) return undefined;
switch (tab) {
case 'deliverables':
return (intentResult.statistics?.length || 0) +
(intentResult.expert_quotes?.length || 0) +
(intentResult.case_studies?.length || 0) +
(intentResult.trends?.length || 0) +
(intentResult.best_practices?.length || 0);
case 'sources':
return intentResult.sources?.length || state.results?.sources?.length || 0;
default:
return undefined;
}
};
const handleExport = () => {
const dataStr = JSON.stringify(state.results, null, 2);
@@ -105,19 +132,253 @@ export const StepResults: React.FC<StepResultsProps> = ({ state, onUpdate, onBac
</div>
</div>
{/* Results Display */}
{/* Unified Tabbed Results Display */}
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
borderRadius: '12px',
border: '1px solid #e0e0e0',
overflow: 'hidden',
padding: intentResult ? '16px' : '0',
}}>
{intentResult ? (
<IntentResultsDisplay result={intentResult} />
) : (
<ResearchResults research={state.results} />
{/* Tab Navigation */}
{hasIntentResults && (
<div style={{
display: 'flex',
borderBottom: '2px solid #e5e7eb',
backgroundColor: '#f8fafc',
}}>
{[
{ id: 'summary', label: '📋 Summary', icon: '📋' },
{ id: 'deliverables', label: '📊 Deliverables', icon: '📊' },
{ id: 'sources', label: '🔗 Sources', icon: '🔗' },
{ id: 'analysis', label: '📈 Analysis', icon: '📈' },
].map((tab) => {
const badge = getTabBadge(tab.id as ResultTab);
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as ResultTab)}
style={{
flex: 1,
padding: '14px 16px',
border: 'none',
background: isActive
? 'white'
: 'transparent',
borderBottom: isActive
? '3px solid #0ea5e9'
: '3px solid transparent',
cursor: 'pointer',
fontSize: '14px',
fontWeight: isActive ? '600' : '500',
color: isActive ? '#0c4a6e' : '#64748b',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = '#f1f5f9';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
}
}}
>
<span>{tab.label}</span>
{badge !== undefined && badge > 0 && (
<span style={{
backgroundColor: isActive ? '#0ea5e9' : '#e2e8f0',
color: isActive ? 'white' : '#64748b',
padding: '2px 8px',
borderRadius: '10px',
fontSize: '11px',
fontWeight: '600',
}}>
{badge}
</span>
)}
</button>
);
})}
</div>
)}
{/* Tab Content */}
<div style={{ padding: '20px' }}>
{hasIntentResults ? (
<>
{/* Summary Tab */}
{activeTab === 'summary' && (
<div style={{ animation: 'fadeIn 0.3s ease' }}>
{intentResult.executive_summary && (
<div style={{
backgroundColor: '#f0f9ff',
border: '1px solid #bae6fd',
borderRadius: '8px',
padding: '16px',
marginBottom: '20px',
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#0c4a6e' }}>Executive Summary</h4>
<p style={{ margin: 0, color: '#334155', lineHeight: 1.6 }}>
{intentResult.executive_summary}
</p>
</div>
)}
{intentResult.primary_answer && (
<div style={{
backgroundColor: '#f0fdf4',
border: '1px solid #86efac',
borderRadius: '8px',
padding: '16px',
marginBottom: '20px',
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#166534' }}>Direct Answer</h4>
<p style={{ margin: 0, color: '#334155', lineHeight: 1.6 }}>
{intentResult.primary_answer}
</p>
</div>
)}
{intentResult.key_takeaways && intentResult.key_takeaways.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<h4 style={{ margin: '0 0 12px 0', color: '#333' }}>Key Takeaways</h4>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{intentResult.key_takeaways.map((takeaway, idx) => (
<li key={idx} style={{ color: '#334155', marginBottom: '8px', lineHeight: 1.5 }}>
{takeaway}
</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Deliverables Tab - Uses IntentResultsDisplay */}
{activeTab === 'deliverables' && (
<div style={{ animation: 'fadeIn 0.3s ease' }}>
<IntentResultsDisplay result={intentResult} hideHeader />
</div>
)}
{/* Sources Tab - Shows traditional sources view */}
{activeTab === 'sources' && (
<div style={{ animation: 'fadeIn 0.3s ease' }}>
{intentResult.sources && intentResult.sources.length > 0 ? (
<div>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
{intentResult.sources.length} Sources Found
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{intentResult.sources.map((source: any, idx: number) => (
<div
key={idx}
style={{
padding: '16px',
backgroundColor: '#f8fafc',
borderRadius: '8px',
border: '1px solid #e2e8f0',
}}
>
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: '15px',
fontWeight: '600',
color: '#0ea5e9',
textDecoration: 'none',
}}
>
{source.title}
</a>
<div style={{
fontSize: '12px',
color: '#64748b',
marginTop: '4px',
}}>
{source.url}
</div>
{source.excerpt && (
<p style={{
margin: '8px 0 0 0',
fontSize: '13px',
color: '#475569',
lineHeight: 1.5,
}}>
{source.excerpt}
</p>
)}
</div>
))}
</div>
</div>
) : state.results?.sources ? (
<ResearchResults research={state.results} showSourcesOnly />
) : (
<p style={{ color: '#666' }}>No sources available</p>
)}
</div>
)}
{/* Analysis Tab - Shows keyword analysis, angles, etc. */}
{activeTab === 'analysis' && (
<div style={{ animation: 'fadeIn 0.3s ease' }}>
{state.results ? (
<ResearchResults research={state.results} showAnalysisOnly />
) : (
<div>
{intentResult.suggested_outline && intentResult.suggested_outline.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<h4 style={{ margin: '0 0 12px 0', color: '#333' }}>Suggested Outline</h4>
<ol style={{ margin: 0, paddingLeft: '24px' }}>
{intentResult.suggested_outline.map((item, idx) => (
<li key={idx} style={{ color: '#334155', marginBottom: '8px' }}>
{item}
</li>
))}
</ol>
</div>
)}
{intentResult.gaps_identified && intentResult.gaps_identified.length > 0 && (
<div style={{
backgroundColor: '#fff7ed',
border: '1px solid #fdba74',
borderRadius: '8px',
padding: '16px',
}}>
<h4 style={{ margin: '0 0 12px 0', color: '#9a3412' }}>Gaps Identified</h4>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{intentResult.gaps_identified.map((gap, idx) => (
<li key={idx} style={{ color: '#9a3412', marginBottom: '4px' }}>
{gap}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
)}
</>
) : state.results ? (
// Traditional results display (no tabs)
<ResearchResults research={state.results} />
) : (
<p style={{ color: '#666', textAlign: 'center', padding: '40px' }}>
No results available
</p>
)}
</div>
</div>
{/* Action Section */}

View File

@@ -0,0 +1,47 @@
/**
* AdvancedOptionsSection Component
*
* Displays advanced provider options (Exa/Tavily) when advanced mode is enabled
*/
import React from 'react';
import { ProviderAvailability } from '../../../../api/researchConfig';
import { ResearchConfig } from '../../../../services/blogWriterApi';
import { ExaOptions } from './ExaOptions';
import { TavilyOptions } from './TavilyOptions';
interface AdvancedOptionsSectionProps {
advanced: boolean;
providerAvailability: ProviderAvailability | null;
config: ResearchConfig;
onConfigUpdate: (updates: Partial<ResearchConfig>) => void;
}
export const AdvancedOptionsSection: React.FC<AdvancedOptionsSectionProps> = ({
advanced,
providerAvailability,
config,
onConfigUpdate,
}) => {
if (!advanced) return null;
return (
<>
{/* Tavily-Specific Options */}
{providerAvailability?.tavily_available && (
<TavilyOptions
config={config}
onConfigUpdate={onConfigUpdate}
/>
)}
{/* Exa-Specific Options */}
{providerAvailability?.exa_available && (
<ExaOptions
config={config}
onConfigUpdate={onConfigUpdate}
/>
)}
</>
);
};

View File

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

View File

@@ -0,0 +1,70 @@
/**
* ActionButtons Component
*
* Action buttons section (More details toggle and Start Research).
*/
import React from 'react';
import {
Box,
Button,
CircularProgress,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
PlayArrow as PlayIcon,
} from '@mui/icons-material';
interface ActionButtonsProps {
showDetails: boolean;
onToggleDetails: () => void;
onExecute: () => void;
isExecuting: boolean;
canExecute: boolean;
}
export const ActionButtons: React.FC<ActionButtonsProps> = ({
showDetails,
onToggleDetails,
onExecute,
isExecuting,
canExecute,
}) => {
return (
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mt={2}
pt={2}
borderTop="1px solid #e5e7eb"
>
<Button
size="small"
onClick={onToggleDetails}
endIcon={showDetails ? <ExpandLessIcon /> : <ExpandMoreIcon />}
sx={{
color: '#666',
'&:hover': { backgroundColor: '#f3f4f6' },
}}
>
{showDetails ? 'Less details' : 'More details'}
</Button>
<Button
variant="contained"
color="primary"
startIcon={isExecuting ? <CircularProgress size={16} color="inherit" /> : <PlayIcon />}
onClick={onExecute}
disabled={isExecuting || !canExecute}
sx={{
backgroundColor: '#0ea5e9',
'&:hover': { backgroundColor: '#0284c7' },
'&:disabled': { backgroundColor: '#d1d5db', color: '#9ca3af' },
}}
>
{isExecuting ? 'Researching...' : 'Start Research'}
</Button>
</Box>
);
};

View File

@@ -0,0 +1,268 @@
/**
* AdvancedProviderOptionsSection Component
*
* Advanced provider options section with AI-optimized settings.
* This is specific to IntentConfirmationPanel and includes AI justifications.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
Tooltip,
Button,
} from '@mui/material';
import {
Info as InfoIcon,
} from '@mui/icons-material';
import { AnalyzeIntentResponse } from '../../../types/intent.types';
import { ProviderAvailability } from '../../../../../api/researchConfig';
import { ExaOptions } from '../ExaOptions';
import { TavilyOptions } from '../TavilyOptions';
import { ProviderChips } from '../ProviderChips';
import { ResearchProvider } from '../../../../../services/blogWriterApi';
interface AdvancedProviderOptionsSectionProps {
intentAnalysis: AnalyzeIntentResponse;
providerAvailability: ProviderAvailability;
config: any;
onConfigUpdate: (updates: any) => void;
showAdvancedOptions: boolean;
onAdvancedOptionsChange: (show: boolean) => void;
}
export const AdvancedProviderOptionsSection: React.FC<AdvancedProviderOptionsSectionProps> = ({
intentAnalysis,
providerAvailability,
config,
onConfigUpdate,
showAdvancedOptions,
onAdvancedOptionsChange,
}) => {
return (
<>
{/* Toggle Advanced Options Button */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button
size="small"
variant="text"
onClick={() => onAdvancedOptionsChange(!showAdvancedOptions)}
sx={{
color: '#64748b',
fontSize: '12px',
'&:hover': {
backgroundColor: '#f8fafc',
color: '#0ea5e9',
},
}}
>
{showAdvancedOptions ? '▲ Hide Advanced Options' : '▼ Show Advanced Options'}
</Button>
</Box>
{/* Advanced Options Section */}
{showAdvancedOptions && (
<Box sx={{
mt: 2,
pt: 2,
borderTop: '1px dashed rgba(14, 165, 233, 0.3)',
}}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Typography
variant="subtitle2"
sx={{
color: '#0c4a6e',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
AI-Optimized Settings
</Typography>
<Tooltip title="These settings were AI-configured based on your research intent. Hover over each for explanation." arrow>
<InfoIcon sx={{ fontSize: 16, color: '#94a3b8', cursor: 'help' }} />
</Tooltip>
</Box>
{/* AI Justification Banner */}
{intentAnalysis?.optimized_config?.provider_justification && (
<Box sx={{
mb: 2,
p: 1.5,
backgroundColor: '#f0fdf4',
border: '1px solid #86efac',
borderRadius: '8px',
display: 'flex',
alignItems: 'flex-start',
gap: 1,
}}>
<span style={{ fontSize: '14px' }}>🤖</span>
<Typography variant="body2" sx={{ color: '#166534', fontSize: '12px' }}>
<strong>AI Recommendation:</strong> {intentAnalysis.optimized_config.provider_justification}
</Typography>
</Box>
)}
{/* Provider Selection with Justification */}
<Box mb={2}>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<Typography variant="caption" color="#666" fontWeight={600}>
Research Provider
</Typography>
{intentAnalysis?.optimized_config?.provider_justification && (
<Tooltip title={intentAnalysis.optimized_config.provider_justification} arrow>
<Chip
label="AI"
size="small"
sx={{
height: '16px',
fontSize: '9px',
backgroundColor: '#dcfce7',
color: '#166534',
}}
/>
</Tooltip>
)}
</Box>
<ProviderChips providerAvailability={providerAvailability} />
{/* Provider Selector */}
<Box sx={{ mt: 1 }}>
<select
value={config.provider || 'exa'}
onChange={(e) => onConfigUpdate({ provider: e.target.value as ResearchProvider })}
style={{
padding: '6px 12px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
fontSize: '13px',
backgroundColor: 'white',
cursor: 'pointer',
}}
>
{providerAvailability.exa_available && <option value="exa">Exa</option>}
{providerAvailability.tavily_available && <option value="tavily">Tavily</option>}
<option value="google">Google</option>
</select>
</Box>
</Box>
{/* Provider-specific Options with AI tooltips */}
{config.provider === 'exa' && providerAvailability.exa_available && (
<>
{/* AI Settings Summary for Exa */}
{intentAnalysis?.optimized_config && (
<Box sx={{
mb: 2,
p: 1.5,
backgroundColor: '#f8fafc',
borderRadius: '6px',
border: '1px solid #e2e8f0',
}}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
AI-Selected Exa Settings
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
{intentAnalysis.optimized_config.exa_type && (
<Tooltip title={intentAnalysis.optimized_config.exa_type_justification || 'Search type'} arrow>
<Chip
label={`Type: ${intentAnalysis.optimized_config.exa_type}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#e0f2fe', color: '#0369a1' }}
/>
</Tooltip>
)}
{intentAnalysis.optimized_config.exa_category && (
<Tooltip title={intentAnalysis.optimized_config.exa_category_justification || 'Category focus'} arrow>
<Chip
label={`Category: ${intentAnalysis.optimized_config.exa_category}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#fef3c7', color: '#92400e' }}
/>
</Tooltip>
)}
{intentAnalysis.optimized_config.exa_date_filter && (
<Tooltip title={intentAnalysis.optimized_config.exa_date_justification || 'Date filter'} arrow>
<Chip
label={`Since: ${intentAnalysis.optimized_config.exa_date_filter.split('T')[0]}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#f3e8ff', color: '#7c3aed' }}
/>
</Tooltip>
)}
</Box>
</Box>
)}
<ExaOptions
config={config}
onConfigUpdate={onConfigUpdate}
/>
</>
)}
{config.provider === 'tavily' && providerAvailability.tavily_available && (
<>
{/* AI Settings Summary for Tavily */}
{intentAnalysis?.optimized_config && (
<Box sx={{
mb: 2,
p: 1.5,
backgroundColor: '#f8fafc',
borderRadius: '6px',
border: '1px solid #e2e8f0',
}}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
AI-Selected Tavily Settings
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
{intentAnalysis.optimized_config.tavily_topic && (
<Tooltip title={intentAnalysis.optimized_config.tavily_topic_justification || 'Topic category'} arrow>
<Chip
label={`Topic: ${intentAnalysis.optimized_config.tavily_topic}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#e0f2fe', color: '#0369a1' }}
/>
</Tooltip>
)}
{intentAnalysis.optimized_config.tavily_search_depth && (
<Tooltip title={intentAnalysis.optimized_config.tavily_search_depth_justification || 'Search depth'} arrow>
<Chip
label={`Depth: ${intentAnalysis.optimized_config.tavily_search_depth}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#fef3c7', color: '#92400e' }}
/>
</Tooltip>
)}
{intentAnalysis.optimized_config.tavily_time_range && (
<Tooltip title={intentAnalysis.optimized_config.tavily_time_range_justification || 'Time filter'} arrow>
<Chip
label={`Time: ${intentAnalysis.optimized_config.tavily_time_range}`}
size="small"
sx={{ fontSize: '11px', backgroundColor: '#dcfce7', color: '#166534' }}
/>
</Tooltip>
)}
{intentAnalysis.optimized_config.tavily_include_answer && (
<Tooltip title={intentAnalysis.optimized_config.tavily_include_answer_justification || 'AI answer'} arrow>
<Chip
label="AI Answer ✓"
size="small"
sx={{ fontSize: '11px', backgroundColor: '#f3e8ff', color: '#7c3aed' }}
/>
</Tooltip>
)}
</Box>
</Box>
)}
<TavilyOptions
config={config}
onConfigUpdate={onConfigUpdate}
/>
</>
)}
</Box>
)}
</>
);
};

View File

@@ -0,0 +1,76 @@
/**
* DeliverablesSelector Component
*
* Allows user to select/remove expected deliverables.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
Tooltip,
} from '@mui/material';
import {
Info as InfoIcon,
} from '@mui/icons-material';
import {
ResearchIntent,
ExpectedDeliverable,
DELIVERABLE_DISPLAY,
} from '../../../types/intent.types';
interface DeliverablesSelectorProps {
intent: ResearchIntent;
onToggle: (deliverable: ExpectedDeliverable) => void;
}
export const DeliverablesSelector: React.FC<DeliverablesSelectorProps> = ({
intent,
onToggle,
}) => {
return (
<Box
mb={2}
sx={{
p: 2,
backgroundColor: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: 1,
}}
>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="caption" color="#666" fontWeight={600}>
What I'll find for you:
</Typography>
<Tooltip title="Click chips to select/remove deliverables">
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af' }} />
</Tooltip>
</Box>
<Box display="flex" flexWrap="wrap" gap={0.5}>
{Object.entries(DELIVERABLE_DISPLAY).map(([key, label]) => {
const isSelected = intent.expected_deliverables.includes(key as ExpectedDeliverable);
return (
<Chip
key={key}
label={label}
size="small"
onClick={() => onToggle(key as ExpectedDeliverable)}
sx={{
backgroundColor: isSelected ? '#dbeafe' : '#ffffff',
border: `1px solid ${isSelected ? '#3b82f6' : '#d1d5db'}`,
color: isSelected ? '#1e40af' : '#374151',
cursor: 'pointer',
'&:hover': {
backgroundColor: isSelected ? '#bfdbfe' : '#f3f4f6',
borderColor: isSelected ? '#2563eb' : '#9ca3af',
},
fontWeight: isSelected ? 600 : 400,
}}
/>
);
})}
</Box>
</Box>
);
};

View File

@@ -0,0 +1,108 @@
/**
* EditableField Component
*
* Reusable component for inline editing of intent fields.
* Supports text input and select dropdown.
*/
import React, { useState } from 'react';
import {
Box,
Typography,
IconButton,
TextField,
FormControl,
Select,
MenuItem,
} from '@mui/material';
import {
Edit as EditIcon,
Save as SaveIcon,
Cancel as CancelIcon,
} from '@mui/icons-material';
interface EditableFieldProps {
field: string;
value: any;
displayValue: string;
options?: Array<{ key: string; label: string }>;
onSave: (newValue: any) => void;
}
export const EditableField: React.FC<EditableFieldProps> = ({
field,
value,
displayValue,
options,
onSave,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const handleSave = () => {
onSave(editValue);
setIsEditing(false);
};
const handleCancel = () => {
setEditValue(value);
setIsEditing(false);
};
return (
<Box display="flex" alignItems="center" gap={0.5}>
{isEditing ? (
<Box display="flex" alignItems="center" gap={0.5} flex={1}>
{options ? (
<FormControl size="small" fullWidth>
<Select
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
sx={{ backgroundColor: '#ffffff' }}
autoFocus
>
{options.map(opt => (
<MenuItem key={opt.key} value={opt.key}>{opt.label}</MenuItem>
))}
</Select>
</FormControl>
) : (
<TextField
size="small"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
fullWidth
sx={{ backgroundColor: '#ffffff' }}
autoFocus
/>
)}
<IconButton size="small" onClick={handleSave} color="primary">
<SaveIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={handleCancel} color="inherit">
<CancelIcon fontSize="small" />
</IconButton>
</Box>
) : (
<>
<Typography variant="body2" fontWeight={500} color="#333" sx={{ flex: 1 }}>
{displayValue}
</Typography>
<IconButton
size="small"
onClick={() => setIsEditing(true)}
sx={{
color: '#666',
'&:hover': {
backgroundColor: '#f3f4f6',
color: '#0ea5e9',
},
}}
>
<EditIcon fontSize="small" />
</IconButton>
</>
)}
</Box>
);
};

View File

@@ -0,0 +1,83 @@
/**
* ExpandableDetails Component
*
* Collapsible section showing secondary questions, focus areas, and research angles.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
Collapse,
} from '@mui/material';
import { AnalyzeIntentResponse } from '../../../types/intent.types';
interface ExpandableDetailsProps {
intentAnalysis: AnalyzeIntentResponse;
expanded: boolean;
}
export const ExpandableDetails: React.FC<ExpandableDetailsProps> = ({
intentAnalysis,
expanded,
}) => {
const intent = intentAnalysis.intent;
return (
<Collapse in={expanded}>
<Box sx={{ pt: 2, borderTop: '1px solid #e5e7eb', mt: 2 }}>
{/* Secondary Questions */}
{intent.secondary_questions.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
Also answering:
</Typography>
{intent.secondary_questions.slice(0, 3).map((q, idx) => (
<Typography key={idx} variant="body2" color="#333" sx={{ ml: 1, mb: 0.5 }}>
{q}
</Typography>
))}
</Box>
)}
{/* Focus Areas */}
{intent.focus_areas.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} 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"
sx={{
backgroundColor: '#f3f4f6',
border: '1px solid #d1d5db',
color: '#374151',
}}
/>
))}
</Box>
</Box>
)}
{/* Research Angles */}
{intentAnalysis.suggested_angles?.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
Research angles:
</Typography>
{intentAnalysis.suggested_angles.slice(0, 3).map((angle, idx) => (
<Typography key={idx} variant="body2" color="#333" sx={{ ml: 1, mb: 0.5 }}>
{angle}
</Typography>
))}
</Box>
)}
</Box>
</Collapse>
);
};

View File

@@ -0,0 +1,103 @@
/**
* IntentConfirmationHeader Component
*
* Header section showing confidence level and analysis summary.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import {
Psychology as BrainIcon,
Close as CloseIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { AnalyzeIntentResponse } from '../../../types/intent.types';
interface IntentConfirmationHeaderProps {
intentAnalysis: AnalyzeIntentResponse;
onDismiss: () => void;
}
export const IntentConfirmationHeader: React.FC<IntentConfirmationHeaderProps> = ({
intentAnalysis,
onDismiss,
}) => {
const intent = intentAnalysis.intent;
const confidence = intent.confidence;
const isHighConfidence = confidence >= 0.8;
const confidenceReason = (intentAnalysis as any).confidence_reason || '';
const greatExample = (intentAnalysis as any).great_example || '';
return (
<Box
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: isHighConfidence ? '#f0f9ff' : '#fff7ed',
borderBottom: '1px solid #e0e0e0',
}}
>
<Box display="flex" alignItems="center" gap={1.5}>
<BrainIcon sx={{ color: isHighConfidence ? '#10b981' : '#f59e0b' }} />
<Box>
<Typography variant="subtitle1" fontWeight={600} color="#333">
Help ALwrity understand Your Research
</Typography>
<Typography variant="caption" color="#666">
{intentAnalysis.analysis_summary}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Tooltip
title={
<Box sx={{ p: 1, maxWidth: 300 }}>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Confidence: {Math.round(confidence * 100)}%
</Typography>
{confidenceReason && (
<Typography variant="caption" display="block" sx={{ mb: 1 }}>
{confidenceReason}
</Typography>
)}
{greatExample && (
<>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Great example would be:
</Typography>
<Typography variant="caption" display="block">
{greatExample}
</Typography>
</>
)}
</Box>
}
arrow
>
<Chip
size="small"
label={`${Math.round(confidence * 100)}% confident`}
color={isHighConfidence ? 'success' : 'warning'}
variant="outlined"
sx={{
backgroundColor: '#ffffff',
cursor: 'help',
}}
icon={<InfoIcon fontSize="small" />}
/>
</Tooltip>
<IconButton size="small" onClick={onDismiss} sx={{ color: '#666' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,190 @@
/**
* IntentConfirmationPanel Component (Refactored)
*
* Main orchestrator component that composes smaller, focused components.
* Shows the AI-inferred research intent and allows user to confirm or modify.
*/
import React, { useState, useEffect } from 'react';
import { Paper, Box } from '@mui/material';
import {
ResearchIntent,
AnalyzeIntentResponse,
ResearchQuery,
ExpectedDeliverable,
} from '../../../types/intent.types';
import { ProviderAvailability } from '../../../../../api/researchConfig';
// Sub-components
import { LoadingState } from './LoadingState';
import { IntentConfirmationHeader } from './IntentConfirmationHeader';
import { PrimaryQuestionEditor } from './PrimaryQuestionEditor';
import { IntentSummaryGrid } from './IntentSummaryGrid';
import { DeliverablesSelector } from './DeliverablesSelector';
import { ResearchQueriesSection } from './ResearchQueriesSection';
import { TrendsConfigSection } from './TrendsConfigSection';
import { AdvancedProviderOptionsSection } from './AdvancedProviderOptionsSection';
import { ExpandableDetails } from './ExpandableDetails';
import { ActionButtons } from './ActionButtons';
export 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: (selectedQueries?: ResearchQuery[]) => void;
onDismiss: () => void;
isExecuting: boolean;
showAdvancedOptions?: boolean;
onAdvancedOptionsChange?: (show: boolean) => void;
providerAvailability?: ProviderAvailability | null;
config?: any;
onConfigUpdate?: (updates: any) => void;
}
export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = ({
isAnalyzing,
intentAnalysis,
confirmedIntent,
onConfirm,
onUpdateField,
onExecute,
onDismiss,
isExecuting,
showAdvancedOptions = false,
onAdvancedOptionsChange,
providerAvailability,
config,
onConfigUpdate,
}) => {
const [showDetails, setShowDetails] = useState(false);
const [selectedQueries, setSelectedQueries] = useState<Set<number>>(
new Set(intentAnalysis?.suggested_queries?.map((_, idx) => idx) || [])
);
const [editedQueries, setEditedQueries] = useState<ResearchQuery[]>(
intentAnalysis?.suggested_queries || []
);
// Update edited queries when intentAnalysis changes
useEffect(() => {
if (intentAnalysis?.suggested_queries) {
setEditedQueries(intentAnalysis.suggested_queries);
setSelectedQueries(new Set(intentAnalysis.suggested_queries.map((_, idx) => idx)));
}
}, [intentAnalysis]);
// Loading state
if (isAnalyzing) {
return <LoadingState />;
}
// No analysis yet
if (!intentAnalysis || !intentAnalysis.success) {
return null;
}
const intent = intentAnalysis.intent;
const handleDeliverableToggle = (deliverable: ExpectedDeliverable) => {
const current = intent.expected_deliverables || [];
const updated = current.includes(deliverable)
? current.filter(d => d !== deliverable)
: [...current, deliverable];
onUpdateField('expected_deliverables', updated);
};
const handleExecute = () => {
const updatedIntent = { ...intent };
onConfirm(updatedIntent);
const queriesToUse = Array.from(selectedQueries)
.sort((a, b) => a - b)
.map(idx => editedQueries[idx])
.filter(q => q && q.query.trim().length > 0);
onExecute(queriesToUse);
};
return (
<Paper
elevation={0}
sx={{
mt: 2,
borderRadius: 2,
border: '1px solid #e0e0e0',
backgroundColor: '#ffffff',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
}}
>
{/* Header */}
<IntentConfirmationHeader
intentAnalysis={intentAnalysis}
onDismiss={onDismiss}
/>
{/* Main Content */}
<Box sx={{ p: 2, backgroundColor: '#ffffff' }}>
{/* Primary Question */}
<PrimaryQuestionEditor
intent={intent}
onUpdate={(value) => onUpdateField('primary_question', value)}
/>
{/* Quick Summary Grid */}
<IntentSummaryGrid
intent={intent}
queriesCount={editedQueries.length}
onUpdateField={onUpdateField}
/>
{/* Deliverables Selector */}
<DeliverablesSelector
intent={intent}
onToggle={handleDeliverableToggle}
/>
{/* Research Queries Section */}
<ResearchQueriesSection
queries={editedQueries}
selectedQueries={selectedQueries}
onQueriesChange={setEditedQueries}
onSelectionChange={setSelectedQueries}
/>
{/* Google Trends Section */}
{intentAnalysis.trends_config && (
<TrendsConfigSection trendsConfig={intentAnalysis.trends_config} />
)}
{/* Advanced Options Section */}
{providerAvailability && config && onConfigUpdate && onAdvancedOptionsChange && (
<AdvancedProviderOptionsSection
intentAnalysis={intentAnalysis}
providerAvailability={providerAvailability}
config={config}
onConfigUpdate={onConfigUpdate}
showAdvancedOptions={showAdvancedOptions}
onAdvancedOptionsChange={onAdvancedOptionsChange}
/>
)}
{/* Expandable Details */}
<ExpandableDetails
intentAnalysis={intentAnalysis}
expanded={showDetails}
/>
{/* Action Buttons */}
<ActionButtons
showDetails={showDetails}
onToggleDetails={() => setShowDetails(!showDetails)}
onExecute={handleExecute}
isExecuting={isExecuting}
canExecute={selectedQueries.size > 0}
/>
</Box>
</Paper>
);
};
export default IntentConfirmationPanel;

View File

@@ -0,0 +1,103 @@
/**
* IntentConfirmationPanelHeader Component
*
* Header section with title, confidence indicator, and close button.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import {
Psychology as BrainIcon,
Close as CloseIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { AnalyzeIntentResponse } from '../../../types/intent.types';
interface IntentConfirmationPanelHeaderProps {
intentAnalysis: AnalyzeIntentResponse;
onDismiss: () => void;
}
export const IntentConfirmationPanelHeader: React.FC<IntentConfirmationPanelHeaderProps> = ({
intentAnalysis,
onDismiss,
}) => {
const intent = intentAnalysis.intent;
const confidence = intent.confidence;
const isHighConfidence = confidence >= 0.8;
const confidenceReason = (intentAnalysis as any).confidence_reason || '';
const greatExample = (intentAnalysis as any).great_example || '';
return (
<Box
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: isHighConfidence ? '#f0f9ff' : '#fff7ed',
borderBottom: '1px solid #e0e0e0',
}}
>
<Box display="flex" alignItems="center" gap={1.5}>
<BrainIcon sx={{ color: isHighConfidence ? '#10b981' : '#f59e0b' }} />
<Box>
<Typography variant="subtitle1" fontWeight={600} color="#333">
Help ALwrity understand Your Research
</Typography>
<Typography variant="caption" color="#666">
{intentAnalysis.analysis_summary}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Tooltip
title={
<Box sx={{ p: 1, maxWidth: 300 }}>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Confidence: {Math.round(confidence * 100)}%
</Typography>
{confidenceReason && (
<Typography variant="caption" display="block" sx={{ mb: 1 }}>
{confidenceReason}
</Typography>
)}
{greatExample && (
<>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Great example would be:
</Typography>
<Typography variant="caption" display="block">
{greatExample}
</Typography>
</>
)}
</Box>
}
arrow
>
<Chip
size="small"
label={`${Math.round(confidence * 100)}% confident`}
color={isHighConfidence ? 'success' : 'warning'}
variant="outlined"
sx={{
backgroundColor: '#ffffff',
cursor: 'help',
}}
icon={<InfoIcon fontSize="small" />}
/>
</Tooltip>
<IconButton size="small" onClick={onDismiss} sx={{ color: '#666' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,108 @@
/**
* IntentSummaryGrid Component
*
* Quick summary grid showing Purpose, Content Type, Depth, and Queries Count.
*/
import React from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
} from '@mui/material';
import {
ResearchIntent,
ResearchPurpose,
ContentOutput,
ResearchDepthLevel,
PURPOSE_DISPLAY,
CONTENT_OUTPUT_DISPLAY,
DEPTH_DISPLAY,
} from '../../../types/intent.types';
import { EditableField } from './EditableField';
interface IntentSummaryGridProps {
intent: ResearchIntent;
queriesCount: number;
onUpdateField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
}
export const IntentSummaryGrid: React.FC<IntentSummaryGridProps> = ({
intent,
queriesCount,
onUpdateField,
}) => {
return (
<Grid container spacing={2} mb={2}>
{/* Purpose */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Purpose
</Typography>
<EditableField
field="purpose"
value={intent.purpose}
displayValue={PURPOSE_DISPLAY[intent.purpose as ResearchPurpose] || intent.purpose}
options={Object.entries(PURPOSE_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('purpose', val as ResearchPurpose)}
/>
</CardContent>
</Card>
</Grid>
{/* Content Type */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Creating
</Typography>
<EditableField
field="content_output"
value={intent.content_output}
displayValue={CONTENT_OUTPUT_DISPLAY[intent.content_output as ContentOutput] || intent.content_output}
options={Object.entries(CONTENT_OUTPUT_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('content_output', val as ContentOutput)}
/>
</CardContent>
</Card>
</Grid>
{/* Depth */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Depth
</Typography>
<EditableField
field="depth"
value={intent.depth}
displayValue={DEPTH_DISPLAY[intent.depth as ResearchDepthLevel] || intent.depth}
options={Object.entries(DEPTH_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('depth', val as ResearchDepthLevel)}
/>
</CardContent>
</Card>
</Grid>
{/* Queries Count */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Queries
</Typography>
<Typography variant="body2" fontWeight={500} color="#333">
{queriesCount} targeted
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,43 @@
/**
* LoadingState Component
*
* Displays loading indicator while intent is being analyzed.
*/
import React from 'react';
import { Box, Typography, Paper, CircularProgress } from '@mui/material';
interface LoadingStateProps {
message?: string;
subMessage?: string;
}
export const LoadingState: React.FC<LoadingStateProps> = ({
message = '🧠 Analyzing your research intent...',
subMessage = 'AI is understanding what you want to accomplish',
}) => {
return (
<Paper
elevation={0}
sx={{
p: 3,
mt: 2,
borderRadius: 2,
border: '1px solid #e0e0e0',
backgroundColor: '#ffffff',
}}
>
<Box display="flex" alignItems="center" gap={2}>
<CircularProgress size={24} />
<Box>
<Typography variant="subtitle1" fontWeight={600} color="#333">
{message}
</Typography>
<Typography variant="body2" color="#666">
{subMessage}
</Typography>
</Box>
</Box>
</Paper>
);
};

View File

@@ -0,0 +1,115 @@
/**
* PrimaryQuestionEditor Component
*
* Editable primary question section.
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
IconButton,
} from '@mui/material';
import {
Edit as EditIcon,
Save as SaveIcon,
Cancel as CancelIcon,
} from '@mui/icons-material';
import { ResearchIntent } from '../../../types/intent.types';
interface PrimaryQuestionEditorProps {
intent: ResearchIntent;
onUpdate: (value: string) => void;
}
export const PrimaryQuestionEditor: React.FC<PrimaryQuestionEditorProps> = ({
intent,
onUpdate,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(intent.primary_question);
useEffect(() => {
setValue(intent.primary_question);
}, [intent.primary_question]);
const handleSave = () => {
if (value.trim()) {
onUpdate(value.trim());
}
setIsEditing(false);
};
const handleCancel = () => {
setValue(intent.primary_question);
setIsEditing(false);
};
return (
<Box
sx={{
mb: 2,
p: 2,
backgroundColor: '#f0f9ff',
border: '1px solid #bae6fd',
borderRadius: 1,
}}
>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="caption" fontWeight={600} color="#0c4a6e">
Main Question:
</Typography>
{!isEditing && (
<IconButton
size="small"
onClick={() => setIsEditing(true)}
sx={{
color: '#666',
'&:hover': {
backgroundColor: '#e0f2fe',
color: '#0ea5e9',
},
}}
>
<EditIcon fontSize="small" />
</IconButton>
)}
</Box>
{isEditing ? (
<Box display="flex" alignItems="flex-start" gap={1}>
<TextField
fullWidth
multiline
rows={2}
value={value}
onChange={(e) => setValue(e.target.value)}
sx={{ backgroundColor: '#ffffff' }}
autoFocus
/>
<Box display="flex" flexDirection="column" gap={0.5}>
<IconButton
size="small"
onClick={handleSave}
color="primary"
sx={{ backgroundColor: '#e0f2fe' }}
>
<SaveIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={handleCancel}
sx={{ color: '#666' }}
>
<CancelIcon fontSize="small" />
</IconButton>
</Box>
</Box>
) : (
<Typography variant="body2" fontWeight={500} color="#0c4a6e">
{intent.primary_question}
</Typography>
)}
</Box>
);
};

View File

@@ -0,0 +1,150 @@
/**
* QueryEditor Component
*
* Individual query editor with provider, purpose, priority, and expected results.
*/
import React from 'react';
import {
Box,
TextField,
FormControl,
Select,
MenuItem,
Checkbox,
IconButton,
ListItem,
ListItemSecondaryAction,
} from '@mui/material';
import {
Delete as DeleteIcon,
} from '@mui/icons-material';
import {
ResearchQuery,
ExpectedDeliverable,
DELIVERABLE_DISPLAY,
} from '../../../types/intent.types';
interface QueryEditorProps {
query: ResearchQuery;
index: number;
isSelected: boolean;
onToggle: () => void;
onEdit: (field: keyof ResearchQuery, value: any) => void;
onDelete: () => void;
}
export const QueryEditor: React.FC<QueryEditorProps> = ({
query,
index,
isSelected,
onToggle,
onEdit,
onDelete,
}) => {
return (
<ListItem
sx={{
backgroundColor: isSelected ? '#e0f2fe' : '#ffffff',
borderLeft: isSelected ? '3px solid #0ea5e9' : '3px solid transparent',
'&:hover': { backgroundColor: isSelected ? '#bae6fd' : '#f9fafb' },
py: 1.5,
}}
>
<Checkbox
checked={isSelected}
onChange={onToggle}
size="small"
sx={{ mr: 1 }}
/>
<Box flex={1}>
<TextField
fullWidth
size="small"
value={query.query}
onChange={(e) => onEdit('query', e.target.value)}
placeholder="Enter research query"
sx={{
mb: 1,
backgroundColor: '#ffffff',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#0ea5e9' },
'&.Mui-focused fieldset': { borderColor: '#0ea5e9' },
},
}}
/>
<Box display="flex" gap={1} flexWrap="wrap">
<FormControl size="small" sx={{ minWidth: 100 }}>
<Select
value={query.provider}
onChange={(e) => onEdit('provider', e.target.value)}
sx={{
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
>
<MenuItem value="exa">Exa</MenuItem>
<MenuItem value="tavily">Tavily</MenuItem>
<MenuItem value="google">Google</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<Select
value={query.purpose}
onChange={(e) => onEdit('purpose', e.target.value)}
sx={{
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
>
{Object.entries(DELIVERABLE_DISPLAY).map(([key, label]) => (
<MenuItem key={key} value={key}>{label}</MenuItem>
))}
</Select>
</FormControl>
<TextField
size="small"
type="number"
value={query.priority}
onChange={(e) => onEdit('priority', parseInt(e.target.value) || 1)}
inputProps={{ min: 1, max: 5 }}
sx={{
width: 90,
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
label="Priority"
/>
</Box>
<TextField
fullWidth
size="small"
value={query.expected_results}
onChange={(e) => onEdit('expected_results', e.target.value)}
placeholder="What we expect to find"
sx={{
mt: 1,
backgroundColor: '#ffffff',
fontSize: '0.75rem',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#0ea5e9' },
},
}}
/>
</Box>
<ListItemSecondaryAction>
<IconButton
edge="end"
size="small"
onClick={onDelete}
sx={{
color: '#dc2626',
'&:hover': { backgroundColor: '#fee2e2' },
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
};

View File

@@ -0,0 +1,166 @@
/**
* ResearchQueriesSection Component
*
* Accordion section for managing research queries (add, edit, delete, select).
*/
import React, { useState } from 'react';
import {
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
List,
Button,
Divider,
Chip,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
Search as SearchIcon,
} from '@mui/icons-material';
import {
ResearchQuery,
} from '../../../types/intent.types';
import { QueryEditor } from './QueryEditor';
interface ResearchQueriesSectionProps {
queries: ResearchQuery[];
selectedQueries: Set<number>;
onQueriesChange: (queries: ResearchQuery[]) => void;
onSelectionChange: (selected: Set<number>) => void;
}
export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
queries,
selectedQueries,
onQueriesChange,
onSelectionChange,
}) => {
const [expanded, setExpanded] = useState(true);
const handleQueryToggle = (index: number) => {
const newSelected = new Set(selectedQueries);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
onSelectionChange(newSelected);
};
const handleQueryEdit = (index: number, field: keyof ResearchQuery, value: any) => {
const updated = [...queries];
updated[index] = { ...updated[index], [field]: value };
onQueriesChange(updated);
};
const handleDeleteQuery = (index: number) => {
const updated = queries.filter((_, idx) => idx !== index);
onQueriesChange(updated);
const newSelected = new Set(selectedQueries);
newSelected.delete(index);
const adjusted = new Set<number>();
newSelected.forEach(idx => {
if (idx > index) {
adjusted.add(idx - 1);
} else if (idx < index) {
adjusted.add(idx);
}
});
onSelectionChange(adjusted);
};
const handleAddQuery = () => {
const newQuery: ResearchQuery = {
query: '',
purpose: 'key_statistics',
provider: 'exa',
priority: 3,
expected_results: '',
};
onQueriesChange([...queries, newQuery]);
const newSelected = new Set(selectedQueries);
newSelected.add(queries.length);
onSelectionChange(newSelected);
};
return (
<Accordion
expanded={expanded}
onChange={() => setExpanded(!expanded)}
sx={{
mb: 2,
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
'&:before': { display: 'none' },
boxShadow: 'none',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#666' }} />}
sx={{
backgroundColor: '#f9fafb',
'&:hover': { backgroundColor: '#f3f4f6' },
}}
>
<Box display="flex" alignItems="center" gap={1} flex={1}>
<SearchIcon sx={{ color: '#666', fontSize: 20 }} />
<Typography variant="subtitle2" fontWeight={600} color="#333">
Research Queries ({queries.length})
</Typography>
<Chip
size="small"
label={`${selectedQueries.size} selected`}
sx={{
ml: 1,
backgroundColor: '#e0f2fe',
color: '#0369a1',
fontSize: '0.7rem',
height: 20,
fontWeight: 500,
}}
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ p: 0, backgroundColor: '#ffffff' }}>
<List dense sx={{ backgroundColor: '#fafafa' }}>
{queries.map((query, idx) => (
<React.Fragment key={idx}>
<QueryEditor
query={query}
index={idx}
isSelected={selectedQueries.has(idx)}
onToggle={() => handleQueryToggle(idx)}
onEdit={(field, value) => handleQueryEdit(idx, field, value)}
onDelete={() => handleDeleteQuery(idx)}
/>
{idx < queries.length - 1 && <Divider />}
</React.Fragment>
))}
<Box sx={{ p: 1 }}>
<Button
fullWidth
variant="outlined"
size="small"
onClick={handleAddQuery}
sx={{
borderStyle: 'dashed',
borderColor: '#d1d5db',
color: '#666',
'&:hover': {
borderColor: '#0ea5e9',
backgroundColor: '#f0f9ff',
},
}}
>
+ Add Query
</Button>
</Box>
</List>
</AccordionDetails>
</Accordion>
);
};

View File

@@ -0,0 +1,164 @@
/**
* TrendsConfigSection Component
*
* Google Trends configuration section with keywords, expected insights, and settings.
*/
import React from 'react';
import {
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
TextField,
List,
ListItem,
ListItemIcon,
ListItemText,
Grid,
Chip,
Tooltip,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
TrendingUp as TrendIcon,
CheckCircle as CheckIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { TrendsConfig } from '../../../types/intent.types';
interface TrendsConfigSectionProps {
trendsConfig: TrendsConfig;
}
export const TrendsConfigSection: React.FC<TrendsConfigSectionProps> = ({
trendsConfig,
}) => {
if (!trendsConfig.enabled) {
return null;
}
return (
<Accordion
defaultExpanded={true}
sx={{
mb: 2,
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
'&:before': { display: 'none' },
boxShadow: 'none',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#666' }} />}
sx={{
backgroundColor: '#f0fdf4',
'&:hover': { backgroundColor: '#dcfce7' },
}}
>
<Box display="flex" alignItems="center" gap={1} flex={1}>
<TrendIcon sx={{ color: '#10b981', fontSize: 20 }} />
<Typography variant="subtitle2" fontWeight={600} color="#333">
Google Trends Analysis
</Typography>
<Chip
size="small"
label="Auto-enabled"
sx={{
ml: 1,
backgroundColor: '#dcfce7',
color: '#166534',
fontSize: '0.7rem',
height: 20,
fontWeight: 500,
}}
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ p: 2, backgroundColor: '#ffffff' }}>
{/* Trends Keywords */}
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
Trends Keywords
</Typography>
<TextField
fullWidth
size="small"
value={trendsConfig.keywords.join(', ')}
disabled
helperText={trendsConfig.keywords_justification}
sx={{
backgroundColor: '#ffffff',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#10b981' },
'&.Mui-focused fieldset': { borderColor: '#10b981' },
},
}}
/>
</Box>
{/* Expected Insights Preview */}
{trendsConfig.expected_insights.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
What Trends Will Uncover:
</Typography>
<List dense sx={{ backgroundColor: '#f9fafb', borderRadius: 1, p: 1 }}>
{trendsConfig.expected_insights.map((insight, idx) => (
<ListItem key={idx} sx={{ py: 0.5, px: 1 }}>
<ListItemIcon sx={{ minWidth: 24 }}>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={insight}
primaryTypographyProps={{ variant: 'caption', color: '#374151' }}
/>
</ListItem>
))}
</List>
</Box>
)}
{/* Settings with Justifications */}
<Box
sx={{
p: 1.5,
backgroundColor: '#f9fafb',
borderRadius: 1,
border: '1px solid #e5e7eb',
}}
>
<Grid container spacing={1}>
<Grid item xs={6}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" gutterBottom>
Timeframe
</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Typography variant="body2" fontWeight={500} color="#333">
{trendsConfig.timeframe}
</Typography>
<Tooltip title={trendsConfig.timeframe_justification} arrow>
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
</Tooltip>
</Box>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" gutterBottom>
Region
</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Typography variant="body2" fontWeight={500} color="#333">
{trendsConfig.geo}
</Typography>
<Tooltip title={trendsConfig.geo_justification} arrow>
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
</Tooltip>
</Box>
</Grid>
</Grid>
</Box>
</AccordionDetails>
</Accordion>
);
};

View File

@@ -0,0 +1,76 @@
/**
* DeliverablesSelector Component
*
* Allows users to select/remove expected deliverables.
*/
import React from 'react';
import {
Box,
Typography,
Chip,
Tooltip,
} from '@mui/material';
import {
Info as InfoIcon,
} from '@mui/icons-material';
import {
ResearchIntent,
ExpectedDeliverable,
DELIVERABLE_DISPLAY,
} from '../../../../types/intent.types';
interface DeliverablesSelectorProps {
intent: ResearchIntent;
onToggle: (deliverable: ExpectedDeliverable) => void;
}
export const DeliverablesSelector: React.FC<DeliverablesSelectorProps> = ({
intent,
onToggle,
}) => {
return (
<Box
mb={2}
sx={{
p: 2,
backgroundColor: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: 1,
}}
>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="caption" color="#666" fontWeight={600}>
What I'll find for you:
</Typography>
<Tooltip title="Click chips to select/remove deliverables">
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af' }} />
</Tooltip>
</Box>
<Box display="flex" flexWrap="wrap" gap={0.5}>
{Object.entries(DELIVERABLE_DISPLAY).map(([key, label]) => {
const isSelected = intent.expected_deliverables.includes(key as ExpectedDeliverable);
return (
<Chip
key={key}
label={label}
size="small"
onClick={() => onToggle(key as ExpectedDeliverable)}
sx={{
backgroundColor: isSelected ? '#dbeafe' : '#ffffff',
border: `1px solid ${isSelected ? '#3b82f6' : '#d1d5db'}`,
color: isSelected ? '#1e40af' : '#374151',
cursor: 'pointer',
'&:hover': {
backgroundColor: isSelected ? '#bfdbfe' : '#f3f4f6',
borderColor: isSelected ? '#2563eb' : '#9ca3af',
},
fontWeight: isSelected ? 600 : 400,
}}
/>
);
})}
</Box>
</Box>
);
};

View File

@@ -0,0 +1,103 @@
/**
* IntentHeader Component
*
* Header section of IntentConfirmationPanel with title, confidence badge, and close button.
*/
import React, { useState } from 'react';
import {
Box,
Typography,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import {
Psychology as BrainIcon,
Close as CloseIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { AnalyzeIntentResponse } from '../../../../types/intent.types';
interface IntentHeaderProps {
intentAnalysis: AnalyzeIntentResponse;
onDismiss: () => void;
}
export const IntentHeader: React.FC<IntentHeaderProps> = ({
intentAnalysis,
onDismiss,
}) => {
const intent = intentAnalysis.intent;
const confidence = intent.confidence;
const isHighConfidence = confidence >= 0.8;
const confidenceReason = (intentAnalysis as any).confidence_reason || '';
const greatExample = (intentAnalysis as any).great_example || '';
return (
<Box
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: isHighConfidence ? '#f0f9ff' : '#fff7ed',
borderBottom: '1px solid #e0e0e0',
}}
>
<Box display="flex" alignItems="center" gap={1.5}>
<BrainIcon sx={{ color: isHighConfidence ? '#10b981' : '#f59e0b' }} />
<Box>
<Typography variant="subtitle1" fontWeight={600} color="#333">
Help ALwrity understand Your Research
</Typography>
<Typography variant="caption" color="#666">
{intentAnalysis.analysis_summary}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Tooltip
title={
<Box sx={{ p: 1, maxWidth: 300 }}>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Confidence: {Math.round(confidence * 100)}%
</Typography>
{confidenceReason && (
<Typography variant="caption" display="block" sx={{ mb: 1 }}>
{confidenceReason}
</Typography>
)}
{greatExample && (
<>
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
Great example would be:
</Typography>
<Typography variant="caption" display="block">
{greatExample}
</Typography>
</>
)}
</Box>
}
arrow
>
<Chip
size="small"
label={`${Math.round(confidence * 100)}% confident`}
color={isHighConfidence ? 'success' : 'warning'}
variant="outlined"
sx={{
backgroundColor: '#ffffff',
cursor: 'help',
}}
icon={<InfoIcon fontSize="small" />}
/>
</Tooltip>
<IconButton size="small" onClick={onDismiss} sx={{ color: '#666' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,108 @@
/**
* IntentSummaryGrid Component
*
* Quick summary grid showing Purpose, Content Type, Depth, and Queries Count.
*/
import React from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
} from '@mui/material';
import {
ResearchIntent,
ResearchPurpose,
ContentOutput,
ResearchDepthLevel,
PURPOSE_DISPLAY,
CONTENT_OUTPUT_DISPLAY,
DEPTH_DISPLAY,
} from '../../../../types/intent.types';
import { EditableField } from '../shared/EditableField';
interface IntentSummaryGridProps {
intent: ResearchIntent;
queriesCount: number;
onUpdateField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
}
export const IntentSummaryGrid: React.FC<IntentSummaryGridProps> = ({
intent,
queriesCount,
onUpdateField,
}) => {
return (
<Grid container spacing={2} mb={2}>
{/* Purpose */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Purpose
</Typography>
<EditableField
field="purpose"
value={intent.purpose}
displayValue={PURPOSE_DISPLAY[intent.purpose as ResearchPurpose] || intent.purpose}
options={Object.entries(PURPOSE_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('purpose', val as ResearchPurpose)}
/>
</CardContent>
</Card>
</Grid>
{/* Content Type */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Creating
</Typography>
<EditableField
field="content_output"
value={intent.content_output}
displayValue={CONTENT_OUTPUT_DISPLAY[intent.content_output as ContentOutput] || intent.content_output}
options={Object.entries(CONTENT_OUTPUT_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('content_output', val as ContentOutput)}
/>
</CardContent>
</Card>
</Grid>
{/* Depth */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Depth
</Typography>
<EditableField
field="depth"
value={intent.depth}
displayValue={DEPTH_DISPLAY[intent.depth as ResearchDepthLevel] || intent.depth}
options={Object.entries(DEPTH_DISPLAY).map(([key, label]) => ({ key, label }))}
onSave={(val) => onUpdateField('depth', val as ResearchDepthLevel)}
/>
</CardContent>
</Card>
</Grid>
{/* Queries Count */}
<Grid item xs={6} sm={3}>
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
<CardContent sx={{ py: 1.5, px: 1.5, '&:last-child': { pb: 1.5 } }}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" mb={0.5}>
Queries
</Typography>
<Typography variant="body2" fontWeight={500} color="#333">
{queriesCount} targeted
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,115 @@
/**
* PrimaryQuestionEditor Component
*
* Editable primary question section.
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
IconButton,
} from '@mui/material';
import {
Edit as EditIcon,
Save as SaveIcon,
Cancel as CancelIcon,
} from '@mui/icons-material';
import { ResearchIntent } from '../../../../types/intent.types';
interface PrimaryQuestionEditorProps {
intent: ResearchIntent;
onUpdate: (value: string) => void;
}
export const PrimaryQuestionEditor: React.FC<PrimaryQuestionEditorProps> = ({
intent,
onUpdate,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(intent.primary_question);
useEffect(() => {
setValue(intent.primary_question);
}, [intent.primary_question]);
const handleSave = () => {
if (value.trim()) {
onUpdate(value.trim());
}
setIsEditing(false);
};
const handleCancel = () => {
setValue(intent.primary_question);
setIsEditing(false);
};
return (
<Box
sx={{
mb: 2,
p: 2,
backgroundColor: '#f0f9ff',
border: '1px solid #bae6fd',
borderRadius: 1,
}}
>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="caption" fontWeight={600} color="#0c4a6e">
Main Question:
</Typography>
{!isEditing && (
<IconButton
size="small"
onClick={() => setIsEditing(true)}
sx={{
color: '#666',
'&:hover': {
backgroundColor: '#e0f2fe',
color: '#0ea5e9',
},
}}
>
<EditIcon fontSize="small" />
</IconButton>
)}
</Box>
{isEditing ? (
<Box display="flex" alignItems="flex-start" gap={1}>
<TextField
fullWidth
multiline
rows={2}
value={value}
onChange={(e) => setValue(e.target.value)}
sx={{ backgroundColor: '#ffffff' }}
autoFocus
/>
<Box display="flex" flexDirection="column" gap={0.5}>
<IconButton
size="small"
onClick={handleSave}
color="primary"
sx={{ backgroundColor: '#e0f2fe' }}
>
<SaveIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={handleCancel}
sx={{ color: '#666' }}
>
<CancelIcon fontSize="small" />
</IconButton>
</Box>
</Box>
) : (
<Typography variant="body2" fontWeight={500} color="#0c4a6e">
{intent.primary_question}
</Typography>
)}
</Box>
);
};

View File

@@ -0,0 +1,284 @@
/**
* ResearchQueriesSection Component
*
* Manages research queries with selection, editing, adding, and deleting.
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
List,
ListItem,
ListItemSecondaryAction,
Checkbox,
TextField,
FormControl,
Select,
MenuItem,
Button,
IconButton,
Divider,
Chip,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
Search as SearchIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import {
ResearchQuery,
ExpectedDeliverable,
DELIVERABLE_DISPLAY,
} from '../../../../types/intent.types';
interface ResearchQueriesSectionProps {
queries: ResearchQuery[];
onQueriesChange: (queries: ResearchQuery[]) => void;
onSelectionChange: (selectedIndices: Set<number>) => void;
}
export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
queries: initialQueries,
onQueriesChange,
onSelectionChange,
}) => {
const [showQueries, setShowQueries] = useState(true);
const [editedQueries, setEditedQueries] = useState<ResearchQuery[]>(initialQueries);
const [selectedQueries, setSelectedQueries] = useState<Set<number>>(
new Set(initialQueries.map((_, idx) => idx))
);
useEffect(() => {
setEditedQueries(initialQueries);
setSelectedQueries(new Set(initialQueries.map((_, idx) => idx)));
}, [initialQueries]);
useEffect(() => {
onQueriesChange(editedQueries);
}, [editedQueries, onQueriesChange]);
useEffect(() => {
onSelectionChange(selectedQueries);
}, [selectedQueries, onSelectionChange]);
const handleQueryToggle = (index: number) => {
const newSelected = new Set(selectedQueries);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
setSelectedQueries(newSelected);
};
const handleQueryEdit = (index: number, field: keyof ResearchQuery, value: any) => {
const updated = [...editedQueries];
updated[index] = { ...updated[index], [field]: value };
setEditedQueries(updated);
};
const handleDeleteQuery = (index: number) => {
const updated = editedQueries.filter((_, idx) => idx !== index);
setEditedQueries(updated);
const newSelected = new Set(selectedQueries);
newSelected.delete(index);
const adjusted = new Set<number>();
newSelected.forEach(idx => {
if (idx > index) {
adjusted.add(idx - 1);
} else if (idx < index) {
adjusted.add(idx);
}
});
setSelectedQueries(adjusted);
};
const handleAddQuery = () => {
const newQuery: ResearchQuery = {
query: '',
purpose: 'key_statistics',
provider: 'exa',
priority: 3,
expected_results: '',
};
setEditedQueries([...editedQueries, newQuery]);
setSelectedQueries(new Set([...selectedQueries, editedQueries.length]));
};
return (
<Accordion
expanded={showQueries}
onChange={() => setShowQueries(!showQueries)}
sx={{
mb: 2,
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
'&:before': { display: 'none' },
boxShadow: 'none',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#666' }} />}
sx={{
backgroundColor: '#f9fafb',
'&:hover': { backgroundColor: '#f3f4f6' },
}}
>
<Box display="flex" alignItems="center" gap={1} flex={1}>
<SearchIcon sx={{ color: '#666', fontSize: 20 }} />
<Typography variant="subtitle2" fontWeight={600} color="#333">
Research Queries ({editedQueries.length})
</Typography>
<Chip
size="small"
label={`${selectedQueries.size} selected`}
sx={{
ml: 1,
backgroundColor: '#e0f2fe',
color: '#0369a1',
fontSize: '0.7rem',
height: 20,
fontWeight: 500,
}}
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ p: 0, backgroundColor: '#ffffff' }}>
<List dense sx={{ backgroundColor: '#fafafa' }}>
{editedQueries.map((query, idx) => (
<React.Fragment key={idx}>
<ListItem
sx={{
backgroundColor: selectedQueries.has(idx) ? '#e0f2fe' : '#ffffff',
borderLeft: selectedQueries.has(idx) ? '3px solid #0ea5e9' : '3px solid transparent',
'&:hover': { backgroundColor: selectedQueries.has(idx) ? '#bae6fd' : '#f9fafb' },
py: 1.5,
}}
>
<Checkbox
checked={selectedQueries.has(idx)}
onChange={() => handleQueryToggle(idx)}
size="small"
sx={{ mr: 1 }}
/>
<Box flex={1}>
<TextField
fullWidth
size="small"
value={query.query}
onChange={(e) => handleQueryEdit(idx, 'query', e.target.value)}
placeholder="Enter research query"
sx={{
mb: 1,
backgroundColor: '#ffffff',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#0ea5e9' },
'&.Mui-focused fieldset': { borderColor: '#0ea5e9' },
},
}}
/>
<Box display="flex" gap={1} flexWrap="wrap">
<FormControl size="small" sx={{ minWidth: 100 }}>
<Select
value={query.provider}
onChange={(e) => handleQueryEdit(idx, 'provider', e.target.value)}
sx={{
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
>
<MenuItem value="exa">Exa</MenuItem>
<MenuItem value="tavily">Tavily</MenuItem>
<MenuItem value="google">Google</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<Select
value={query.purpose}
onChange={(e) => handleQueryEdit(idx, 'purpose', e.target.value)}
sx={{
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
>
{Object.entries(DELIVERABLE_DISPLAY).map(([key, label]) => (
<MenuItem key={key} value={key}>{label}</MenuItem>
))}
</Select>
</FormControl>
<TextField
size="small"
type="number"
value={query.priority}
onChange={(e) => handleQueryEdit(idx, 'priority', parseInt(e.target.value) || 1)}
inputProps={{ min: 1, max: 5 }}
sx={{
width: 90,
backgroundColor: '#ffffff',
fontSize: '0.75rem',
}}
label="Priority"
/>
</Box>
<TextField
fullWidth
size="small"
value={query.expected_results}
onChange={(e) => handleQueryEdit(idx, 'expected_results', e.target.value)}
placeholder="What we expect to find"
sx={{
mt: 1,
backgroundColor: '#ffffff',
fontSize: '0.75rem',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#0ea5e9' },
},
}}
/>
</Box>
<ListItemSecondaryAction>
<IconButton
edge="end"
size="small"
onClick={() => handleDeleteQuery(idx)}
sx={{
color: '#dc2626',
'&:hover': { backgroundColor: '#fee2e2' },
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
{idx < editedQueries.length - 1 && <Divider />}
</React.Fragment>
))}
<ListItem>
<Button
fullWidth
variant="outlined"
size="small"
onClick={handleAddQuery}
sx={{
mt: 1,
borderStyle: 'dashed',
borderColor: '#d1d5db',
color: '#666',
'&:hover': {
borderColor: '#0ea5e9',
backgroundColor: '#f0f9ff',
},
}}
>
+ Add Query
</Button>
</ListItem>
</List>
</AccordionDetails>
</Accordion>
);
};

View File

@@ -0,0 +1,160 @@
/**
* TrendsConfigSection Component
*
* Google Trends configuration section with keywords, expected insights, and settings.
*/
import React from 'react';
import {
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
TextField,
List,
ListItem,
ListItemIcon,
ListItemText,
Grid,
Chip,
Tooltip,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
TrendingUp as TrendIcon,
CheckCircle as CheckIcon,
Info as InfoIcon,
} from '@mui/icons-material';
import { AnalyzeIntentResponse } from '../../../../types/intent.types';
interface TrendsConfigSectionProps {
trendsConfig: NonNullable<AnalyzeIntentResponse['trends_config']>;
}
export const TrendsConfigSection: React.FC<TrendsConfigSectionProps> = ({
trendsConfig,
}) => {
return (
<Accordion
defaultExpanded={true}
sx={{
mb: 2,
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
'&:before': { display: 'none' },
boxShadow: 'none',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#666' }} />}
sx={{
backgroundColor: '#f0fdf4',
'&:hover': { backgroundColor: '#dcfce7' },
}}
>
<Box display="flex" alignItems="center" gap={1} flex={1}>
<TrendIcon sx={{ color: '#10b981', fontSize: 20 }} />
<Typography variant="subtitle2" fontWeight={600} color="#333">
Google Trends Analysis
</Typography>
<Chip
size="small"
label="Auto-enabled"
sx={{
ml: 1,
backgroundColor: '#dcfce7',
color: '#166534',
fontSize: '0.7rem',
height: 20,
fontWeight: 500,
}}
/>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ p: 2, backgroundColor: '#ffffff' }}>
{/* Trends Keywords */}
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
Trends Keywords
</Typography>
<TextField
fullWidth
size="small"
value={trendsConfig.keywords.join(', ')}
disabled
helperText={trendsConfig.keywords_justification}
sx={{
backgroundColor: '#f9fafb',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': { borderColor: '#10b981' },
'&.Mui-focused fieldset': { borderColor: '#10b981' },
},
}}
/>
</Box>
{/* Expected Insights Preview */}
{trendsConfig.expected_insights.length > 0 && (
<Box mb={2}>
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
What Trends Will Uncover:
</Typography>
<List dense sx={{ backgroundColor: '#f9fafb', borderRadius: 1, p: 1 }}>
{trendsConfig.expected_insights.map((insight, idx) => (
<ListItem key={idx} sx={{ py: 0.5, px: 1 }}>
<ListItemIcon sx={{ minWidth: 24 }}>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={insight}
primaryTypographyProps={{ variant: 'caption', color: '#374151' }}
/>
</ListItem>
))}
</List>
</Box>
)}
{/* Settings with Justifications */}
<Box
sx={{
p: 1.5,
backgroundColor: '#f9fafb',
borderRadius: 1,
border: '1px solid #e5e7eb',
}}
>
<Grid container spacing={1}>
<Grid item xs={6}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" gutterBottom>
Timeframe
</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Typography variant="body2" fontWeight={500} color="#333">
{trendsConfig.timeframe}
</Typography>
<Tooltip title={trendsConfig.timeframe_justification} arrow>
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
</Tooltip>
</Box>
</Grid>
<Grid item xs={6}>
<Typography variant="caption" color="#666" fontWeight={500} display="block" gutterBottom>
Region
</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Typography variant="body2" fontWeight={500} color="#333">
{trendsConfig.geo}
</Typography>
<Tooltip title={trendsConfig.geo_justification} arrow>
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
</Tooltip>
</Box>
</Grid>
</Grid>
</Box>
</AccordionDetails>
</Accordion>
);
};

View File

@@ -0,0 +1,23 @@
/**
* IntentConfirmationPanel Module
*
* Refactored modular components for intent confirmation panel.
* Each component handles a specific responsibility.
*/
export { IntentConfirmationPanel } from './IntentConfirmationPanel';
export type { IntentConfirmationPanelProps } from './IntentConfirmationPanel';
// Export sub-components for potential reuse
export { LoadingState } from './LoadingState';
export { EditableField } from './EditableField';
export { IntentConfirmationHeader } from './IntentConfirmationHeader';
export { PrimaryQuestionEditor } from './PrimaryQuestionEditor';
export { IntentSummaryGrid } from './IntentSummaryGrid';
export { DeliverablesSelector } from './DeliverablesSelector';
export { QueryEditor } from './QueryEditor';
export { ResearchQueriesSection } from './ResearchQueriesSection';
export { TrendsConfigSection } from './TrendsConfigSection';
export { AdvancedProviderOptionsSection } from './AdvancedProviderOptionsSection';
export { ExpandableDetails } from './ExpandableDetails';
export { ActionButtons } from './ActionButtons';

View File

@@ -0,0 +1,107 @@
/**
* EditableField Component
*
* Reusable component for inline editing of fields with save/cancel functionality.
*/
import React, { useState } from 'react';
import {
Box,
Typography,
IconButton,
TextField,
FormControl,
Select,
MenuItem,
} from '@mui/material';
import {
Edit as EditIcon,
Save as SaveIcon,
Cancel as CancelIcon,
} from '@mui/icons-material';
interface EditableFieldProps {
field: string;
value: any;
displayValue: string;
options?: Array<{ key: string; label: string }>;
onSave: (newValue: any) => void;
}
export const EditableField: React.FC<EditableFieldProps> = ({
field,
value,
displayValue,
options,
onSave,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const handleSave = () => {
onSave(editValue);
setIsEditing(false);
};
const handleCancel = () => {
setEditValue(value);
setIsEditing(false);
};
return (
<Box display="flex" alignItems="center" gap={0.5}>
{isEditing ? (
<Box display="flex" alignItems="center" gap={0.5} flex={1}>
{options ? (
<FormControl size="small" fullWidth>
<Select
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
sx={{ backgroundColor: '#ffffff' }}
autoFocus
>
{options.map(opt => (
<MenuItem key={opt.key} value={opt.key}>{opt.label}</MenuItem>
))}
</Select>
</FormControl>
) : (
<TextField
size="small"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
fullWidth
sx={{ backgroundColor: '#ffffff' }}
autoFocus
/>
)}
<IconButton size="small" onClick={handleSave} color="primary">
<SaveIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={handleCancel} color="inherit">
<CancelIcon fontSize="small" />
</IconButton>
</Box>
) : (
<>
<Typography variant="body2" fontWeight={500} color="#333" sx={{ flex: 1 }}>
{displayValue}
</Typography>
<IconButton
size="small"
onClick={() => setIsEditing(true)}
sx={{
color: '#666',
'&:hover': {
backgroundColor: '#f3f4f6',
color: '#0ea5e9',
},
}}
>
<EditIcon fontSize="small" />
</IconButton>
</>
)}
</Box>
);
};

View File

@@ -0,0 +1,40 @@
/**
* LoadingState Component
*
* Loading indicator for intent analysis.
*/
import React from 'react';
import {
Box,
Typography,
Paper,
CircularProgress,
} from '@mui/material';
export const LoadingState: React.FC = () => {
return (
<Paper
elevation={0}
sx={{
p: 3,
mt: 2,
borderRadius: 2,
border: '1px solid #e0e0e0',
backgroundColor: '#ffffff',
}}
>
<Box display="flex" alignItems="center" gap={2}>
<CircularProgress size={24} />
<Box>
<Typography variant="subtitle1" fontWeight={600} color="#333">
🧠 Analyzing your research intent...
</Typography>
<Typography variant="body2" color="#666">
AI is understanding what you want to accomplish
</Typography>
</Box>
</Box>
</Paper>
);
};

View File

@@ -26,6 +26,12 @@ import {
AccordionSummary,
AccordionDetails,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from '@mui/material';
import {
CheckCircle as CheckIcon,
@@ -37,18 +43,27 @@ import {
OpenInNew as OpenIcon,
ExpandMore as ExpandMoreIcon,
Warning as WarningIcon,
Public as PublicIcon,
Search as SearchIcon,
ArrowUpward as ArrowUpIcon,
ArrowDownward as ArrowDownIcon,
} from '@mui/icons-material';
import {
IntentDrivenResearchResponse,
DELIVERABLE_DISPLAY,
} from '../../types/intent.types';
import { TrendsChart } from './TrendsChart';
import { TrendsExport } from './TrendsExport';
interface IntentResultsDisplayProps {
result: IntentDrivenResearchResponse;
hideHeader?: boolean;
}
export const IntentResultsDisplay: React.FC<IntentResultsDisplayProps> = ({ result }) => {
export const IntentResultsDisplay: React.FC<IntentResultsDisplayProps> = ({ result, hideHeader = false }) => {
const [tabIndex, setTabIndex] = useState(0);
const [topicsTabIndex, setTopicsTabIndex] = useState(0);
const [queriesTabIndex, setQueriesTabIndex] = useState(0);
// Build available tabs based on what we have
const tabs = [
@@ -316,49 +331,299 @@ export const IntentResultsDisplay: React.FC<IntentResultsDisplayProps> = ({ resu
{/* 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}
<Box>
{/* Google Trends Data Section */}
{result.google_trends_data && (
<Box mb={3}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendIcon color="primary" />
Google Trends Analysis
</Typography>
<TrendsExport
trendsData={result.google_trends_data}
aiTrends={result.trends}
keywords={result.google_trends_data.keywords}
/>
</Box>
{/* Interest Over Time - Advanced Chart */}
{result.google_trends_data.interest_over_time.length > 0 && (
<Card variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="subtitle2" fontWeight={600}>
Interest Over Time
</Typography>
<Chip
size="small"
label={`${result.google_trends_data.timeframe}${result.google_trends_data.geo}`}
sx={{ backgroundColor: '#f0f9ff', color: '#0369a1' }}
/>
</Box>
<Box data-trends-chart>
<TrendsChart
data={result.google_trends_data}
height={300}
/>
</Box>
</CardContent>
</Card>
)}
{/* Interest by Region */}
{result.google_trends_data.interest_by_region.length > 0 && (
<Card variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle2" fontWeight={600} gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PublicIcon fontSize="small" />
Interest by Region
</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}
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Region</TableCell>
<TableCell align="right">Interest</TableCell>
</TableRow>
</TableHead>
<TableBody>
{result.google_trends_data.interest_by_region.slice(0, 10).map((region: any, idx: number) => {
const geoKey = Object.keys(region).find(k => k.includes('geo') || k.includes('name'));
const regionName = region.geoName || (geoKey ? region[geoKey] : null) || 'Unknown';
const value = Object.values(region).find(v => typeof v === 'number' && v !== null) as number || 0;
return (
<TableRow key={idx}>
<TableCell>{regionName}</TableCell>
<TableCell align="right">
<Box display="flex" alignItems="center" justifyContent="flex-end" gap={1}>
<Box
sx={{
width: 60,
height: 8,
backgroundColor: '#e5e7eb',
borderRadius: 1,
overflow: 'hidden',
}}
>
<Box
sx={{
width: `${value}%`,
height: '100%',
backgroundColor: '#10b981',
}}
/>
</Box>
<Typography variant="body2" fontWeight={500}>
{value}
</Typography>
</Box>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
)}
{/* Related Topics */}
{(result.google_trends_data.related_topics.top.length > 0 || result.google_trends_data.related_topics.rising.length > 0) && (
<Card variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
Related Topics
</Typography>
)}
{trend.timeline && (
<Typography variant="caption" color="text.secondary">
Timeline: {trend.timeline}
<Tabs
value={topicsTabIndex}
onChange={(_, newValue) => setTopicsTabIndex(newValue)}
sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
>
<Tab label={`Top (${result.google_trends_data.related_topics.top.length})`} />
<Tab label={`Rising (${result.google_trends_data.related_topics.rising.length})`} />
</Tabs>
{topicsTabIndex === 0 && (
<Box display="flex" flexWrap="wrap" gap={1}>
{result.google_trends_data.related_topics.top.slice(0, 15).map((topic: any, idx: number) => {
const topicTitle = topic.topic_title || topic.title || topic[Object.keys(topic)[0]] || 'Unknown';
const value = topic.value || '';
return (
<Chip
key={idx}
label={value ? `${topicTitle} (${value})` : topicTitle}
size="small"
sx={{ backgroundColor: '#e0f2fe', color: '#0369a1' }}
/>
);
})}
</Box>
)}
{topicsTabIndex === 1 && (
<Box display="flex" flexWrap="wrap" gap={1}>
{result.google_trends_data.related_topics.rising.slice(0, 15).map((topic: any, idx: number) => {
const topicTitle = topic.topic_title || topic.title || topic[Object.keys(topic)[0]] || 'Unknown';
const value = topic.value || '';
return (
<Chip
key={idx}
label={value ? `${topicTitle} (${value})` : topicTitle}
size="small"
icon={<ArrowUpIcon />}
sx={{ backgroundColor: '#dcfce7', color: '#166534' }}
/>
);
})}
</Box>
)}
</CardContent>
</Card>
)}
{/* Related Queries */}
{(result.google_trends_data.related_queries.top.length > 0 || result.google_trends_data.related_queries.rising.length > 0) && (
<Card variant="outlined" sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle2" fontWeight={600} gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon fontSize="small" />
Related Queries
</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>
<Tabs
value={queriesTabIndex}
onChange={(_, newValue) => setQueriesTabIndex(newValue)}
sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
>
<Tab label={`Top (${result.google_trends_data.related_queries.top.length})`} />
<Tab label={`Rising (${result.google_trends_data.related_queries.rising.length})`} />
</Tabs>
{queriesTabIndex === 0 && (
<List dense>
{result.google_trends_data.related_queries.top.slice(0, 15).map((query: any, idx: number) => {
const queryText = query.query || query[Object.keys(query)[0]] || 'Unknown';
return (
<ListItem key={idx} sx={{ py: 0.5, '&:hover': { backgroundColor: '#f9fafb' } }}>
<ListItemText
primary={queryText}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
);
})}
</List>
)}
{queriesTabIndex === 1 && (
<List dense>
{result.google_trends_data.related_queries.rising.slice(0, 15).map((query: any, idx: number) => {
const queryText = query.query || query[Object.keys(query)[0]] || 'Unknown';
return (
<ListItem key={idx} sx={{ py: 0.5, '&:hover': { backgroundColor: '#f9fafb' } }}>
<ListItemIcon sx={{ minWidth: 24 }}>
<ArrowUpIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={queryText}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
);
})}
</List>
)}
</CardContent>
</Card>
)}
</Box>
)}
{/* AI-Extracted Trends */}
{result.trends.length > 0 && (
<Box>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IdeaIcon color="primary" />
AI-Extracted Trends
</Typography>
<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'}
/>
{trend.interest_score !== undefined && (
<Chip
size="small"
label={`Interest: ${Math.round(trend.interest_score)}`}
sx={{ backgroundColor: '#fef3c7', color: '#92400e' }}
/>
)}
</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>
)}
{trend.regional_interest && Object.keys(trend.regional_interest).length > 0 && (
<Box mt={1}>
<Typography variant="caption" color="text.secondary" display="block" gutterBottom>
Top Regions:
</Typography>
<Box display="flex" flexWrap="wrap" gap={0.5}>
{Object.entries(trend.regional_interest)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([region, score]) => (
<Chip
key={region}
label={`${region}: ${Math.round(score)}`}
size="small"
variant="outlined"
/>
))}
</Box>
</Box>
)}
<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>
</Box>
)}
{/* No trends message */}
{result.trends.length === 0 && !result.google_trends_data && (
<Alert severity="info">
No trends data available. Trends will appear here when your research includes trend analysis.
</Alert>
)}
</Box>
)}
{/* Sources Tab */}

View File

@@ -1,19 +1,31 @@
import React, { useState, useEffect } from 'react';
import { Tooltip, CircularProgress } from '@mui/material';
import { Psychology as BrainIcon, Settings as SettingsIcon, Info as InfoIcon } from '@mui/icons-material';
interface ResearchInputContainerProps {
keywords: string[];
placeholder: string;
onKeywordsChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
// New props for unified intent & options
onIntentAndOptions?: () => void;
isAnalyzingIntent?: boolean;
hasIntentAnalysis?: boolean;
intentConfidence?: number;
}
export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
keywords,
placeholder,
onKeywordsChange,
onIntentAndOptions,
isAnalyzingIntent = false,
hasIntentAnalysis = false,
intentConfidence = 0,
}) => {
const [inputValue, setInputValue] = useState('');
const [wordCount, setWordCount] = useState(0);
const MAX_WORDS = 1000;
const MIN_WORDS_FOR_INTENT = 2; // Enable button after 2+ words
// Initialize input value from keywords only on mount or when keywords are cleared
useEffect(() => {
@@ -112,17 +124,96 @@ export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
}}
/>
{/* Word count indicator */}
{/* Bottom bar with word count and Intent & Options button */}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: '8px',
fontSize: '12px',
color: wordCount >= MAX_WORDS ? '#ef4444' : '#64748b',
fontWeight: wordCount >= MAX_WORDS ? '600' : '400',
paddingTop: '12px',
borderTop: '1px solid rgba(14, 165, 233, 0.1)',
marginTop: '8px',
}}>
{wordCount} / {MAX_WORDS} words
{/* Word count indicator */}
<div style={{
fontSize: '12px',
color: wordCount >= MAX_WORDS ? '#ef4444' : '#64748b',
fontWeight: wordCount >= MAX_WORDS ? '600' : '400',
}}>
{wordCount} / {MAX_WORDS} words
</div>
{/* Intent & Options Button */}
<Tooltip
title={
wordCount < MIN_WORDS_FOR_INTENT
? `Enter at least ${MIN_WORDS_FOR_INTENT} words to analyze intent`
: hasIntentAnalysis
? `Intent analyzed with ${Math.round(intentConfidence * 100)}% confidence. Click to re-analyze.`
: 'Let AI understand what you want to accomplish and configure optimal settings'
}
arrow
placement="top"
>
<span>
<button
onClick={onIntentAndOptions}
disabled={wordCount < MIN_WORDS_FOR_INTENT || isAnalyzingIntent}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 14px',
background: wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent
? hasIntentAnalysis
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'rgba(100, 116, 139, 0.15)',
color: wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent ? 'white' : '#94a3b8',
border: 'none',
borderRadius: '10px',
cursor: wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent ? 'pointer' : 'not-allowed',
fontSize: '13px',
fontWeight: '600',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent
? '0 2px 8px rgba(102, 126, 234, 0.3)'
: 'none',
}}
onMouseEnter={(e) => {
if (wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent) {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
}
}}
onMouseLeave={(e) => {
if (wordCount >= MIN_WORDS_FOR_INTENT && !isAnalyzingIntent) {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(102, 126, 234, 0.3)';
}
}}
>
{isAnalyzingIntent ? (
<>
<CircularProgress size={14} sx={{ color: 'inherit' }} />
Analyzing...
</>
) : hasIntentAnalysis ? (
<>
<BrainIcon sx={{ fontSize: 16 }} />
Intent Ready
</>
) : (
<>
<BrainIcon sx={{ fontSize: 16 }} />
Intent & Options
<Tooltip title="AI analyzes your research goals and configures optimal Exa/Tavily settings automatically" arrow>
<InfoIcon sx={{ fontSize: 12, opacity: 0.7 }} />
</Tooltip>
</>
)}
</button>
</span>
</Tooltip>
</div>
</div>
);

View File

@@ -0,0 +1,141 @@
/**
* ResearchInputHeader Component
*
* Header section with title, personalization indicator, and action buttons (Advanced toggle, Upload)
*/
import React from 'react';
import { PersonalizationIndicator } from './PersonalizationIndicator';
interface ResearchInputHeaderProps {
hasPersona: boolean;
advanced: boolean;
onAdvancedChange: (advanced: boolean) => void;
onFileUpload: () => void;
}
export const ResearchInputHeader: React.FC<ResearchInputHeaderProps> = ({
hasPersona,
advanced,
onAdvancedChange,
onFileUpload,
}) => {
return (
<div style={{
marginBottom: '12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
}}>
<label style={{
fontSize: '15px',
fontWeight: '600',
color: '#0c4a6e',
display: 'flex',
alignItems: 'center',
gap: '8px',
flex: '1',
}}>
<span style={{ fontSize: '20px' }}>🔍</span>
Research Topic & Keywords
<PersonalizationIndicator
type="placeholder"
hasPersona={hasPersona}
source={hasPersona ? "from your research persona" : undefined}
/>
</label>
{/* Advanced Toggle and Upload Button */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
{/* Advanced Toggle */}
<label
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer',
padding: '6px 10px',
borderRadius: '8px',
border: `1px solid ${advanced ? 'rgba(14, 165, 233, 0.3)' : 'rgba(15, 23, 42, 0.1)'}`,
background: advanced
? 'linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)'
: '#ffffff',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
fontSize: '11px',
fontWeight: '600',
color: advanced ? '#0369a1' : '#475569',
boxShadow: advanced ? '0 1px 3px rgba(14, 165, 233, 0.12)' : '0 1px 2px rgba(0, 0, 0, 0.04)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = advanced ? 'rgba(14, 165, 233, 0.4)' : 'rgba(15, 23, 42, 0.15)';
e.currentTarget.style.boxShadow = advanced
? '0 2px 4px rgba(14, 165, 233, 0.18)'
: '0 1px 3px rgba(0, 0, 0, 0.06)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = advanced ? 'rgba(14, 165, 233, 0.3)' : 'rgba(15, 23, 42, 0.1)';
e.currentTarget.style.boxShadow = advanced
? '0 1px 3px rgba(14, 165, 233, 0.12)'
: '0 1px 2px rgba(0, 0, 0, 0.04)';
}}
title="Enable advanced research options (Exa and Tavily configurations)"
>
<input
type="checkbox"
checked={advanced}
onChange={(e) => onAdvancedChange(e.target.checked)}
style={{
width: '14px',
height: '14px',
cursor: 'pointer',
accentColor: '#0ea5e9',
}}
/>
<span>Advanced</span>
</label>
{/* Upload Button */}
<button
onClick={onFileUpload}
type="button"
style={{
padding: '6px 10px',
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.25)',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '11px',
fontWeight: '600',
color: '#0369a1',
display: 'flex',
alignItems: 'center',
gap: '5px',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 1px 2px rgba(14, 165, 233, 0.12)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.35)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 2px 4px rgba(14, 165, 233, 0.18)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)';
e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.25)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 1px 2px rgba(14, 165, 233, 0.12)';
}}
title="Upload Document"
>
<span style={{ fontSize: '13px' }}>📎</span>
<span>Upload</span>
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,217 @@
/**
* SmartResearchInfo Component
*
* Tooltip/modal explaining what Smart Research does and why it's useful.
*/
import React, { useState } from 'react';
import {
Box,
Typography,
Tooltip,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
List,
ListItem,
ListItemIcon,
ListItemText,
Chip,
Divider,
} from '@mui/material';
import {
Info as InfoIcon,
Psychology as BrainIcon,
CheckCircle as CheckIcon,
TrendingUp as TrendIcon,
FormatQuote as QuoteIcon,
BarChart as StatsIcon,
School as CaseStudyIcon,
Close as CloseIcon,
} from '@mui/icons-material';
interface SmartResearchInfoProps {
variant?: 'tooltip' | 'button' | 'icon';
}
export const SmartResearchInfo: React.FC<SmartResearchInfoProps> = ({ variant = 'icon' }) => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const tooltipContent = (
<Box sx={{ maxWidth: 400 }}>
<Typography variant="subtitle2" fontWeight={600} gutterBottom>
🧠 Smart Research
</Typography>
<Typography variant="body2">
AI understands what you want to accomplish and finds exactly what you need:
statistics, expert quotes, case studies, trends, and more all organized
by deliverable type instead of generic search results.
</Typography>
</Box>
);
if (variant === 'tooltip') {
return (
<>
<Tooltip title={tooltipContent} arrow placement="top">
<IconButton size="small" onClick={handleOpen} sx={{ ml: 1 }}>
<InfoIcon fontSize="small" color="primary" />
</IconButton>
</Tooltip>
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>
<Box display="flex" alignItems="center" gap={1}>
<BrainIcon color="primary" />
<Typography variant="h6">What is Smart Research?</Typography>
</Box>
</DialogTitle>
<DialogContent>
<DialogContentText>
{tooltipContent}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Got it</Button>
</DialogActions>
</Dialog>
</>
);
}
return (
<>
<Tooltip title="Learn about Smart Research" arrow>
<IconButton
size="small"
onClick={handleOpen}
sx={{
ml: 0.5,
color: 'primary.main',
'&:hover': { bgcolor: 'primary.light', color: 'white' },
}}
>
<InfoIcon fontSize="small" />
</IconButton>
</Tooltip>
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={1}>
<BrainIcon color="primary" sx={{ fontSize: 32 }} />
<Typography variant="h5" fontWeight={600}>
🧠 Smart Research
</Typography>
</Box>
<IconButton onClick={handleClose} size="small">
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Typography variant="body1" color="text.secondary" paragraph>
Traditional research gives you links to sift through. Smart Research understands
what you want to accomplish and delivers exactly what you need organized and ready to use.
</Typography>
<Divider sx={{ my: 3 }} />
<Typography variant="h6" gutterBottom fontWeight={600}>
How it works:
</Typography>
<List>
<ListItem>
<ListItemIcon>
<BrainIcon color="primary" />
</ListItemIcon>
<ListItemText
primary="1. AI Analyzes Your Intent"
secondary="Understands what you want to accomplish, what questions need answering, and what deliverables you expect"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<TrendIcon color="primary" />
</ListItemIcon>
<ListItemText
primary="2. Generates Targeted Queries"
secondary="Creates multiple focused queries, each targeting a specific deliverable (statistics, quotes, case studies, etc.)"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<CheckIcon color="primary" />
</ListItemIcon>
<ListItemText
primary="3. Extracts Exactly What You Need"
secondary="Analyzes results through the lens of your intent, extracting statistics, expert quotes, case studies, trends, and more"
/>
</ListItem>
</List>
<Divider sx={{ my: 3 }} />
<Typography variant="h6" gutterBottom fontWeight={600}>
What you'll get:
</Typography>
<Box display="flex" flexWrap="wrap" gap={1} mb={2}>
<Chip icon={<StatsIcon />} label="Statistics with Citations" color="primary" variant="outlined" />
<Chip icon={<QuoteIcon />} label="Expert Quotes" color="primary" variant="outlined" />
<Chip icon={<CaseStudyIcon />} label="Case Studies" color="primary" variant="outlined" />
<Chip icon={<TrendIcon />} label="Trends Analysis" color="primary" variant="outlined" />
<Chip label="Best Practices" color="primary" variant="outlined" />
<Chip label="Comparisons" color="primary" variant="outlined" />
<Chip label="Step-by-Step Guides" color="primary" variant="outlined" />
<Chip label="Pros & Cons" color="primary" variant="outlined" />
</Box>
<Divider sx={{ my: 3 }} />
<Typography variant="h6" gutterBottom fontWeight={600}>
Benefits:
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary="No more sifting through generic search results" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary="Get exactly what you need, organized by deliverable type" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary="Save time with AI-powered extraction and analysis" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText primary="Content-ready outputs: statistics, quotes, case studies, trends" />
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="contained" color="primary">
Got it!
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default SmartResearchInfo;

View File

@@ -0,0 +1,206 @@
/**
* TrendsChart Component
*
* Advanced chart visualization for Google Trends data using Recharts.
* Displays interest over time with interactive tooltips and zoom capabilities.
*/
import React, { useMemo } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import { Box, Typography, useTheme } from '@mui/material';
import { GoogleTrendsData } from '../../types/intent.types';
interface TrendsChartProps {
data: GoogleTrendsData;
height?: number;
showAverage?: boolean;
}
export const TrendsChart: React.FC<TrendsChartProps> = ({
data,
height = 300,
showAverage = true,
}) => {
const theme = useTheme();
// Transform data for Recharts
const chartData = useMemo(() => {
if (!data.interest_over_time || data.interest_over_time.length === 0) {
return [];
}
return data.interest_over_time.map((point: any) => {
const result: any = {};
// Extract date
const dateKey = Object.keys(point).find(k =>
k.toLowerCase().includes('date') || k === 'date'
);
if (dateKey && point[dateKey]) {
const dateValue = point[dateKey];
result.date = typeof dateValue === 'string'
? new Date(dateValue).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
: dateValue;
result.fullDate = typeof dateValue === 'string' ? dateValue : dateValue;
}
// Extract interest values for each keyword
data.keywords.forEach((keyword, idx) => {
// Find the value column for this keyword
const valueKey = Object.keys(point).find(k => {
const val = point[k];
return typeof val === 'number' && val !== null && val !== undefined && k !== 'isPartial';
});
if (valueKey) {
result[keyword] = point[valueKey];
} else {
// Try to find by index if keywords match column order
const numericKeys = Object.keys(point).filter(k => {
const val = point[k];
return typeof val === 'number' && val !== null && val !== undefined && k !== 'isPartial';
});
if (numericKeys[idx]) {
result[keyword] = point[numericKeys[idx]];
}
}
});
// Add isPartial flag if available
if (point.isPartial !== undefined) {
result.isPartial = point.isPartial;
}
return result;
}).filter(item => item.date); // Filter out items without dates
}, [data.interest_over_time, data.keywords]);
// Calculate average if needed
const averageValue = useMemo(() => {
if (!showAverage || chartData.length === 0) return null;
const allValues: number[] = [];
chartData.forEach((point: any) => {
data.keywords.forEach(keyword => {
if (point[keyword] !== undefined && point[keyword] !== null) {
allValues.push(point[keyword]);
}
});
});
if (allValues.length === 0) return null;
return allValues.reduce((sum, val) => sum + val, 0) / allValues.length;
}, [chartData, data.keywords, showAverage]);
// Color palette for multiple keywords
const colors = [
theme.palette.primary.main,
theme.palette.secondary.main,
'#10b981', // green
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // purple
];
if (chartData.length === 0) {
return (
<Box sx={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
</Box>
);
}
return (
<Box sx={{ width: '100%', height }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke={theme.palette.divider} />
<XAxis
dataKey="date"
stroke={theme.palette.text.secondary}
style={{ fontSize: '12px' }}
angle={-45}
textAnchor="end"
height={60}
interval="preserveStartEnd"
/>
<YAxis
stroke={theme.palette.text.secondary}
style={{ fontSize: '12px' }}
domain={[0, 100]}
label={{
value: 'Interest (0-100)',
angle: -90,
position: 'insideLeft',
style: { fontSize: '12px', fill: theme.palette.text.secondary }
}}
/>
<Tooltip
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: '8px',
}}
formatter={(value: any, name: string) => {
if (typeof value === 'number') {
return [`${Math.round(value)}`, name];
}
return [value, name];
}}
labelFormatter={(label) => `Date: ${label}`}
/>
{data.keywords.length > 1 && (
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="line"
/>
)}
{showAverage && averageValue !== null && (
<ReferenceLine
y={averageValue}
stroke={theme.palette.text.secondary}
strokeDasharray="5 5"
label={{
value: `Avg: ${Math.round(averageValue)}`,
position: 'right',
style: { fontSize: '11px', fill: theme.palette.text.secondary }
}}
/>
)}
{data.keywords.map((keyword, idx) => (
<Line
key={keyword}
type="monotone"
dataKey={keyword}
stroke={colors[idx % colors.length]}
strokeWidth={2}
dot={{ r: 3 }}
activeDot={{ r: 5 }}
name={keyword}
/>
))}
</LineChart>
</ResponsiveContainer>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block', textAlign: 'center' }}>
Values are normalized (0-100) where 100 is peak popularity
{data.timeframe && ` • Timeframe: ${data.timeframe}`}
{data.geo && ` • Region: ${data.geo}`}
</Typography>
</Box>
);
};

View File

@@ -0,0 +1,284 @@
/**
* TrendsExport Component
*
* Provides export functionality for Google Trends data.
* Supports CSV export and image export (chart screenshot).
*/
import React, { useRef, useState } from 'react';
import {
Box,
Button,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Tooltip,
CircularProgress,
} from '@mui/material';
import {
Download as DownloadIcon,
FileDownload as FileDownloadIcon,
Image as ImageIcon,
TableChart as TableChartIcon,
} from '@mui/icons-material';
import { GoogleTrendsData, TrendAnalysis } from '../../types/intent.types';
interface TrendsExportProps {
trendsData: GoogleTrendsData;
aiTrends?: TrendAnalysis[];
keywords: string[];
}
export const TrendsExport: React.FC<TrendsExportProps> = ({
trendsData,
aiTrends = [],
keywords,
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [exporting, setExporting] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// Export to CSV
const exportToCSV = () => {
setExporting(true);
try {
// Prepare CSV data
const csvRows: string[] = [];
// Header
csvRows.push('Google Trends Data Export');
csvRows.push(`Keywords: ${keywords.join(', ')}`);
csvRows.push(`Timeframe: ${trendsData.timeframe}`);
csvRows.push(`Region: ${trendsData.geo}`);
csvRows.push(`Exported: ${new Date().toISOString()}`);
csvRows.push('');
// Interest Over Time
if (trendsData.interest_over_time.length > 0) {
csvRows.push('Interest Over Time');
const headers = ['Date', ...keywords];
csvRows.push(headers.join(','));
trendsData.interest_over_time.forEach((point: any) => {
const dateKey = Object.keys(point).find(k =>
k.toLowerCase().includes('date') || k === 'date'
);
const date = dateKey ? point[dateKey] : '';
const values = keywords.map(keyword => {
const valueKey = Object.keys(point).find(k => {
const val = point[k];
return typeof val === 'number' && val !== null && val !== undefined && k !== 'isPartial';
});
return valueKey ? point[valueKey] : '';
});
csvRows.push([date, ...values].join(','));
});
csvRows.push('');
}
// Interest by Region
if (trendsData.interest_by_region.length > 0) {
csvRows.push('Interest by Region');
csvRows.push('Region,Interest');
trendsData.interest_by_region.forEach((region: any) => {
const geoKey = Object.keys(region).find(k => k.includes('geo') || k.includes('name'));
const regionName = region.geoName || (geoKey ? region[geoKey] : null) || 'Unknown';
const value = Object.values(region).find(v => typeof v === 'number' && v !== null) as number || 0;
csvRows.push(`${regionName},${value}`);
});
csvRows.push('');
}
// Related Topics
if (trendsData.related_topics.top.length > 0 || trendsData.related_topics.rising.length > 0) {
csvRows.push('Related Topics');
csvRows.push('Type,Topic,Value');
trendsData.related_topics.top.forEach((topic: any) => {
const topicTitle = topic.topic_title || topic.title || topic[Object.keys(topic)[0]] || 'Unknown';
const value = topic.value || '';
csvRows.push(`Top,${topicTitle},${value}`);
});
trendsData.related_topics.rising.forEach((topic: any) => {
const topicTitle = topic.topic_title || topic.title || topic[Object.keys(topic)[0]] || 'Unknown';
const value = topic.value || '';
csvRows.push(`Rising,${topicTitle},${value}`);
});
csvRows.push('');
}
// Related Queries
if (trendsData.related_queries.top.length > 0 || trendsData.related_queries.rising.length > 0) {
csvRows.push('Related Queries');
csvRows.push('Type,Query');
trendsData.related_queries.top.forEach((query: any) => {
const queryText = query.query || query[Object.keys(query)[0]] || 'Unknown';
csvRows.push(`Top,${queryText}`);
});
trendsData.related_queries.rising.forEach((query: any) => {
const queryText = query.query || query[Object.keys(query)[0]] || 'Unknown';
csvRows.push(`Rising,${queryText}`);
});
csvRows.push('');
}
// AI-Extracted Trends
if (aiTrends.length > 0) {
csvRows.push('AI-Extracted Trends');
csvRows.push('Trend,Direction,Impact,Timeline,Interest Score');
aiTrends.forEach(trend => {
csvRows.push([
trend.trend,
trend.direction,
trend.impact || '',
trend.timeline || '',
trend.interest_score ? Math.round(trend.interest_score).toString() : '',
].join(','));
});
}
// Create and download CSV
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `google-trends-${keywords.join('-')}-${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('Error exporting CSV:', error);
alert('Failed to export CSV. Please try again.');
} finally {
setExporting(false);
handleClose();
}
};
// Export chart as image (requires html2canvas)
const exportChartAsImage = async () => {
setExporting(true);
try {
// Dynamically import html2canvas if available
let html2canvas: ((element: HTMLElement, options?: any) => Promise<HTMLCanvasElement>);
try {
// Dynamic import - html2canvas may not be installed, we handle this gracefully
const html2canvasModule = await import('html2canvas');
html2canvas = html2canvasModule.default as (element: HTMLElement, options?: any) => Promise<HTMLCanvasElement>;
} catch (importError) {
alert('Image export requires html2canvas package. Please install it: npm install html2canvas');
setExporting(false);
handleClose();
return;
}
const chartElement = document.querySelector('[data-trends-chart]');
if (!chartElement) {
alert('Chart not found. Please ensure the chart is visible.');
setExporting(false);
handleClose();
return;
}
const canvas = await html2canvas(chartElement as HTMLElement, {
backgroundColor: '#ffffff',
scale: 2,
logging: false,
});
canvas.toBlob((blob: Blob | null) => {
if (!blob) {
alert('Failed to generate image.');
return;
}
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `trends-chart-${keywords.join('-')}-${new Date().toISOString().split('T')[0]}.png`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
} catch (error) {
console.error('Error exporting image:', error);
// If html2canvas is not installed, show helpful message
if (error instanceof Error && error.message.includes('Cannot find module')) {
alert('Image export requires html2canvas package. Please install it: npm install html2canvas');
} else {
alert('Failed to export image. Please try again.');
}
} finally {
setExporting(false);
handleClose();
}
};
return (
<Box>
<Tooltip title="Export trends data">
<Button
size="small"
variant="outlined"
startIcon={exporting ? <CircularProgress size={16} /> : <DownloadIcon />}
onClick={handleClick}
disabled={exporting}
sx={{
borderColor: '#e5e7eb',
color: '#374151',
'&:hover': {
borderColor: '#0ea5e9',
backgroundColor: '#f0f9ff',
},
}}
>
Export
</Button>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<MenuItem onClick={exportToCSV} disabled={exporting}>
<ListItemIcon>
<TableChartIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Export as CSV</ListItemText>
</MenuItem>
<MenuItem onClick={exportChartAsImage} disabled={exporting}>
<ListItemIcon>
<ImageIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Export Chart as Image</ListItemText>
</MenuItem>
</Menu>
</Box>
);
};

View File

@@ -0,0 +1,51 @@
/**
* Hook for keyword expansion with persona support
*/
import { useState, useEffect } from 'react';
import { expandKeywords, expandKeywordsWithPersona } from '../../../../utils/keywordExpansion';
interface ResearchPersona {
keyword_expansion_patterns?: Record<string, string[]>;
suggested_keywords?: string[];
}
interface KeywordExpansion {
original: string[];
expanded: string[];
suggestions: string[];
}
export const useKeywordExpansion = (
keywords: string[],
industry: string,
researchPersona: ResearchPersona | null
): KeywordExpansion | null => {
const [keywordExpansion, setKeywordExpansion] = useState<KeywordExpansion | null>(null);
useEffect(() => {
if (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(
keywords,
researchPersona.keyword_expansion_patterns,
researchPersona.suggested_keywords
);
} else if (industry !== 'General') {
// Fallback to industry-based expansion
expansion = expandKeywords(keywords, industry);
} else {
expansion = { original: keywords, expanded: keywords, suggestions: [] };
}
setKeywordExpansion(expansion);
} else {
setKeywordExpansion(null);
}
}, [keywords, industry, researchPersona]);
return keywordExpansion;
};

View File

@@ -0,0 +1,62 @@
/**
* Hook for generating research angles with persona support
*/
import { useState, useEffect } from 'react';
import { generateResearchAngles } from '../../../../utils/researchAngles';
interface ResearchPersona {
research_angles?: string[];
}
export const useResearchAngles = (
keywords: string[],
industry: string,
researchPersona: ResearchPersona | null
): string[] => {
const [researchAngles, setResearchAngles] = useState<string[]>([]);
useEffect(() => {
if (keywords.length > 0) {
const query = keywords.join(' ');
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 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, 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([]);
}
}, [keywords, industry, researchPersona]);
return researchAngles;
};

View File

@@ -0,0 +1,222 @@
/**
* Hook for loading and managing research configuration and persona defaults
*/
import { useState, useEffect } from 'react';
import { getResearchConfig, ProviderAvailability } from '../../../../api/researchConfig';
import { WizardState } from '../../types/research.types';
import { ResearchProvider } from '../../../../services/blogWriterApi';
interface ResearchPersona {
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;
}
interface UseResearchConfigResult {
providerAvailability: ProviderAvailability | null;
researchPersona: ResearchPersona | null;
loadingConfig: boolean;
applyPersonaDefaults: (state: WizardState, onUpdate: (updates: Partial<WizardState>) => void) => void;
}
export const useResearchConfig = (): UseResearchConfigResult => {
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability | null>(null);
const [researchPersona, setResearchPersona] = useState<ResearchPersona | null>(null);
const [loadingConfig, setLoadingConfig] = useState(true);
const [configData, setConfigData] = useState<any>(null);
// Load research configuration on mount
useEffect(() => {
const loadConfig = async () => {
try {
const config = await getResearchConfig();
setConfigData(config);
// Set provider availability with fallback
setProviderAvailability(config?.provider_availability || {
google_available: true,
exa_available: false,
tavily_available: false,
tavily_key_status: 'missing',
gemini_key_status: 'missing',
exa_key_status: 'missing'
});
// Store research persona data if available
if (config?.research_persona || config?.persona_defaults) {
const defaults = config.persona_defaults || {};
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
});
}
} catch (error) {
console.error('[useResearchConfig] Failed to load research config:', error);
setProviderAvailability({
google_available: true,
exa_available: false,
tavily_available: false,
tavily_key_status: 'missing',
gemini_key_status: 'missing',
exa_key_status: 'missing'
});
} finally {
setLoadingConfig(false);
}
};
loadConfig();
}, []);
const applyPersonaDefaults = (state: WizardState, onUpdate: (updates: Partial<WizardState>) => void) => {
if (!configData?.persona_defaults) return;
const defaults = configData.persona_defaults;
// Apply industry if provided and user hasn't customized
if (defaults.industry && (!state.industry || state.industry === 'General')) {
onUpdate({ industry: defaults.industry });
}
// 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 (configData.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: defaults.suggested_domains
}
});
}
}
// Apply suggested Exa category if available
if (defaults.suggested_exa_category && !state.config.exa_category) {
onUpdate({
config: {
...state.config,
exa_category: defaults.suggested_exa_category
}
});
}
// 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'
}
});
}
// 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
}
});
}
// Apply additional hyper-personalization defaults from research persona
if (defaults.has_research_persona) {
// 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' && configData.provider_availability?.exa_available) ||
(defaults.default_provider === 'tavily' && configData.provider_availability?.tavily_available) ||
(defaults.default_provider === 'google' && configData.provider_availability?.google_available);
if (providerAvailable && !state.config.provider) {
onUpdate({
config: {
...state.config,
provider: defaults.default_provider as ValidProvider
}
});
}
}
}
}
};
return {
providerAvailability,
researchPersona,
loadingConfig,
applyPersonaDefaults,
};
};

View File

@@ -123,6 +123,49 @@ export interface TrendAnalysis {
impact: string | null;
timeline: string | null;
sources: string[];
// Google Trends specific (optional)
google_trends_data?: GoogleTrendsData;
interest_score?: number; // 0-100 from Google Trends
regional_interest?: Record<string, number>;
related_topics?: {
top: string[];
rising: string[];
};
related_queries?: {
top: string[];
rising: string[];
};
}
export interface GoogleTrendsData {
interest_over_time: Array<Record<string, any>>;
interest_by_region: Array<Record<string, any>>;
related_topics: {
top: Array<Record<string, any>>;
rising: Array<Record<string, any>>;
};
related_queries: {
top: Array<Record<string, any>>;
rising: Array<Record<string, any>>;
};
trending_searches?: string[];
timeframe: string;
geo: string;
keywords: string[];
timestamp: string;
cached?: boolean;
error?: string;
}
export interface TrendsConfig {
enabled: boolean;
keywords: string[];
keywords_justification: string;
timeframe: string;
timeframe_justification: string;
geo: string;
geo_justification: string;
expected_insights: string[];
}
export interface ComparisonItem {
@@ -172,6 +215,42 @@ export interface AnalyzeIntentRequest {
use_competitor_data: boolean;
}
// Optimized provider configuration with AI-driven justifications
export interface OptimizedConfig {
provider: 'exa' | 'tavily' | 'google';
provider_justification?: string;
// Exa settings with justifications
exa_type?: string;
exa_type_justification?: string;
exa_category?: string;
exa_category_justification?: string;
exa_include_domains?: string[];
exa_include_domains_justification?: string;
exa_num_results?: number;
exa_num_results_justification?: string;
exa_date_filter?: string;
exa_date_justification?: string;
exa_highlights?: boolean;
exa_highlights_justification?: string;
exa_context?: boolean;
exa_context_justification?: string;
// Tavily settings with justifications
tavily_topic?: string;
tavily_topic_justification?: string;
tavily_search_depth?: string;
tavily_search_depth_justification?: string;
tavily_include_answer?: boolean | string;
tavily_include_answer_justification?: string;
tavily_time_range?: string;
tavily_time_range_justification?: string;
tavily_max_results?: number;
tavily_max_results_justification?: string;
tavily_raw_content?: string;
tavily_raw_content_justification?: string;
}
export interface AnalyzeIntentResponse {
success: boolean;
intent: ResearchIntent;
@@ -180,7 +259,16 @@ export interface AnalyzeIntentResponse {
suggested_keywords: string[];
suggested_angles: string[];
quick_options: QuickOption[];
confidence_reason?: string;
great_example?: string;
error_message: string | null;
// Unified: Optimized provider parameters based on intent
optimized_config?: OptimizedConfig;
recommended_provider?: 'exa' | 'tavily' | 'google';
// Google Trends configuration (if trends in deliverables)
trends_config?: TrendsConfig;
}
export interface QuickOption {
@@ -200,6 +288,7 @@ export interface IntentDrivenResearchRequest {
max_sources: number;
include_domains: string[];
exclude_domains: string[];
trends_config?: TrendsConfig; // Google Trends configuration
skip_inference: boolean;
}
@@ -237,6 +326,9 @@ export interface IntentDrivenResearchResponse {
// The intent used
intent: ResearchIntent | null;
// Google Trends data (if trends were analyzed)
google_trends_data?: GoogleTrendsData;
// Error
error_message: string | null;
}

View File

@@ -2,7 +2,8 @@ import { BlogResearchResponse, ResearchMode, ResearchConfig } from '../../../ser
import {
ResearchIntent,
AnalyzeIntentResponse,
IntentDrivenResearchResponse
IntentDrivenResearchResponse,
ResearchQuery,
} from './intent.types';
export interface WizardState {
@@ -35,7 +36,7 @@ export interface ResearchExecution {
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>;
executeIntentResearch: (state: WizardState, selectedQueries?: ResearchQuery[]) => Promise<IntentDrivenResearchResponse | null>;
clearIntent: () => void;
}