AI Researcher and Video Studio implementation complete

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

View File

@@ -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>
);