AI Researcher and Video Studio implementation complete
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user