import React, { useState, useEffect, useRef } from 'react'; import { WizardStepProps } from '../types/research.types'; import { ResearchProvider, ResearchMode } from '../../../services/researchApi'; import { getResearchConfig, ProviderAvailability } from '../../../api/researchConfig'; import { getResearchHistory, ResearchHistoryEntry } from '../../../utils/researchHistory'; // Utilities import { parseIntelligentInput } from './utils/inputParser'; import { getIndustryPlaceholders } from './utils/placeholders'; import { suggestResearchMode } from './utils/researchModeSuggester'; import { getIndustryDomainSuggestions, getIndustryExaCategory } from './utils/industryDefaults'; // Components import { ResearchHistory } from './components/ResearchHistory'; import { ResearchInputContainer } from './components/ResearchInputContainer'; import { SmartInputIndicator } from './components/SmartInputIndicator'; import { KeywordExpansion } from './components/KeywordExpansion'; // Removed: CurrentKeywords - keywords now managed in IntentConfirmationPanel // Removed: ResearchAngles - intent-driven research already generates targeted queries import { ResearchInputHeader } from './components/ResearchInputHeader'; // Removed: AdvancedOptionsSection - now handled by AdvancedProviderOptionsSection in IntentConfirmationPanel import { IntentConfirmationPanel } from './components/IntentConfirmationPanel'; import { ResearchExecution } from '../types/research.types'; // Hooks import { useKeywordExpansion } from './hooks/useKeywordExpansion'; // Removed: useResearchAngles - ResearchAngles component removed interface ResearchInputProps extends WizardStepProps { advanced?: boolean; onAdvancedChange?: (advanced: boolean) => void; execution?: ResearchExecution; } export const ResearchInput: React.FC = ({ state, onUpdate, onNext, advanced: advancedProp, onAdvancedChange, execution }) => { const [currentPlaceholder, setCurrentPlaceholder] = useState(0); const [providerAvailability, setProviderAvailability] = useState(null); const [loadingConfig, setLoadingConfig] = useState(true); const [suggestedMode, setSuggestedMode] = useState(null); const [researchHistory, setResearchHistory] = useState([]); const [researchPersona, setResearchPersona] = useState<{ research_angles?: string[]; recommended_presets?: Array<{ name: string; keywords: string | string[]; description?: string; }>; suggested_keywords?: string[]; keyword_expansion_patterns?: Record; industry?: string; target_audience?: string; } | null>(null); const fileInputRef = React.useRef(null); // Use prop if provided, otherwise use local state const [localAdvanced, setLocalAdvanced] = useState(false); const advanced = advancedProp !== undefined ? advancedProp : localAdvanced; const setAdvanced = onAdvancedChange || setLocalAdvanced; // Load research history on mount and when component updates useEffect(() => { const history = getResearchHistory(); setResearchHistory(history); }, []); // Load once on mount // Reload history when keywords change (after research completes) useEffect(() => { const history = getResearchHistory(); setResearchHistory(history); }, [state.keywords]); // Load research configuration on mount useEffect(() => { const loadConfig = async () => { try { const config = await getResearchConfig(); // Set provider availability with fallback setProviderAvailability(config?.provider_availability || { google_available: true, // Default to available, will be corrected by actual key status exa_available: false, tavily_available: false, tavily_key_status: 'missing', gemini_key_status: 'missing', exa_key_status: 'missing' }); // Phase 2: Apply persona defaults from API // Backend now returns hyper-personalized values (never "General") // Always apply if we have values and user hasn't customized if (config?.persona_defaults) { const defaults = config.persona_defaults; // Log whether research persona exists console.log('[ResearchInput] Persona defaults loaded:', { hasResearchPersona: defaults.has_research_persona, industry: defaults.industry, targetAudience: defaults.target_audience, hasDomains: defaults.suggested_domains?.length > 0 }); // Apply industry if provided and user hasn't customized // Phase 2: Backend never returns "General", so we apply unless user has real value if (defaults.industry && (!state.industry || state.industry === 'General')) { onUpdate({ industry: defaults.industry }); } // Apply target audience if provided if (defaults.target_audience && (!state.targetAudience || state.targetAudience === 'General')) { onUpdate({ targetAudience: defaults.target_audience }); } // Apply suggested Exa domains if Exa is available and not already set if (config.provider_availability?.exa_available && 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 } }); } // Phase 2+: Apply enhanced Exa defaults from research persona if (defaults.suggested_exa_search_type && !state.config.exa_search_type) { onUpdate({ config: { ...state.config, exa_search_type: defaults.suggested_exa_search_type as 'auto' | 'keyword' | 'neural' | 'fast' } }); } // Phase 2+: Apply Tavily defaults from research persona if (defaults.suggested_tavily_topic && !state.config.tavily_topic) { onUpdate({ config: { ...state.config, tavily_topic: defaults.suggested_tavily_topic as 'general' | 'news' | 'finance' } }); } if (defaults.suggested_tavily_search_depth && !state.config.tavily_search_depth) { onUpdate({ config: { ...state.config, tavily_search_depth: defaults.suggested_tavily_search_depth as 'basic' | 'advanced' } }); } if (defaults.suggested_tavily_include_answer && !state.config.tavily_include_answer) { const answerValue = defaults.suggested_tavily_include_answer === 'true' ? true : defaults.suggested_tavily_include_answer === 'false' ? false : defaults.suggested_tavily_include_answer as 'basic' | 'advanced'; onUpdate({ config: { ...state.config, tavily_include_answer: answerValue } }); } if (defaults.suggested_tavily_time_range && !state.config.tavily_time_range) { onUpdate({ config: { ...state.config, tavily_time_range: defaults.suggested_tavily_time_range as 'day' | 'week' | 'month' | 'year' } }); } if (defaults.suggested_tavily_raw_content_format && !state.config.tavily_include_raw_content) { const rawContentValue = defaults.suggested_tavily_raw_content_format === 'true' ? true : defaults.suggested_tavily_raw_content_format === 'false' ? false : defaults.suggested_tavily_raw_content_format as 'markdown' | 'text'; onUpdate({ config: { ...state.config, tavily_include_raw_content: rawContentValue } }); } // Phase 2: Apply additional hyper-personalization defaults from research persona if (defaults.has_research_persona && config.research_persona) { console.log('[ResearchInput] Applying research persona hyper-personalization:', { researchMode: defaults.default_research_mode, provider: defaults.default_provider, suggestedKeywords: defaults.suggested_keywords?.length || 0, researchAngles: defaults.research_angles?.length || 0, recommendedPresets: config.research_persona.recommended_presets?.length || 0 }); // Store research persona data for personalized placeholders, keyword expansion, and research angles setResearchPersona({ research_angles: config.research_persona.research_angles || defaults.research_angles, recommended_presets: config.research_persona.recommended_presets || [], suggested_keywords: config.research_persona.suggested_keywords || defaults.suggested_keywords, keyword_expansion_patterns: config.research_persona.keyword_expansion_patterns, industry: config.research_persona.default_industry || defaults.industry, target_audience: config.research_persona.default_target_audience || defaults.target_audience }); // Apply default research mode if not already customized if (defaults.default_research_mode && state.researchMode === 'comprehensive') { const validModes = ['basic', 'comprehensive', 'targeted'] as const; if (validModes.includes(defaults.default_research_mode as typeof validModes[number])) { onUpdate({ researchMode: defaults.default_research_mode as typeof validModes[number] }); } } // Apply default provider (only if it's available) if (defaults.default_provider) { const validProviders = ['exa', 'tavily', 'google'] as const; type ValidProvider = typeof validProviders[number]; if (validProviders.includes(defaults.default_provider as ValidProvider)) { const providerAvailable = (defaults.default_provider === 'exa' && config.provider_availability?.exa_available) || (defaults.default_provider === 'tavily' && config.provider_availability?.tavily_available) || (defaults.default_provider === 'google' && config.provider_availability?.google_available); if (providerAvailable && !state.config.provider) { onUpdate({ config: { ...state.config, provider: defaults.default_provider as ValidProvider } }); } } } } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('[ResearchInput] Failed to load research config:', errorMessage); // Set default provider availability on error setProviderAvailability({ google_available: true, // Optimistically assume available exa_available: false, tavily_available: false, tavily_key_status: 'missing', gemini_key_status: 'missing', exa_key_status: 'missing' }); // Continue with defaults - don't block the UI } finally { setLoadingConfig(false); } }; loadConfig(); }, []); // Only run once on mount // Get industry-specific placeholders, enhanced with research persona data const placeholderExamples = getIndustryPlaceholders(state.industry, researchPersona || undefined); // Rotate placeholder examples every 4 seconds useEffect(() => { const interval = setInterval(() => { setCurrentPlaceholder((prev) => (prev + 1) % placeholderExamples.length); }, 4000); return () => clearInterval(interval); }, [placeholderExamples.length]); // Reset placeholder index when industry changes useEffect(() => { setCurrentPlaceholder(0); }, [state.industry]); // Auto-set provider based on availability // Priority: Exa → Tavily → Google for ALL modes (including basic) // This provides better semantic search results for content creators useEffect(() => { if (!providerAvailability) return; // Priority: Exa → Tavily → Google for all modes let newProvider: ResearchProvider = 'google'; if (providerAvailability.exa_available) { newProvider = 'exa'; } else if (providerAvailability.tavily_available) { newProvider = 'tavily'; } else { newProvider = 'google'; } // Only update if provider changed if (state.config.provider !== newProvider) { console.log('[ResearchInput] Auto-selecting provider:', newProvider, 'for mode:', state.researchMode); onUpdate({ config: { ...state.config, provider: newProvider } }); } }, [state.researchMode, providerAvailability]); // Dynamic domain suggestions when industry changes useEffect(() => { if (!providerAvailability || state.industry === 'General') return; const newDomains = getIndustryDomainSuggestions(state.industry); const newCategory = getIndustryExaCategory(state.industry); // Only update if Exa is available and domains/category should change if (providerAvailability.exa_available && newDomains.length > 0) { const configUpdates: any = {}; // Update domains if different const currentDomains = state.config.exa_include_domains || []; if (JSON.stringify(currentDomains) !== JSON.stringify(newDomains)) { configUpdates.exa_include_domains = newDomains; } // Update category if available and different if (newCategory && state.config.exa_category !== newCategory) { configUpdates.exa_category = newCategory; } // Apply updates if any if (Object.keys(configUpdates).length > 0) { onUpdate({ config: { ...state.config, ...configUpdates } }); } } }, [state.industry, providerAvailability]); // Use keyword expansion hook const keywordExpansion = useKeywordExpansion(state.keywords, state.industry, researchPersona); // Event handlers const handleKeywordsChange = (e: React.ChangeEvent) => { const value = e.target.value; const keywords = parseIntelligentInput(value); onUpdate({ keywords }); // Update suggested mode const suggested = suggestResearchMode(keywords); setSuggestedMode(suggested); }; const handleAddSuggestion = (suggestion: string) => { const currentKeywords = [...state.keywords]; // Check if suggestion already exists (case-insensitive) const exists = currentKeywords.some(k => k.toLowerCase() === suggestion.toLowerCase()); if (!exists) { currentKeywords.push(suggestion); onUpdate({ keywords: currentKeywords }); } }; // Removed: handleRemoveKeyword - keywords now managed in IntentConfirmationPanel // Removed: handleUseAngle - intent-driven research already generates targeted queries const handleIndustryChange = (industry: string) => { onUpdate({ industry }); }; const handleModeChange = (mode: ResearchMode) => { onUpdate({ researchMode: mode }); }; const handleProviderChange = (provider: ResearchProvider) => { onUpdate({ config: { ...state.config, provider } }); }; const handleFileUpload = () => { fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { console.log('File selected:', file.name); // TODO: Implement file upload logic } }; const handleConfigUpdate = (updates: Partial) => { onUpdate({ config: { ...state.config, ...updates } }); }; const handleLoadHistory = (entry: Partial) => { onUpdate(entry); }; const handleHistoryCleared = () => { setResearchHistory([]); }; return (
{/* Main Input Area */}
{ e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.4)'; e.currentTarget.style.boxShadow = '0 4px 20px rgba(14, 165, 233, 0.12)'; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.2)'; e.currentTarget.style.boxShadow = 'none'; }} > {/* Header */} {/* Research History */} {/* Main Input Container with Intent & Options button */} onUpdate({ userPurpose: purpose })} onContentOutputChange={(output) => onUpdate({ userContentOutput: output })} onDepthChange={(depth) => onUpdate({ userDepth: depth })} 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' | 'fast' | 'deep'; if (optConfig.exa_include_domains) configUpdates.exa_include_domains = optConfig.exa_include_domains; if (optConfig.exa_num_results !== undefined) configUpdates.exa_num_results = optConfig.exa_num_results; if (optConfig.exa_date_filter) configUpdates.exa_date_filter = optConfig.exa_date_filter; if (optConfig.exa_end_published_date) configUpdates.exa_end_published_date = optConfig.exa_end_published_date; if (optConfig.exa_start_crawl_date) configUpdates.exa_start_crawl_date = optConfig.exa_start_crawl_date; if (optConfig.exa_end_crawl_date) configUpdates.exa_end_crawl_date = optConfig.exa_end_crawl_date; if (optConfig.exa_include_text) configUpdates.exa_include_text = optConfig.exa_include_text; if (optConfig.exa_exclude_text) configUpdates.exa_exclude_text = optConfig.exa_exclude_text; if (optConfig.exa_highlights !== undefined) configUpdates.exa_highlights = optConfig.exa_highlights; if (optConfig.exa_highlights_num_sentences !== undefined) configUpdates.exa_highlights_num_sentences = optConfig.exa_highlights_num_sentences; if (optConfig.exa_highlights_per_url !== undefined) configUpdates.exa_highlights_per_url = optConfig.exa_highlights_per_url; if (optConfig.exa_context !== undefined) configUpdates.exa_context = optConfig.exa_context; if (optConfig.exa_context_max_characters !== undefined) configUpdates.exa_context_max_characters = optConfig.exa_context_max_characters; if (optConfig.exa_text_max_characters !== undefined) configUpdates.exa_text_max_characters = optConfig.exa_text_max_characters; if (optConfig.exa_summary_query) configUpdates.exa_summary_query = optConfig.exa_summary_query; if (optConfig.exa_additional_queries && optConfig.exa_additional_queries.length > 0) { configUpdates.exa_additional_queries = optConfig.exa_additional_queries; } // 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 */} {/* Smart Input Detection Indicator */} {/* Error Display */} {execution && execution.error && (
⚠️ Smart Research Error

{execution.error}

)} {/* Intent Analysis Panel - Always inline when available (Unified Design) */} {execution && execution.intentAnalysis && (
execution.confirmIntent(intent, wizardState || state)} wizardState={state} 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} />
)} {/* Keyword Expansion Suggestions */} {keywordExpansion && keywordExpansion.suggestions.length > 0 && ( )} {/* Note: Current Keywords removed - keywords are now managed in IntentConfirmationPanel */} {/* Note: Research Angles removed - intent-driven research already generates targeted queries */}
); };