AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useResearchWizard } from './hooks/useResearchWizard';
|
||||
import { useResearchExecution } from './hooks/useResearchExecution';
|
||||
import { ResearchInput } from './steps/ResearchInput';
|
||||
@@ -7,11 +7,18 @@ import { StepResults } from './steps/StepResults';
|
||||
import { ResearchWizardProps } from './types/research.types';
|
||||
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';
|
||||
import { intentResearchApi } from '../../api/intentResearchApi';
|
||||
import { clearDraftFromStorage } from '../../utils/researchDraftManager';
|
||||
|
||||
export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
export interface ResearchWizardHeaderActions {
|
||||
onOpenPersona?: () => void;
|
||||
onOpenCompetitors?: () => void;
|
||||
personaExists?: boolean;
|
||||
}
|
||||
|
||||
export const ResearchWizard: React.FC<ResearchWizardProps & { headerActions?: ResearchWizardHeaderActions }> = ({
|
||||
onComplete,
|
||||
onCancel,
|
||||
initialKeywords,
|
||||
@@ -19,6 +26,8 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
initialTargetAudience,
|
||||
initialResearchMode,
|
||||
initialConfig,
|
||||
initialResults,
|
||||
headerActions,
|
||||
}) => {
|
||||
const wizard = useResearchWizard(
|
||||
initialKeywords,
|
||||
@@ -30,6 +39,30 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
const execution = useResearchExecution();
|
||||
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability | null>(null);
|
||||
const [advanced, setAdvanced] = useState<boolean>(false);
|
||||
const hasSavedProject = useRef(false); // Track if we've already saved this project
|
||||
|
||||
// Restore initial results if provided (e.g., from saved project)
|
||||
useEffect(() => {
|
||||
if (initialResults && !wizard.state.results) {
|
||||
wizard.updateState({ results: initialResults });
|
||||
// Navigate to results step if results are available
|
||||
if (wizard.state.currentStep < 3) {
|
||||
wizard.updateState({ currentStep: 3 });
|
||||
}
|
||||
}
|
||||
}, [initialResults]); // Only run once on mount
|
||||
|
||||
// Restore intent analysis and confirmed intent from draft
|
||||
useEffect(() => {
|
||||
if (execution.intentAnalysis && wizard.state.keywords.length > 0) {
|
||||
// Intent analysis already restored by useResearchExecution hook
|
||||
console.log('[ResearchWizard] ✅ Intent analysis restored from draft');
|
||||
}
|
||||
if (execution.confirmedIntent && wizard.state.keywords.length > 0) {
|
||||
// Confirmed intent already restored by useResearchExecution hook
|
||||
console.log('[ResearchWizard] ✅ Confirmed intent restored from draft');
|
||||
}
|
||||
}, [execution.intentAnalysis, execution.confirmedIntent, wizard.state.keywords]);
|
||||
|
||||
// Load provider availability on mount
|
||||
useEffect(() => {
|
||||
@@ -68,6 +101,61 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
}
|
||||
}, [execution.result, execution.isExecuting]); // Don't depend on currentStep to avoid loops
|
||||
|
||||
// Auto-save research project when research completes
|
||||
useEffect(() => {
|
||||
// Save when intent-driven research completes
|
||||
if (execution.intentResult?.success && !hasSavedProject.current && wizard.state.keywords.length > 0) {
|
||||
hasSavedProject.current = true;
|
||||
|
||||
// Generate project title from keywords
|
||||
const projectTitle = `Research: ${wizard.state.keywords.slice(0, 3).join(', ')}`;
|
||||
|
||||
// Save project to Asset Library
|
||||
intentResearchApi.saveResearchProject(wizard.state, {
|
||||
intentAnalysis: execution.intentAnalysis,
|
||||
confirmedIntent: execution.confirmedIntent,
|
||||
intentResult: execution.intentResult,
|
||||
title: projectTitle,
|
||||
description: `Research project on ${wizard.state.keywords.join(', ')}. ` +
|
||||
`Industry: ${wizard.state.industry}, Target Audience: ${wizard.state.targetAudience}`,
|
||||
}).then((response) => {
|
||||
if (response.success) {
|
||||
console.log('[ResearchWizard] ✅ Final research project saved to Asset Library:', response.asset_id);
|
||||
// Clear draft after successful final save
|
||||
clearDraftFromStorage();
|
||||
} else {
|
||||
console.warn('[ResearchWizard] ⚠️ Failed to save final research project:', response.message);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('[ResearchWizard] ❌ Error saving final research project:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Save when legacy research completes (fallback)
|
||||
if (wizard.state.results && !hasSavedProject.current && wizard.state.keywords.length > 0 && !execution.intentResult) {
|
||||
hasSavedProject.current = true;
|
||||
|
||||
const projectTitle = `Research: ${wizard.state.keywords.slice(0, 3).join(', ')}`;
|
||||
|
||||
intentResearchApi.saveResearchProject(wizard.state, {
|
||||
legacyResult: wizard.state.results,
|
||||
title: projectTitle,
|
||||
description: `Completed research project on ${wizard.state.keywords.join(', ')}. ` +
|
||||
`Industry: ${wizard.state.industry}, Target Audience: ${wizard.state.targetAudience}`,
|
||||
}).then((response) => {
|
||||
if (response.success) {
|
||||
console.log('[ResearchWizard] ✅ Final research project saved to Asset Library:', response.asset_id);
|
||||
// Clear draft after successful final save
|
||||
clearDraftFromStorage();
|
||||
} else {
|
||||
console.warn('[ResearchWizard] ⚠️ Failed to save final research project:', response.message);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('[ResearchWizard] ❌ Error saving final research project:', error);
|
||||
});
|
||||
}
|
||||
}, [execution.intentResult, wizard.state.results, wizard.state.keywords, wizard.state.industry, wizard.state.targetAudience, execution.intentAnalysis, execution.confirmedIntent]);
|
||||
|
||||
// Handle completion callback and track history
|
||||
useEffect(() => {
|
||||
if (wizard.state.results && onComplete) {
|
||||
@@ -144,8 +232,91 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
Research Wizard
|
||||
</h1>
|
||||
|
||||
{/* Provider Status Chips */}
|
||||
<ProviderChips providerAvailability={providerAvailability} advanced={advanced} />
|
||||
{/* Persona Button */}
|
||||
{headerActions?.onOpenPersona && (
|
||||
<button
|
||||
onClick={headerActions.onOpenPersona}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: headerActions.personaExists ? '#22c55e' : '#ef4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: headerActions.personaExists
|
||||
? '0 0 12px rgba(34, 197, 94, 0.4), 0 1px 4px rgba(34, 197, 94, 0.2)'
|
||||
: '0 0 12px rgba(239, 68, 68, 0.4), 0 1px 4px rgba(239, 68, 68, 0.2)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
if (headerActions.personaExists) {
|
||||
e.currentTarget.style.boxShadow = '0 0 16px rgba(34, 197, 94, 0.5), 0 2px 6px rgba(34, 197, 94, 0.3)';
|
||||
} else {
|
||||
e.currentTarget.style.boxShadow = '0 0 16px rgba(239, 68, 68, 0.5), 0 2px 6px rgba(239, 68, 68, 0.3)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
if (headerActions.personaExists) {
|
||||
e.currentTarget.style.boxShadow = '0 0 12px rgba(34, 197, 94, 0.4), 0 1px 4px rgba(34, 197, 94, 0.2)';
|
||||
} else {
|
||||
e.currentTarget.style.boxShadow = '0 0 12px rgba(239, 68, 68, 0.4), 0 1px 4px rgba(239, 68, 68, 0.2)';
|
||||
}
|
||||
}}
|
||||
title={headerActions.personaExists ? 'View Research Persona' : 'Create Research Persona'}
|
||||
>
|
||||
<span style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: 'white',
|
||||
boxShadow: '0 0 4px rgba(255, 255, 255, 0.8)',
|
||||
}} />
|
||||
<span>Persona</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Competitors Button */}
|
||||
{headerActions?.onOpenCompetitors && (
|
||||
<button
|
||||
onClick={headerActions.onOpenCompetitors}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#0284c7',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: '0 2px 6px rgba(2, 132, 199, 0.2)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#0369a1';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 10px rgba(2, 132, 199, 0.3)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#0284c7';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 6px rgba(2, 132, 199, 0.2)';
|
||||
}}
|
||||
title="View Competitor Analysis"
|
||||
>
|
||||
<span>📊</span>
|
||||
<span>Competitors</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Advanced Chip */}
|
||||
<AdvancedChip advanced={advanced} />
|
||||
@@ -337,106 +508,60 @@ export const ResearchWizard: React.FC<ResearchWizardProps> = ({
|
||||
← Back
|
||||
</button>
|
||||
|
||||
{/* 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
|
||||
}
|
||||
});
|
||||
{/* Research Button - Hidden on Step 1 when IntentConfirmationPanel is visible (has its own "Start Research" button) */}
|
||||
{!(wizard.state.currentStep === 1 && execution.intentAnalysis) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (wizard.state.currentStep === 1) {
|
||||
// On Step 1: No intent analysis - go to progress step for traditional research
|
||||
wizard.nextStep();
|
||||
} else {
|
||||
// No intent or low confidence - go to progress step for traditional research
|
||||
wizard.nextStep();
|
||||
}
|
||||
} else {
|
||||
wizard.nextStep();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
wizard.state.currentStep === 1
|
||||
? !wizard.canGoNext() || !execution.intentAnalysis || execution.isExecuting
|
||||
: !wizard.canGoNext()
|
||||
}
|
||||
style={{
|
||||
padding: '10px 24px',
|
||||
background: (() => {
|
||||
const canProceed = wizard.state.currentStep === 1
|
||||
? wizard.canGoNext() && execution.intentAnalysis && !execution.isExecuting
|
||||
: wizard.canGoNext();
|
||||
return canProceed
|
||||
}}
|
||||
disabled={!wizard.canGoNext()}
|
||||
style={{
|
||||
padding: '10px 24px',
|
||||
background: wizard.canGoNext()
|
||||
? '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: (() => {
|
||||
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: (() => {
|
||||
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) => {
|
||||
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)';
|
||||
: 'rgba(100, 116, 139, 0.2)',
|
||||
color: wizard.canGoNext() ? 'white' : '#94a3b8',
|
||||
border: 'none',
|
||||
borderRadius: '10px',
|
||||
cursor: wizard.canGoNext() ? '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',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (wizard.canGoNext()) {
|
||||
e.currentTarget.style.transform = 'translateX(4px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(14, 165, 233, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (wizard.canGoNext()) {
|
||||
e.currentTarget.style.transform = 'translateX(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
|
||||
}
|
||||
}}
|
||||
title={
|
||||
wizard.isLastStep ? 'Complete research' : 'Continue to next step'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
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'
|
||||
}
|
||||
>
|
||||
{execution.isExecuting ? (
|
||||
<>
|
||||
<span style={{ animation: 'pulse 1.5s ease-in-out infinite' }}>🔍</span>
|
||||
Researching...
|
||||
</>
|
||||
) : wizard.isLastStep ? (
|
||||
'Finish'
|
||||
) : (
|
||||
<>
|
||||
🚀 Research
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
>
|
||||
{wizard.isLastStep ? (
|
||||
'Finish'
|
||||
) : (
|
||||
<>
|
||||
→ Next
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { researchCache } from '../../../services/researchCache';
|
||||
import { WizardState } from '../types/research.types';
|
||||
import { researchEngineApi, ResearchEngineRequest } from '../../../services/researchEngineApi';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AnalyzeIntentResponse,
|
||||
ResearchQuery,
|
||||
} from '../types/intent.types';
|
||||
import { autoSaveDraft, restoreDraft } from '../../../utils/researchDraftManager';
|
||||
|
||||
export const useResearchExecution = () => {
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
@@ -22,6 +23,25 @@ export const useResearchExecution = () => {
|
||||
const [confirmedIntent, setConfirmedIntent] = useState<ResearchIntent | null>(null);
|
||||
const [intentResult, setIntentResult] = useState<IntentDrivenResearchResponse | null>(null);
|
||||
const [useIntentMode, setUseIntentMode] = useState(true); // Enable by default
|
||||
|
||||
// Restore intent analysis and confirmed intent from draft on mount
|
||||
useEffect(() => {
|
||||
const draft = restoreDraft();
|
||||
if (draft) {
|
||||
if (draft.intent_analysis) {
|
||||
setIntentAnalysis(draft.intent_analysis);
|
||||
console.log('[useResearchExecution] 🔄 Restored intent analysis from draft');
|
||||
}
|
||||
if (draft.confirmed_intent) {
|
||||
setConfirmedIntent(draft.confirmed_intent);
|
||||
console.log('[useResearchExecution] 🔄 Restored confirmed intent from draft');
|
||||
}
|
||||
if (draft.intent_result) {
|
||||
setIntentResult(draft.intent_result);
|
||||
console.log('[useResearchExecution] 🔄 Restored intent result from draft');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const polling = useResearchPolling({
|
||||
onComplete: (result) => {
|
||||
@@ -145,6 +165,9 @@ export const useResearchExecution = () => {
|
||||
keywords: state.keywords,
|
||||
use_persona: true,
|
||||
use_competitor_data: true,
|
||||
user_provided_purpose: state.userPurpose,
|
||||
user_provided_content_output: state.userContentOutput,
|
||||
user_provided_depth: state.userDepth,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -161,6 +184,14 @@ export const useResearchExecution = () => {
|
||||
setConfirmedIntent(response.intent);
|
||||
}
|
||||
|
||||
// Save draft with intent analysis
|
||||
autoSaveDraft(state, {
|
||||
intentAnalysis: response,
|
||||
confirmedIntent: response.intent.confidence >= 0.85 && !response.intent.needs_clarification ? response.intent : undefined,
|
||||
}).catch(error => {
|
||||
console.warn('[useResearchExecution] Failed to save draft after intent analysis:', error);
|
||||
});
|
||||
|
||||
setIsAnalyzingIntent(false);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
@@ -199,6 +230,7 @@ export const useResearchExecution = () => {
|
||||
expected_deliverables: ['key_statistics'],
|
||||
depth: 'detailed',
|
||||
focus_areas: [],
|
||||
also_answering: [],
|
||||
perspective: null,
|
||||
time_sensitivity: null,
|
||||
input_type: 'keywords',
|
||||
@@ -220,9 +252,19 @@ export const useResearchExecution = () => {
|
||||
/**
|
||||
* Confirm the analyzed intent (possibly with user modifications).
|
||||
*/
|
||||
const confirmIntent = useCallback((intent: ResearchIntent) => {
|
||||
const confirmIntent = useCallback((intent: ResearchIntent, state?: WizardState) => {
|
||||
setConfirmedIntent(intent);
|
||||
}, []);
|
||||
|
||||
// Save draft with confirmed intent
|
||||
if (state) {
|
||||
autoSaveDraft(state, {
|
||||
intentAnalysis: intentAnalysis || undefined,
|
||||
confirmedIntent: intent,
|
||||
}).catch(error => {
|
||||
console.warn('[useResearchExecution] Failed to save draft after intent confirmation:', error);
|
||||
});
|
||||
}
|
||||
}, [intentAnalysis]);
|
||||
|
||||
/**
|
||||
* Update a specific field in the analyzed intent.
|
||||
@@ -289,6 +331,15 @@ export const useResearchExecution = () => {
|
||||
|
||||
setIntentResult(response);
|
||||
|
||||
// Save draft with research results
|
||||
autoSaveDraft(state, {
|
||||
intentAnalysis: intentAnalysis || undefined,
|
||||
confirmedIntent: intent,
|
||||
intentResult: response,
|
||||
}).catch(error => {
|
||||
console.warn('[useResearchExecution] Failed to save draft after research completion:', error);
|
||||
});
|
||||
|
||||
// Also set the legacy result for backward compatibility with StepResults
|
||||
// Transform intent result to match the expected format
|
||||
const legacyResult = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { WizardState, WizardStepProps } from '../types/research.types';
|
||||
import { ResearchMode, ResearchConfig, BlogResearchResponse } from '../../../services/blogWriterApi';
|
||||
import { ResearchMode, ResearchConfig, ResearchResponse } from '../../../services/researchApi';
|
||||
import { restoreDraft, autoSaveDraft } from '../../../utils/researchDraftManager';
|
||||
|
||||
const WIZARD_STATE_KEY = 'alwrity_research_wizard_state';
|
||||
const MAX_STEPS = 3; // Input (combined) -> Progress -> Results
|
||||
@@ -34,7 +35,7 @@ export const useResearchWizard = (
|
||||
initialConfig?: ResearchConfig
|
||||
) => {
|
||||
const [state, setState] = useState<WizardState>(() => {
|
||||
// If initial values are provided (preset clicked), clear localStorage and use them
|
||||
// If initial values are provided (preset clicked), clear drafts and use them
|
||||
if (initialKeywords || initialIndustry || initialTargetAudience || initialResearchMode || initialConfig) {
|
||||
localStorage.removeItem(WIZARD_STATE_KEY);
|
||||
return {
|
||||
@@ -47,7 +48,27 @@ export const useResearchWizard = (
|
||||
};
|
||||
}
|
||||
|
||||
// Try to load from localStorage only if no initial values
|
||||
// Try to restore from draft first (more comprehensive)
|
||||
const draft = restoreDraft();
|
||||
if (draft && ((draft.keywords && draft.keywords.length > 0) || draft.intent_analysis || draft.confirmed_intent)) {
|
||||
console.log('[useResearchWizard] 🔄 Restoring from draft:', {
|
||||
step: draft.current_step,
|
||||
keywords: draft.keywords?.length || 0,
|
||||
hasIntentAnalysis: !!draft.intent_analysis,
|
||||
hasConfirmedIntent: !!draft.confirmed_intent,
|
||||
});
|
||||
return {
|
||||
currentStep: draft.current_step || 1,
|
||||
keywords: draft.keywords || [],
|
||||
industry: draft.industry || defaultState.industry,
|
||||
targetAudience: draft.target_audience || defaultState.targetAudience,
|
||||
researchMode: (draft.research_mode as ResearchMode) || defaultState.researchMode,
|
||||
config: draft.config || defaultState.config,
|
||||
results: draft.legacy_result || null, // Only restore legacy_result for WizardState.results
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to localStorage (legacy)
|
||||
const saved = localStorage.getItem(WIZARD_STATE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
@@ -78,11 +99,16 @@ export const useResearchWizard = (
|
||||
}
|
||||
}, [initialKeywords, initialIndustry, initialTargetAudience, initialResearchMode, initialConfig]);
|
||||
|
||||
// Persist state to localStorage
|
||||
// Persist state to localStorage only (no database save until intent analysis)
|
||||
useEffect(() => {
|
||||
if (state.currentStep > 1) {
|
||||
// Always save to localStorage for backward compatibility
|
||||
if (state.keywords.length > 0 || state.currentStep > 1) {
|
||||
localStorage.setItem(WIZARD_STATE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
// NOTE: Database draft saving only happens after user clicks "intent and options"
|
||||
// This is handled in useResearchExecution.analyzeIntent()
|
||||
// We only save to localStorage here to preserve state across refreshes
|
||||
}, [state]);
|
||||
|
||||
const updateState = useCallback((updates: Partial<WizardState>) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { WizardStepProps } from '../types/research.types';
|
||||
import { ResearchProvider, ResearchMode } from '../../../services/blogWriterApi';
|
||||
import { ResearchProvider, ResearchMode } from '../../../services/researchApi';
|
||||
import { getResearchConfig, ProviderAvailability } from '../../../api/researchConfig';
|
||||
import {
|
||||
getResearchHistory,
|
||||
@@ -18,16 +18,16 @@ import { ResearchHistory } from './components/ResearchHistory';
|
||||
import { ResearchInputContainer } from './components/ResearchInputContainer';
|
||||
import { SmartInputIndicator } from './components/SmartInputIndicator';
|
||||
import { KeywordExpansion } from './components/KeywordExpansion';
|
||||
import { CurrentKeywords } from './components/CurrentKeywords';
|
||||
import { ResearchAngles } from './components/ResearchAngles';
|
||||
// Removed: CurrentKeywords - keywords now managed in IntentConfirmationPanel
|
||||
// Removed: ResearchAngles - intent-driven research already generates targeted queries
|
||||
import { ResearchInputHeader } from './components/ResearchInputHeader';
|
||||
import { AdvancedOptionsSection } from './components/AdvancedOptionsSection';
|
||||
// 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';
|
||||
import { useResearchAngles } from './hooks/useResearchAngles';
|
||||
// Removed: useResearchAngles - ResearchAngles component removed
|
||||
|
||||
interface ResearchInputProps extends WizardStepProps {
|
||||
advanced?: boolean;
|
||||
@@ -140,7 +140,7 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
onUpdate({
|
||||
config: {
|
||||
...state.config,
|
||||
exa_search_type: defaults.suggested_exa_search_type as 'auto' | 'keyword' | 'neural'
|
||||
exa_search_type: defaults.suggested_exa_search_type as 'auto' | 'keyword' | 'neural' | 'fast'
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -348,9 +348,6 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
// Use keyword expansion hook
|
||||
const keywordExpansion = useKeywordExpansion(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>) => {
|
||||
const value = e.target.value;
|
||||
@@ -372,16 +369,8 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveKeyword = (keywordToRemove: string) => {
|
||||
const currentKeywords = state.keywords.filter(k => k.toLowerCase() !== keywordToRemove.toLowerCase());
|
||||
onUpdate({ keywords: currentKeywords });
|
||||
};
|
||||
|
||||
const handleUseAngle = (angle: string) => {
|
||||
// Parse the angle as a new research query
|
||||
const keywords = parseIntelligentInput(angle);
|
||||
onUpdate({ keywords });
|
||||
};
|
||||
// Removed: handleRemoveKeyword - keywords now managed in IntentConfirmationPanel
|
||||
// Removed: handleUseAngle - intent-driven research already generates targeted queries
|
||||
|
||||
const handleIndustryChange = (industry: string) => {
|
||||
onUpdate({ industry });
|
||||
@@ -461,6 +450,12 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
keywords={state.keywords}
|
||||
placeholder={placeholderExamples[currentPlaceholder]}
|
||||
onKeywordsChange={handleKeywordsChange}
|
||||
userPurpose={state.userPurpose}
|
||||
userContentOutput={state.userContentOutput}
|
||||
userDepth={state.userDepth}
|
||||
onPurposeChange={(purpose) => onUpdate({ userPurpose: purpose })}
|
||||
onContentOutputChange={(output) => onUpdate({ userContentOutput: output })}
|
||||
onDepthChange={(depth) => onUpdate({ userDepth: depth })}
|
||||
onIntentAndOptions={async () => {
|
||||
if (execution?.analyzeIntent) {
|
||||
try {
|
||||
@@ -478,9 +473,25 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
|
||||
// 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_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) configUpdates.exa_num_results = optConfig.exa_num_results;
|
||||
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;
|
||||
@@ -566,7 +577,8 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
isAnalyzing={execution.isAnalyzingIntent}
|
||||
intentAnalysis={execution.intentAnalysis}
|
||||
confirmedIntent={execution.confirmedIntent}
|
||||
onConfirm={execution.confirmIntent}
|
||||
onConfirm={(intent, wizardState) => execution.confirmIntent(intent, wizardState || state)}
|
||||
wizardState={state}
|
||||
onUpdateField={execution.updateIntentField}
|
||||
onExecute={async (selectedQueries) => {
|
||||
const result = await execution.executeIntentResearch(state, selectedQueries);
|
||||
@@ -596,30 +608,11 @@ export const ResearchInput: React.FC<ResearchInputProps> = ({ state, onUpdate, o
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Current Keywords Display */}
|
||||
<CurrentKeywords
|
||||
keywords={state.keywords}
|
||||
onRemoveKeyword={handleRemoveKeyword}
|
||||
/>
|
||||
|
||||
{/* Alternative Research Angles */}
|
||||
<ResearchAngles
|
||||
angles={researchAngles}
|
||||
onUseAngle={handleUseAngle}
|
||||
hasPersona={!!researchPersona}
|
||||
/>
|
||||
{/* Note: Current Keywords removed - keywords are now managed in IntentConfirmationPanel */}
|
||||
{/* Note: Research Angles removed - intent-driven research already generates targeted queries */}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* Advanced Options Section */}
|
||||
<AdvancedOptionsSection
|
||||
advanced={advanced}
|
||||
providerAvailability={providerAvailability}
|
||||
config={state.config}
|
||||
onConfigUpdate={handleConfigUpdate}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { WizardStepProps, ModeCardInfo } from '../types/research.types';
|
||||
import { ResearchProvider } from '../../../services/blogWriterApi';
|
||||
import { ResearchProvider } from '../../../services/researchApi';
|
||||
|
||||
const modeCards: ModeCardInfo[] = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { WizardStepProps, ResearchExecution } from '../types/research.types';
|
||||
import { ResearchResults } from '../../BlogWriter/ResearchResults';
|
||||
import { ResearchResponse } from '../../../services/researchApi';
|
||||
import { BlogResearchResponse } from '../../../services/blogWriterApi';
|
||||
import { IntentResultsDisplay } from './components/IntentResultsDisplay';
|
||||
import { IntentDrivenResearchResponse } from '../types/intent.types';
|
||||
@@ -332,7 +333,7 @@ export const StepResults: React.FC<StepResultsProps> = ({ state, onUpdate, onBac
|
||||
{activeTab === 'analysis' && (
|
||||
<div style={{ animation: 'fadeIn 0.3s ease' }}>
|
||||
{state.results ? (
|
||||
<ResearchResults research={state.results} showAnalysisOnly />
|
||||
<ResearchResults research={state.results as BlogResearchResponse} showAnalysisOnly />
|
||||
) : (
|
||||
<div>
|
||||
{intentResult.suggested_outline && intentResult.suggested_outline.length > 0 && (
|
||||
@@ -372,7 +373,7 @@ export const StepResults: React.FC<StepResultsProps> = ({ state, onUpdate, onBac
|
||||
</>
|
||||
) : state.results ? (
|
||||
// Traditional results display (no tabs)
|
||||
<ResearchResults research={state.results} />
|
||||
<ResearchResults research={state.results as BlogResearchResponse} />
|
||||
) : (
|
||||
<p style={{ color: '#666', textAlign: 'center', padding: '40px' }}>
|
||||
No results available
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ProviderAvailability } from '../../../../api/researchConfig';
|
||||
import { ResearchConfig } from '../../../../services/blogWriterApi';
|
||||
import { ResearchConfig } from '../../../../services/researchApi';
|
||||
import { ExaOptions } from './ExaOptions';
|
||||
import { TavilyOptions } from './TavilyOptions';
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React from 'react';
|
||||
import { ResearchConfig } from '../../../../services/blogWriterApi';
|
||||
import { ResearchConfig } from '../../../../services/researchApi';
|
||||
import { exaCategories, exaSearchTypes } from '../utils/constants';
|
||||
import { OptimizedConfig } from '../../types/intent.types';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { exaOptionTooltips } from './utils/exaTooltips';
|
||||
|
||||
interface ExaOptionsProps {
|
||||
config: ResearchConfig;
|
||||
onConfigUpdate: (updates: Partial<ResearchConfig>) => void;
|
||||
optimizedConfig?: OptimizedConfig; // AI-optimized config with justifications
|
||||
}
|
||||
|
||||
export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }) => {
|
||||
export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate, optimizedConfig }) => {
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
onConfigUpdate({ exa_category: value || undefined });
|
||||
};
|
||||
|
||||
const handleSearchTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value as 'auto' | 'keyword' | 'neural';
|
||||
const value = e.target.value as 'auto' | 'keyword' | 'neural' | 'fast' | 'deep';
|
||||
onConfigUpdate({ exa_search_type: value });
|
||||
};
|
||||
|
||||
@@ -30,6 +34,210 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
onConfigUpdate({ exa_exclude_domains: domains });
|
||||
};
|
||||
|
||||
const handleNumResultsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1 && value <= 100) {
|
||||
onConfigUpdate({ exa_num_results: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
// Convert YYYY-MM-DD to ISO format if needed
|
||||
const isoDate = value ? `${value}T00:00:00.000Z` : undefined;
|
||||
onConfigUpdate({ exa_date_filter: isoDate || undefined });
|
||||
};
|
||||
|
||||
const handleEndPublishedDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const isoDate = value ? `${value}T23:59:59.999Z` : undefined;
|
||||
onConfigUpdate({ exa_end_published_date: isoDate || undefined });
|
||||
};
|
||||
|
||||
const handleStartCrawlDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const isoDate = value ? `${value}T00:00:00.000Z` : undefined;
|
||||
onConfigUpdate({ exa_start_crawl_date: isoDate || undefined });
|
||||
};
|
||||
|
||||
const handleEndCrawlDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const isoDate = value ? `${value}T23:59:59.999Z` : undefined;
|
||||
onConfigUpdate({ exa_end_crawl_date: isoDate || undefined });
|
||||
};
|
||||
|
||||
const handleIncludeTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
// Only one string supported, up to 5 words
|
||||
const words = value.trim().split(/\s+/).filter(Boolean).slice(0, 5);
|
||||
onConfigUpdate({ exa_include_text: words.length > 0 ? [words.join(' ')] : undefined });
|
||||
};
|
||||
|
||||
const handleExcludeTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
// Only one string supported, up to 5 words
|
||||
const words = value.trim().split(/\s+/).filter(Boolean).slice(0, 5);
|
||||
onConfigUpdate({ exa_exclude_text: words.length > 0 ? [words.join(' ')] : undefined });
|
||||
};
|
||||
|
||||
const handleHighlightsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onConfigUpdate({ exa_highlights: e.target.checked });
|
||||
};
|
||||
|
||||
const handleHighlightsNumSentencesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1) {
|
||||
onConfigUpdate({ exa_highlights_num_sentences: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleHighlightsPerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1) {
|
||||
onConfigUpdate({ exa_highlights_per_url: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMaxCharactersChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value > 0) {
|
||||
onConfigUpdate({
|
||||
exa_context: true,
|
||||
exa_context_max_characters: value
|
||||
});
|
||||
} else if (value === 0 || e.target.value === '') {
|
||||
onConfigUpdate({
|
||||
exa_context: false,
|
||||
exa_context_max_characters: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextMaxCharactersChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value > 0) {
|
||||
onConfigUpdate({ exa_text_max_characters: value });
|
||||
} else if (e.target.value === '') {
|
||||
onConfigUpdate({ exa_text_max_characters: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSummaryQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.trim();
|
||||
onConfigUpdate({ exa_summary_query: value || undefined });
|
||||
};
|
||||
|
||||
const handleContextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onConfigUpdate({ exa_context: e.target.checked });
|
||||
};
|
||||
|
||||
const handleAdditionalQueriesChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
// Parse comma or newline-separated queries
|
||||
const queries = value
|
||||
.split(/[,\n]/)
|
||||
.map(q => q.trim())
|
||||
.filter(Boolean);
|
||||
onConfigUpdate({ exa_additional_queries: queries.length > 0 ? queries : undefined });
|
||||
};
|
||||
|
||||
// Get AI justification for a field
|
||||
const getJustification = (field: string): string | undefined => {
|
||||
if (!optimizedConfig) return undefined;
|
||||
const justificationKey = `exa_${field}_justification` as keyof OptimizedConfig;
|
||||
return optimizedConfig[justificationKey] as string | undefined;
|
||||
};
|
||||
|
||||
// Get detailed tooltip content for a field
|
||||
const getTooltipContent = (field: string): string => {
|
||||
const aiJustification = getJustification(field);
|
||||
const tooltipKey = field as keyof typeof exaOptionTooltips;
|
||||
const baseTooltip = exaOptionTooltips[tooltipKey];
|
||||
|
||||
if (!baseTooltip) {
|
||||
// Fallback to AI justification if no base tooltip
|
||||
return aiJustification || '';
|
||||
}
|
||||
|
||||
let tooltip = '';
|
||||
|
||||
switch (field) {
|
||||
case 'category':
|
||||
const categoryTooltip = baseTooltip as any;
|
||||
tooltip = `${baseTooltip.description}\n\nExamples:\n${Object.entries(categoryTooltip.examples || {}).map(([key, val]) => `• ${key}: ${val}`).join('\n')}`;
|
||||
break;
|
||||
case 'searchType':
|
||||
const selectedType = config.exa_search_type || 'auto';
|
||||
const searchTypeTooltip = baseTooltip as any;
|
||||
const types = searchTypeTooltip.types;
|
||||
const typeInfo = types?.[selectedType];
|
||||
if (typeInfo) {
|
||||
tooltip = `${typeInfo.description}\n\nWhen to use: ${typeInfo.whenToUse}`;
|
||||
if (typeInfo.latency) tooltip += `\n\nLatency: ${typeInfo.latency}`;
|
||||
if (typeInfo.quality) tooltip += `\n\nQuality: ${typeInfo.quality}`;
|
||||
if (typeInfo.limits) tooltip += `\n\nLimits: ${typeInfo.limits}`;
|
||||
if (typeInfo.note) tooltip += `\n\nNote: ${typeInfo.note}`;
|
||||
} else {
|
||||
tooltip = baseTooltip.description || '';
|
||||
}
|
||||
break;
|
||||
case 'numResults':
|
||||
tooltip = `${baseTooltip.description}\n\n${(baseTooltip as any).limits || ''}\n\nRecommendations:\n${Object.entries((baseTooltip as any).recommendations || {}).map(([key, val]) => `• ${key} results: ${val}`).join('\n')}`;
|
||||
break;
|
||||
case 'dateFilter':
|
||||
case 'endPublishedDate':
|
||||
case 'startCrawlDate':
|
||||
case 'endCrawlDate':
|
||||
case 'includeText':
|
||||
case 'excludeText':
|
||||
case 'highlightsNumSentences':
|
||||
case 'highlightsPerUrl':
|
||||
case 'contextMaxCharacters':
|
||||
case 'textMaxCharacters':
|
||||
case 'summaryQuery':
|
||||
tooltip = `${baseTooltip.description}\n\nWhen to use:\n${((baseTooltip as any).whenToUse || []).map((u: string) => `• ${u}`).join('\n')}\n\n${(baseTooltip as any).format || ''}\n\nExample: ${(baseTooltip as any).example || ''}\n\n${(baseTooltip as any).recommendation || ''}\n\n${(baseTooltip as any).limit || ''}\n\n${(baseTooltip as any).note || ''}`;
|
||||
break;
|
||||
case 'highlights':
|
||||
tooltip = `${baseTooltip.description}\n\nBenefits:\n${((baseTooltip as any).benefits || []).map((b: string) => `• ${b}`).join('\n')}\n\n${(baseTooltip as any).whenToUse || ''}`;
|
||||
break;
|
||||
case 'context':
|
||||
tooltip = `${baseTooltip.description}\n\nBenefits:\n${((baseTooltip as any).benefits || []).map((b: string) => `• ${b}`).join('\n')}\n\n${(baseTooltip as any).whenToUse || ''}\n\n${(baseTooltip as any).recommendation || ''}`;
|
||||
break;
|
||||
case 'includeDomains':
|
||||
tooltip = `${baseTooltip.description}\n\nWhen to use:\n${((baseTooltip as any).whenToUse || []).map((u: string) => `• ${u}`).join('\n')}\n\n${(baseTooltip as any).format || ''}\n\nExample: ${(baseTooltip as any).example || ''}\n\n${(baseTooltip as any).limit || ''}`;
|
||||
break;
|
||||
case 'excludeDomains':
|
||||
tooltip = `${baseTooltip.description}\n\nWhen to use:\n${((baseTooltip as any).whenToUse || []).map((u: string) => `• ${u}`).join('\n')}\n\n${(baseTooltip as any).format || ''}\n\nExample: ${(baseTooltip as any).example || ''}\n\n${(baseTooltip as any).limit || ''}`;
|
||||
break;
|
||||
case 'dateFilter':
|
||||
case 'endPublishedDate':
|
||||
case 'startCrawlDate':
|
||||
case 'endCrawlDate':
|
||||
tooltip = `${baseTooltip.description}\n\nWhen to use:\n${((baseTooltip as any).whenToUse || []).map((u: string) => `• ${u}`).join('\n')}\n\n${(baseTooltip as any).format || ''}\n\nExample: ${(baseTooltip as any).example || ''}\n\n${(baseTooltip as any).note || ''}`;
|
||||
break;
|
||||
default:
|
||||
tooltip = (baseTooltip as any).description || '';
|
||||
}
|
||||
|
||||
// Append AI justification if available
|
||||
if (aiJustification) {
|
||||
tooltip += `\n\n🤖 AI Recommendation: ${aiJustification}`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
};
|
||||
|
||||
// Format date for input (YYYY-MM-DD from ISO string)
|
||||
const formatDateForInput = (isoDate?: string): string => {
|
||||
if (!isoDate) return '';
|
||||
try {
|
||||
const date = new Date(isoDate);
|
||||
return date.toISOString().split('T')[0];
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%)',
|
||||
@@ -57,13 +265,22 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
{/* Exa Category */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Content Category
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('category')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.exa_category || ''}
|
||||
@@ -88,13 +305,22 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
{/* Exa Search Type */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Search Algorithm
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('searchType')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.exa_search_type || 'auto'}
|
||||
@@ -115,8 +341,630 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Number of Results */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Number of Results
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('numResults')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.exa_num_results || optimizedConfig?.exa_num_results || 10}
|
||||
onChange={handleNumResultsChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Date Filters - Published Dates */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Start Published Date (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('dateFilter')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formatDateForInput(config.exa_date_filter || optimizedConfig?.exa_date_filter)}
|
||||
onChange={handleDateFilterChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
End Published Date (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('endPublishedDate')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formatDateForInput(config.exa_end_published_date)}
|
||||
onChange={handleEndPublishedDateChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crawl Date Filters */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Start Crawl Date (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('startCrawlDate')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formatDateForInput(config.exa_start_crawl_date)}
|
||||
onChange={handleStartCrawlDateChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
End Crawl Date (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('endCrawlDate')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formatDateForInput(config.exa_end_crawl_date)}
|
||||
onChange={handleEndCrawlDateChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Filters */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Include Text (optional, max 5 words)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('includeText')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.exa_include_text?.[0] || ''}
|
||||
onChange={handleIncludeTextChange}
|
||||
placeholder="e.g., large language model"
|
||||
maxLength={50}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Exclude Text (optional, max 5 words)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('excludeText')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.exa_exclude_text?.[0] || ''}
|
||||
onChange={handleExcludeTextChange}
|
||||
placeholder="e.g., course tutorial"
|
||||
maxLength={50}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Boolean Options Row */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
marginBottom: '12px',
|
||||
}}>
|
||||
{/* Highlights */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.exa_highlights ?? optimizedConfig?.exa_highlights ?? true}
|
||||
onChange={handleHighlightsChange}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
cursor: 'pointer',
|
||||
flex: 1,
|
||||
}}>
|
||||
Extract Highlights
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('highlights')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Context */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={typeof config.exa_context === 'object' ? true : (config.exa_context ?? (typeof optimizedConfig?.exa_context === 'object' ? true : optimizedConfig?.exa_context ?? true))}
|
||||
onChange={handleContextChange}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
cursor: 'pointer',
|
||||
flex: 1,
|
||||
}}>
|
||||
Return Context String
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('context')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configurable Contents Options */}
|
||||
{config.exa_highlights && (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Highlights: Sentences Per Snippet
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('highlightsNumSentences')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.exa_highlights_num_sentences || 2}
|
||||
onChange={handleHighlightsNumSentencesChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Highlights: Snippets Per URL
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('highlightsPerUrl')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.exa_highlights_per_url || 3}
|
||||
onChange={handleHighlightsPerUrlChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.exa_context && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
}}>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Context: Max Characters (optional, recommended 10,000+)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('contextMaxCharacters')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={config.exa_context_max_characters || ''}
|
||||
onChange={handleContextMaxCharactersChange}
|
||||
placeholder="Leave empty for no limit (recommended)"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Text: Max Characters (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('textMaxCharacters')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={config.exa_text_max_characters || 1000}
|
||||
onChange={handleTextMaxCharactersChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Summary: Custom Query (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('summaryQuery')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.exa_summary_query || ''}
|
||||
onChange={handleSummaryQueryChange}
|
||||
placeholder="e.g., Key insights about..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Queries for Deep Search */}
|
||||
{config.exa_search_type === 'deep' && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Additional Query Variations (Deep Search Only)
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>
|
||||
Provide 2-3 query variations to expand your Deep search. These queries are used alongside the main query for comprehensive results. Deep search will auto-generate variations if not provided.
|
||||
{'\n\n'}
|
||||
Example:
|
||||
{'\n'}• LLM advancements
|
||||
{'\n'}• large language model progress
|
||||
{'\n'}• recent AI breakthroughs
|
||||
{optimizedConfig?.exa_additional_queries_justification && `\n\n🤖 AI Recommendation: ${optimizedConfig.exa_additional_queries_justification}`}
|
||||
</div>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={config.exa_additional_queries?.join(', ') || ''}
|
||||
onChange={handleAdditionalQueriesChange}
|
||||
placeholder="Enter query variations separated by commas or new lines (e.g., LLM advancements, large language model progress)"
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#0f172a',
|
||||
fontFamily: 'inherit',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
/>
|
||||
{optimizedConfig?.exa_additional_queries && optimizedConfig.exa_additional_queries.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '6px',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#f0fdf4',
|
||||
border: '1px solid #86efac',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
color: '#166534',
|
||||
}}>
|
||||
<strong>AI Suggested:</strong> {optimizedConfig.exa_additional_queries.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Domain Filters */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
@@ -125,13 +973,22 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Include Domains (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('includeDomains')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -152,13 +1009,22 @@ export const ExaOptions: React.FC<ExaOptionsProps> = ({ config, onConfigUpdate }
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#6b21a8',
|
||||
}}>
|
||||
Exclude Domains (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTooltipContent('excludeDomains')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -9,11 +9,14 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
PlayArrow as PlayIcon,
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
@@ -32,39 +35,89 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||
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' },
|
||||
}}
|
||||
<Box mt={2}>
|
||||
{/* Guidance Message */}
|
||||
{canExecute && !isExecuting && (
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<InfoIcon />}
|
||||
sx={{
|
||||
mb: 2,
|
||||
backgroundColor: '#e0f2fe',
|
||||
border: '1px solid #bae6fd',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
'& .MuiAlert-message': {
|
||||
color: '#0c4a6e',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" fontWeight={500} gutterBottom>
|
||||
Ready to start research!
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
Review the research question and parameters above. Click "Start Research" to begin gathering information. You can edit any field before starting.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!canExecute && !isExecuting && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{
|
||||
mb: 2,
|
||||
backgroundColor: '#fff7ed',
|
||||
border: '1px solid #fed7aa',
|
||||
'& .MuiAlert-icon': {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
'& .MuiAlert-message': {
|
||||
color: '#92400e',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" fontWeight={500}>
|
||||
Please select at least one research query to proceed.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={2}
|
||||
borderTop="1px solid #e5e7eb"
|
||||
>
|
||||
{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>
|
||||
<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' },
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
{isExecuting ? 'Researching...' : 'Start Research'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
* This is specific to IntentConfirmationPanel and includes AI justifications.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Info as InfoIcon,
|
||||
@@ -21,7 +23,7 @@ import { ProviderAvailability } from '../../../../../api/researchConfig';
|
||||
import { ExaOptions } from '../ExaOptions';
|
||||
import { TavilyOptions } from '../TavilyOptions';
|
||||
import { ProviderChips } from '../ProviderChips';
|
||||
import { ResearchProvider } from '../../../../../services/blogWriterApi';
|
||||
import { ResearchProvider } from '../../../../../services/researchApi';
|
||||
|
||||
interface AdvancedProviderOptionsSectionProps {
|
||||
intentAnalysis: AnalyzeIntentResponse;
|
||||
@@ -40,6 +42,35 @@ export const AdvancedProviderOptionsSection: React.FC<AdvancedProviderOptionsSec
|
||||
showAdvancedOptions,
|
||||
onAdvancedOptionsChange,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<number>(() => {
|
||||
// Initialize tab based on current provider
|
||||
if (config.provider === 'tavily' && providerAvailability.tavily_available) return 1;
|
||||
if (config.provider === 'exa' && providerAvailability.exa_available) return 0;
|
||||
// Default to first available provider
|
||||
if (providerAvailability.exa_available) return 0;
|
||||
if (providerAvailability.tavily_available) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Sync active tab when provider changes externally
|
||||
useEffect(() => {
|
||||
if (config.provider === 'tavily' && providerAvailability.tavily_available) {
|
||||
setActiveTab(1);
|
||||
} else if (config.provider === 'exa' && providerAvailability.exa_available) {
|
||||
setActiveTab(0);
|
||||
}
|
||||
}, [config.provider, providerAvailability.exa_available, providerAvailability.tavily_available]);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
// Update provider based on selected tab
|
||||
if (newValue === 0 && providerAvailability.exa_available) {
|
||||
onConfigUpdate({ provider: 'exa' });
|
||||
} else if (newValue === 1 && providerAvailability.tavily_available) {
|
||||
onConfigUpdate({ provider: 'tavily' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toggle Advanced Options Button */}
|
||||
@@ -127,139 +158,60 @@ export const AdvancedProviderOptionsSection: React.FC<AdvancedProviderOptionsSec
|
||||
)}
|
||||
</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>
|
||||
{/* Provider Tabs */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
'& .MuiTab-root': {
|
||||
textTransform: 'none',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
minHeight: '40px',
|
||||
'&.Mui-selected': {
|
||||
color: '#0ea5e9',
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
backgroundColor: '#0ea5e9',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{providerAvailability.exa_available && (
|
||||
<Tab
|
||||
label="Exa Options"
|
||||
disabled={!providerAvailability.exa_available}
|
||||
/>
|
||||
)}
|
||||
<ExaOptions
|
||||
config={config}
|
||||
onConfigUpdate={onConfigUpdate}
|
||||
/>
|
||||
</>
|
||||
{providerAvailability.tavily_available && (
|
||||
<Tab
|
||||
label="Tavily Options"
|
||||
disabled={!providerAvailability.tavily_available}
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Provider-specific Options */}
|
||||
{activeTab === 0 && providerAvailability.exa_available && (
|
||||
<ExaOptions
|
||||
config={config}
|
||||
onConfigUpdate={onConfigUpdate}
|
||||
optimizedConfig={intentAnalysis?.optimized_config}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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}
|
||||
/>
|
||||
</>
|
||||
{activeTab === 1 && providerAvailability.tavily_available && (
|
||||
<TavilyOptions
|
||||
config={config}
|
||||
onConfigUpdate={onConfigUpdate}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -40,11 +40,27 @@ export const DeliverablesSelector: React.FC<DeliverablesSelectorProps> = ({
|
||||
}}
|
||||
>
|
||||
<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
|
||||
title={
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
|
||||
Research Deliverables
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ mb: 0.5 }}>
|
||||
These are the specific types of information ALwrity will extract from the research results. Click chips to toggle them on/off.
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1, fontStyle: 'italic' }}>
|
||||
Selected deliverables will be highlighted in blue. Unselected ones will be skipped during research analysis.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography variant="caption" color="#666" fontWeight={600} sx={{ cursor: 'help', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
What I'll find for you:
|
||||
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
|
||||
@@ -58,7 +58,47 @@ export const EditableField: React.FC<EditableFieldProps> = ({
|
||||
<Select
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
sx={{ backgroundColor: '#ffffff' }}
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1e293b',
|
||||
border: '1px solid #0ea5e9',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.875rem',
|
||||
'&:hover': {
|
||||
borderColor: '#0284c7',
|
||||
},
|
||||
'& .MuiSelect-select': {
|
||||
color: '#1e293b',
|
||||
padding: '6px 14px',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0284c7',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0284c7',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiMenuItem-root': {
|
||||
color: '#1e293b',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
color: '#0284c7',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
autoFocus
|
||||
>
|
||||
{options.map(opt => (
|
||||
@@ -72,7 +112,24 @@ export const EditableField: React.FC<EditableFieldProps> = ({
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
fullWidth
|
||||
sx={{ backgroundColor: '#ffffff' }}
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiInputBase-input': {
|
||||
color: '#1e293b',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#0284c7',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0284c7',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* EditableListField Component
|
||||
*
|
||||
* Editable list field for managing arrays of strings (e.g., focus areas, also answering).
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Chip,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface EditableListFieldProps {
|
||||
label: string;
|
||||
items: string[];
|
||||
onUpdate: (items: string[]) => void;
|
||||
placeholder?: string;
|
||||
tooltip?: string;
|
||||
maxItems?: number;
|
||||
}
|
||||
|
||||
export const EditableListField: React.FC<EditableListFieldProps> = ({
|
||||
label,
|
||||
items,
|
||||
onUpdate,
|
||||
placeholder = 'Add item...',
|
||||
tooltip,
|
||||
maxItems,
|
||||
}) => {
|
||||
const [newItem, setNewItem] = useState('');
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [editingValue, setEditingValue] = useState('');
|
||||
|
||||
const handleAdd = () => {
|
||||
if (newItem.trim() && (!maxItems || items.length < maxItems)) {
|
||||
onUpdate([...items, newItem.trim()]);
|
||||
setNewItem('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
onUpdate(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleStartEdit = (index: number) => {
|
||||
setEditingIndex(index);
|
||||
setEditingValue(items[index]);
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
if (editingIndex !== null && editingValue.trim()) {
|
||||
const updated = [...items];
|
||||
updated[editingIndex] = editingValue.trim();
|
||||
onUpdate(updated);
|
||||
setEditingIndex(null);
|
||||
setEditingValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingIndex(null);
|
||||
setEditingValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box display="flex" alignItems="center" gap={0.5} mb={1}>
|
||||
<Typography variant="caption" color="#666" fontWeight={600}>
|
||||
{label}:
|
||||
</Typography>
|
||||
{tooltip && (
|
||||
<Tooltip title={tooltip} arrow>
|
||||
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Existing Items */}
|
||||
{items.length > 0 && (
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5} mb={1}>
|
||||
{items.map((item, idx) => (
|
||||
<Chip
|
||||
key={idx}
|
||||
label={
|
||||
editingIndex === idx ? (
|
||||
<TextField
|
||||
value={editingValue}
|
||||
onChange={(e) => setEditingValue(e.target.value)}
|
||||
onBlur={handleSaveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancelEdit();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
size="small"
|
||||
sx={{
|
||||
width: '120px',
|
||||
'& .MuiInputBase-root': {
|
||||
fontSize: '0.75rem',
|
||||
height: '20px',
|
||||
color: '#1e293b',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
color: '#1e293b',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
item
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
onDelete={editingIndex === idx ? undefined : () => handleDelete(idx)}
|
||||
deleteIcon={editingIndex === idx ? undefined : <DeleteIcon sx={{ fontSize: 14 }} />}
|
||||
onClick={() => editingIndex !== idx && handleStartEdit(idx)}
|
||||
sx={{
|
||||
backgroundColor: '#f3f4f6',
|
||||
border: '1px solid #d1d5db',
|
||||
color: '#374151',
|
||||
cursor: editingIndex === idx ? 'default' : 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#e5e7eb',
|
||||
},
|
||||
'& .MuiChip-label': {
|
||||
padding: editingIndex === idx ? '0 4px' : '0 8px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Add New Item */}
|
||||
{(!maxItems || items.length < maxItems) && (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder={placeholder}
|
||||
value={newItem}
|
||||
onChange={(e) => setNewItem(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newItem.trim()) {
|
||||
handleAdd();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleAdd}
|
||||
disabled={!newItem.trim()}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
fontSize: '0.875rem',
|
||||
color: '#1e293b',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
color: '#1e293b',
|
||||
'&::placeholder': {
|
||||
color: '#9ca3af',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#9ca3af',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2,38 +2,61 @@
|
||||
* ExpandableDetails Component
|
||||
*
|
||||
* Collapsible section showing secondary questions, focus areas, and research angles.
|
||||
* Now with editable "Also Answering" and "Focus Areas" sections.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Collapse,
|
||||
} from '@mui/material';
|
||||
import { AnalyzeIntentResponse } from '../../../types/intent.types';
|
||||
import { AnalyzeIntentResponse, ResearchIntent } from '../../../types/intent.types';
|
||||
import { EditableListField } from './EditableListField';
|
||||
|
||||
interface ExpandableDetailsProps {
|
||||
intentAnalysis: AnalyzeIntentResponse;
|
||||
expanded: boolean;
|
||||
intent: ResearchIntent;
|
||||
onUpdateField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
|
||||
}
|
||||
|
||||
export const ExpandableDetails: React.FC<ExpandableDetailsProps> = ({
|
||||
intentAnalysis,
|
||||
expanded,
|
||||
intent,
|
||||
onUpdateField,
|
||||
}) => {
|
||||
const intent = intentAnalysis.intent;
|
||||
|
||||
return (
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ pt: 2, borderTop: '1px solid #e5e7eb', mt: 2 }}>
|
||||
{/* Secondary Questions */}
|
||||
{/* Also Answering (Secondary Questions) - Editable */}
|
||||
<EditableListField
|
||||
label="Also Answering"
|
||||
items={intent.also_answering || []}
|
||||
onUpdate={(items) => onUpdateField('also_answering', items)}
|
||||
placeholder="Add a question or topic to also answer..."
|
||||
tooltip="Additional questions or topics that should be addressed in the research results, even if not explicitly asked."
|
||||
maxItems={10}
|
||||
/>
|
||||
|
||||
{/* Focus Areas - Editable */}
|
||||
<EditableListField
|
||||
label="Focus Areas"
|
||||
items={intent.focus_areas || []}
|
||||
onUpdate={(items) => onUpdateField('focus_areas', items)}
|
||||
placeholder="Add a focus area..."
|
||||
tooltip="Specific aspects or areas to focus on during research (e.g., 'academic research', 'industry trends', 'company analysis')."
|
||||
maxItems={10}
|
||||
/>
|
||||
|
||||
{/* Secondary Questions (Read-only, for reference) */}
|
||||
{intent.secondary_questions.length > 0 && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="caption" color="#666" fontWeight={600} gutterBottom display="block">
|
||||
Also answering:
|
||||
Secondary Questions:
|
||||
</Typography>
|
||||
{intent.secondary_questions.slice(0, 3).map((q, idx) => (
|
||||
{intent.secondary_questions.map((q, idx) => (
|
||||
<Typography key={idx} variant="body2" color="#333" sx={{ ml: 1, mb: 0.5 }}>
|
||||
• {q}
|
||||
</Typography>
|
||||
@@ -41,29 +64,6 @@ export const ExpandableDetails: React.FC<ExpandableDetailsProps> = ({
|
||||
</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}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AnalyzeIntentResponse,
|
||||
ResearchQuery,
|
||||
ExpectedDeliverable,
|
||||
TrendsConfig,
|
||||
} from '../../../types/intent.types';
|
||||
import { ProviderAvailability } from '../../../../../api/researchConfig';
|
||||
|
||||
@@ -31,7 +32,7 @@ export interface IntentConfirmationPanelProps {
|
||||
isAnalyzing: boolean;
|
||||
intentAnalysis: AnalyzeIntentResponse | null;
|
||||
confirmedIntent: ResearchIntent | null;
|
||||
onConfirm: (intent: ResearchIntent) => void;
|
||||
onConfirm: (intent: ResearchIntent, state?: any) => void; // Added optional state parameter
|
||||
onUpdateField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
|
||||
onExecute: (selectedQueries?: ResearchQuery[]) => void;
|
||||
onDismiss: () => void;
|
||||
@@ -41,6 +42,7 @@ export interface IntentConfirmationPanelProps {
|
||||
providerAvailability?: ProviderAvailability | null;
|
||||
config?: any;
|
||||
onConfigUpdate?: (updates: any) => void;
|
||||
wizardState?: any; // Add wizard state for draft saving
|
||||
}
|
||||
|
||||
export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = ({
|
||||
@@ -57,6 +59,7 @@ export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = (
|
||||
providerAvailability,
|
||||
config,
|
||||
onConfigUpdate,
|
||||
wizardState,
|
||||
}) => {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [selectedQueries, setSelectedQueries] = useState<Set<number>>(
|
||||
@@ -65,13 +68,19 @@ export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = (
|
||||
const [editedQueries, setEditedQueries] = useState<ResearchQuery[]>(
|
||||
intentAnalysis?.suggested_queries || []
|
||||
);
|
||||
const [editedTrendsConfig, setEditedTrendsConfig] = useState<TrendsConfig | null>(
|
||||
intentAnalysis?.trends_config || null
|
||||
);
|
||||
|
||||
// Update edited queries when intentAnalysis changes
|
||||
// Update edited queries and trends config when intentAnalysis changes
|
||||
useEffect(() => {
|
||||
if (intentAnalysis?.suggested_queries) {
|
||||
setEditedQueries(intentAnalysis.suggested_queries);
|
||||
setSelectedQueries(new Set(intentAnalysis.suggested_queries.map((_, idx) => idx)));
|
||||
}
|
||||
if (intentAnalysis?.trends_config) {
|
||||
setEditedTrendsConfig(intentAnalysis.trends_config);
|
||||
}
|
||||
}, [intentAnalysis]);
|
||||
|
||||
// Loading state
|
||||
@@ -96,14 +105,30 @@ export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = (
|
||||
|
||||
const handleExecute = () => {
|
||||
const updatedIntent = { ...intent };
|
||||
onConfirm(updatedIntent);
|
||||
// Pass wizard state to onConfirm for draft saving
|
||||
onConfirm(updatedIntent, wizardState);
|
||||
const queriesToUse = Array.from(selectedQueries)
|
||||
.sort((a, b) => a - b)
|
||||
.map(idx => editedQueries[idx])
|
||||
.filter(q => q && q.query.trim().length > 0);
|
||||
|
||||
// Store updated trends config in intentAnalysis for execution
|
||||
// The execution hook will use trends_config from intentAnalysis
|
||||
if (editedTrendsConfig && intentAnalysis) {
|
||||
intentAnalysis.trends_config = editedTrendsConfig;
|
||||
}
|
||||
|
||||
onExecute(queriesToUse);
|
||||
};
|
||||
|
||||
const handleTrendsConfigUpdate = (updatedConfig: TrendsConfig) => {
|
||||
setEditedTrendsConfig(updatedConfig);
|
||||
// Also update intentAnalysis to keep it in sync
|
||||
if (intentAnalysis) {
|
||||
intentAnalysis.trends_config = updatedConfig;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
@@ -152,8 +177,11 @@ export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = (
|
||||
/>
|
||||
|
||||
{/* Google Trends Section */}
|
||||
{intentAnalysis.trends_config && (
|
||||
<TrendsConfigSection trendsConfig={intentAnalysis.trends_config} />
|
||||
{editedTrendsConfig && (
|
||||
<TrendsConfigSection
|
||||
trendsConfig={editedTrendsConfig}
|
||||
onUpdate={handleTrendsConfigUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Advanced Options Section */}
|
||||
@@ -172,6 +200,8 @@ export const IntentConfirmationPanel: React.FC<IntentConfirmationPanelProps> = (
|
||||
<ExpandableDetails
|
||||
intentAnalysis={intentAnalysis}
|
||||
expanded={showDetails}
|
||||
intent={intent}
|
||||
onUpdateField={onUpdateField}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
import {
|
||||
ResearchIntent,
|
||||
ResearchPurpose,
|
||||
@@ -38,11 +42,18 @@ export const IntentSummaryGrid: React.FC<IntentSummaryGridProps> = ({
|
||||
<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' }}>
|
||||
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb', '&:hover': { borderColor: '#0ea5e9', boxShadow: '0 2px 4px rgba(14, 165, 233, 0.1)' }, transition: 'all 0.2s ease' }}>
|
||||
<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>
|
||||
<Tooltip
|
||||
title="The primary goal of your research. This helps ALwrity understand what you're trying to accomplish (e.g., learning, comparing options, making decisions, creating content)."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography variant="caption" color="#666" fontWeight={500} display="flex" alignItems="center" gap={0.5} mb={0.5} sx={{ cursor: 'help' }}>
|
||||
Purpose
|
||||
<InfoIcon sx={{ fontSize: 12, color: '#9ca3af' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<EditableField
|
||||
field="purpose"
|
||||
value={intent.purpose}
|
||||
@@ -56,11 +67,18 @@ export const IntentSummaryGrid: React.FC<IntentSummaryGridProps> = ({
|
||||
|
||||
{/* Content Type */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
|
||||
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb', '&:hover': { borderColor: '#0ea5e9', boxShadow: '0 2px 4px rgba(14, 165, 233, 0.1)' }, transition: 'all 0.2s ease' }}>
|
||||
<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>
|
||||
<Tooltip
|
||||
title="The type of content you're creating with this research. This helps ALwrity tailor the research results and format them appropriately (e.g., blog post, report, presentation, video script)."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography variant="caption" color="#666" fontWeight={500} display="flex" alignItems="center" gap={0.5} mb={0.5} sx={{ cursor: 'help' }}>
|
||||
Creating
|
||||
<InfoIcon sx={{ fontSize: 12, color: '#9ca3af' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<EditableField
|
||||
field="content_output"
|
||||
value={intent.content_output}
|
||||
@@ -74,11 +92,18 @@ export const IntentSummaryGrid: React.FC<IntentSummaryGridProps> = ({
|
||||
|
||||
{/* Depth */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb' }}>
|
||||
<Card variant="outlined" sx={{ height: '100%', backgroundColor: '#ffffff', border: '1px solid #e5e7eb', '&:hover': { borderColor: '#0ea5e9', boxShadow: '0 2px 4px rgba(14, 165, 233, 0.1)' }, transition: 'all 0.2s ease' }}>
|
||||
<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>
|
||||
<Tooltip
|
||||
title="How deep and comprehensive you want the research to be. Overview = quick summary, Detailed = thorough analysis, Expert = in-depth with advanced insights and multiple perspectives."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography variant="caption" color="#666" fontWeight={500} display="flex" alignItems="center" gap={0.5} mb={0.5} sx={{ cursor: 'help' }}>
|
||||
Depth
|
||||
<InfoIcon sx={{ fontSize: 12, color: '#9ca3af' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<EditableField
|
||||
field="depth"
|
||||
value={intent.depth}
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
Edit as EditIcon,
|
||||
Save as SaveIcon,
|
||||
Cancel as CancelIcon,
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { ResearchIntent } from '../../../types/intent.types';
|
||||
|
||||
interface PrimaryQuestionEditorProps {
|
||||
@@ -57,9 +59,16 @@ export const PrimaryQuestionEditor: React.FC<PrimaryQuestionEditorProps> = ({
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="caption" fontWeight={600} color="#0c4a6e">
|
||||
Main Question:
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title="The primary research question that guides the entire research process. This question will be used to generate targeted search queries and extract relevant information."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography variant="caption" fontWeight={600} color="#0c4a6e" sx={{ cursor: 'help', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
Research Question:
|
||||
<InfoIcon sx={{ fontSize: 12, color: '#9ca3af' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
{!isEditing && (
|
||||
<IconButton
|
||||
size="small"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* QueryEditor Component
|
||||
*
|
||||
* Individual query editor with provider, purpose, priority, and expected results.
|
||||
* Compact, professional query editor with tooltips and helpful messaging.
|
||||
* Each query targets a specific deliverable and uses the optimal provider.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -15,9 +16,19 @@ import {
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Chip,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Info as InfoIcon,
|
||||
Search as SearchIcon,
|
||||
Storage as ProviderIcon,
|
||||
Category as PurposeIcon,
|
||||
PriorityHigh as PriorityIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
} from '@mui/icons-material';
|
||||
import {
|
||||
ResearchQuery,
|
||||
@@ -29,121 +40,656 @@ interface QueryEditorProps {
|
||||
query: ResearchQuery;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
isExpanded?: boolean;
|
||||
onToggle: () => void;
|
||||
onEdit: (field: keyof ResearchQuery, value: any) => void;
|
||||
onDelete: () => void;
|
||||
onToggleExpansion?: () => void;
|
||||
totalQueries?: number;
|
||||
selectedCount?: number;
|
||||
estimatedCost?: number;
|
||||
}
|
||||
|
||||
// Provider descriptions for tooltips
|
||||
const PROVIDER_INFO = {
|
||||
exa: {
|
||||
name: 'Exa',
|
||||
description: 'Semantic search engine. Best for deep research, academic papers, and comprehensive content.',
|
||||
color: '#6366f1',
|
||||
},
|
||||
tavily: {
|
||||
name: 'Tavily',
|
||||
description: 'AI-powered real-time search. Best for news, trends, and current events.',
|
||||
color: '#10b981',
|
||||
},
|
||||
google: {
|
||||
name: 'Google',
|
||||
description: 'Factual web search. Best for general information and quick facts.',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
};
|
||||
|
||||
export const QueryEditor: React.FC<QueryEditorProps> = ({
|
||||
query,
|
||||
index,
|
||||
isSelected,
|
||||
isExpanded = true,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggleExpansion,
|
||||
totalQueries = 0,
|
||||
selectedCount = 0,
|
||||
estimatedCost = 0.01,
|
||||
}) => {
|
||||
const providerInfo = PROVIDER_INFO[query.provider as keyof typeof PROVIDER_INFO] || PROVIDER_INFO.exa;
|
||||
const deliverableLabel = DELIVERABLE_DISPLAY[query.purpose as ExpectedDeliverable] || query.purpose;
|
||||
const isFirstQuery = index === 0;
|
||||
|
||||
// Generate justification text based on query properties
|
||||
const getJustification = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Provider justification
|
||||
if (query.provider === 'exa') {
|
||||
parts.push(`This query uses Exa because it's best for finding ${deliverableLabel.toLowerCase()} in academic papers, technical reports, and comprehensive content.`);
|
||||
} else if (query.provider === 'tavily') {
|
||||
parts.push(`This query uses Tavily because it excels at finding ${deliverableLabel.toLowerCase()} in recent news, trends, and real-time information.`);
|
||||
} else {
|
||||
parts.push(`This query uses Google for general factual information and quick answers.`);
|
||||
}
|
||||
|
||||
// Purpose justification
|
||||
parts.push(`It targets "${deliverableLabel}" to extract specific ${deliverableLabel.toLowerCase()} from the research results.`);
|
||||
|
||||
// Priority justification
|
||||
if (query.priority >= 4) {
|
||||
parts.push(`High priority (${query.priority}/5) - this query is essential for answering your research question.`);
|
||||
} else if (query.priority <= 2) {
|
||||
parts.push(`Lower priority (${query.priority}/5) - this query provides supplementary information.`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const getComprehensiveTooltip = () => {
|
||||
return (
|
||||
<Box sx={{ p: 1.5, maxWidth: 400 }}>
|
||||
<Typography variant="caption" fontWeight={700} display="block" gutterBottom sx={{ fontSize: '0.85rem', mb: 1 }}>
|
||||
Research Query #{index + 1}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom sx={{ fontSize: '0.75rem', mt: 1 }}>
|
||||
What is this query?
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 1 }}>
|
||||
{query.query || 'No query text'}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 1, color: '#64748b' }}>
|
||||
This query will search for: {query.expected_results || deliverableLabel}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom sx={{ fontSize: '0.75rem', mt: 1 }}>
|
||||
How was it suggested?
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 1 }}>
|
||||
ALwrity's AI analyzed your research question and automatically generated this query to find the specific information you need. It's designed to target "{deliverableLabel}" using the {providerInfo.name} provider.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom sx={{ fontSize: '0.75rem', mt: 1 }}>
|
||||
Why this query?
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 1 }}>
|
||||
{getJustification()}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom sx={{ fontSize: '0.75rem', mt: 1 }}>
|
||||
What can you do?
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 0.5 }}>
|
||||
• <strong>Select/Deselect:</strong> Check/uncheck to include or exclude this query
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 0.5 }}>
|
||||
• <strong>Edit:</strong> Click on the query text or parameters to modify
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 0.5 }}>
|
||||
• <strong>Delete:</strong> Remove this query if not needed
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 1 }}>
|
||||
• <strong>Change Provider:</strong> Switch between Exa, Tavily, or Google
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 1.5, pt: 1, borderTop: '1px solid rgba(255,255,255,0.2)' }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom sx={{ fontSize: '0.75rem' }}>
|
||||
Cost & Execution
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 0.5 }}>
|
||||
Estimated cost: <strong>${estimatedCost.toFixed(3)}</strong> per query
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', mb: 0.5 }}>
|
||||
Total queries: {totalQueries} ({selectedCount} selected)
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ fontSize: '0.7rem', fontStyle: 'italic', color: '#9ca3af' }}>
|
||||
* Costs are estimates and may vary based on API response length
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Collapsed view (for queries after the first one)
|
||||
if (!isExpanded && !isFirstQuery) {
|
||||
return (
|
||||
<ListItem
|
||||
onMouseEnter={onToggleExpansion}
|
||||
sx={{
|
||||
backgroundColor: isSelected ? '#f0f9ff' : '#ffffff',
|
||||
borderLeft: isSelected ? '4px solid #0ea5e9' : '4px solid transparent',
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? '#bae6fd' : '#e5e7eb',
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
py: 1,
|
||||
px: 2,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected ? '#e0f2fe' : '#f9fafb',
|
||||
borderColor: isSelected ? '#7dd3fc' : '#0ea5e9',
|
||||
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.15)',
|
||||
},
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
mr: 1.5,
|
||||
color: '#0ea5e9',
|
||||
'&.Mui-checked': {
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
<Box flex={1} sx={{ minWidth: 0 }} onClick={onToggleExpansion}>
|
||||
<Box display="flex" alignItems="center" gap={1} mb={0.5}>
|
||||
<SearchIcon sx={{ fontSize: 16, color: '#6b7280', flexShrink: 0 }} />
|
||||
<Tooltip title={getComprehensiveTooltip()} arrow placement="right">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: '#1f2937',
|
||||
fontSize: '0.875rem',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'help',
|
||||
}}
|
||||
>
|
||||
{query.query || 'Empty query'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={0.5} flexWrap="wrap">
|
||||
<Chip
|
||||
size="small"
|
||||
label={providerInfo.name}
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.65rem',
|
||||
backgroundColor: `${providerInfo.color}15`,
|
||||
color: providerInfo.color,
|
||||
border: `1px solid ${providerInfo.color}40`,
|
||||
}}
|
||||
icon={
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: providerInfo.color,
|
||||
ml: 0.5,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={deliverableLabel}
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.65rem',
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#4b5563',
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`~$${estimatedCost.toFixed(3)}`}
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.65rem',
|
||||
backgroundColor: '#fef3c7',
|
||||
color: '#92400e',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Remove this query" arrow>
|
||||
<IconButton
|
||||
edge="end"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
sx={{
|
||||
color: '#dc2626',
|
||||
'&:hover': {
|
||||
backgroundColor: '#fee2e2',
|
||||
color: '#b91c1c',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded view (default for first query, or when hovered/clicked)
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
backgroundColor: isSelected ? '#e0f2fe' : '#ffffff',
|
||||
borderLeft: isSelected ? '3px solid #0ea5e9' : '3px solid transparent',
|
||||
'&:hover': { backgroundColor: isSelected ? '#bae6fd' : '#f9fafb' },
|
||||
backgroundColor: isSelected ? '#f0f9ff' : '#ffffff',
|
||||
borderLeft: isSelected ? '4px solid #0ea5e9' : '4px solid transparent',
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? '#bae6fd' : '#e5e7eb',
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected ? '#e0f2fe' : '#f9fafb',
|
||||
borderColor: isSelected ? '#7dd3fc' : '#d1d5db',
|
||||
},
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={onToggle}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
sx={{
|
||||
mr: 1.5,
|
||||
color: '#0ea5e9',
|
||||
'&.Mui-checked': {
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<Box flex={1} sx={{ minWidth: 0 }}>
|
||||
{/* Query Header with Tooltip */}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Box display="flex" alignItems="center" gap={1} flex={1}>
|
||||
<Tooltip title={getComprehensiveTooltip()} arrow placement="top">
|
||||
<InfoIcon
|
||||
sx={{
|
||||
fontSize: 18,
|
||||
color: '#0ea5e9',
|
||||
cursor: 'help',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 500 }}>
|
||||
Query #{index + 1} • {deliverableLabel} • {providerInfo.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
{!isFirstQuery && onToggleExpansion && (
|
||||
<Tooltip title="Click to collapse" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onToggleExpansion}
|
||||
sx={{
|
||||
color: '#9ca3af',
|
||||
'&:hover': { color: '#0ea5e9' },
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon sx={{ transform: 'rotate(180deg)' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Main Query Input - Enhanced Focus */}
|
||||
<Box display="flex" alignItems="center" gap={1} mb={1.5}>
|
||||
<SearchIcon sx={{ fontSize: 18, color: '#0ea5e9', flexShrink: 0 }} />
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="number"
|
||||
value={query.priority}
|
||||
onChange={(e) => onEdit('priority', parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1, max: 5 }}
|
||||
value={query.query}
|
||||
onChange={(e) => onEdit('query', e.target.value)}
|
||||
placeholder="Research query (e.g., 'AI trends 2024')"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: 90,
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.75rem',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1f2937',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
'& input': {
|
||||
color: '#0c4a6e',
|
||||
py: 1,
|
||||
fontWeight: 500,
|
||||
},
|
||||
'& fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#0284c7',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0284c7',
|
||||
borderWidth: '2.5px',
|
||||
boxShadow: '0 0 0 3px rgba(14, 165, 233, 0.1)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Compact Controls Row */}
|
||||
<Box display="flex" alignItems="center" gap={1.5} flexWrap="wrap">
|
||||
{/* Provider Selector with Tooltip */}
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
|
||||
{providerInfo.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
{providerInfo.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 100,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.8125rem',
|
||||
height: '32px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={query.provider}
|
||||
onChange={(e) => onEdit('provider', e.target.value)}
|
||||
sx={{
|
||||
color: '#1f2937',
|
||||
'& .MuiSelect-select': {
|
||||
color: '#1f2937',
|
||||
py: 0.5,
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
}}
|
||||
startAdornment={
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: providerInfo.color,
|
||||
mr: 1,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItem value="exa" sx={{ fontSize: '0.8125rem', color: '#1f2937' }}>
|
||||
Exa
|
||||
</MenuItem>
|
||||
<MenuItem value="tavily" sx={{ fontSize: '0.8125rem', color: '#1f2937' }}>
|
||||
Tavily
|
||||
</MenuItem>
|
||||
<MenuItem value="google" sx={{ fontSize: '0.8125rem', color: '#1f2937' }}>
|
||||
Google
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
|
||||
{/* Purpose/Deliverable Selector with Tooltip */}
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
|
||||
Deliverable Type
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
What type of information this query will find: statistics, quotes, case studies, trends, etc.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 140,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.8125rem',
|
||||
height: '32px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={query.purpose}
|
||||
onChange={(e) => onEdit('purpose', e.target.value)}
|
||||
sx={{
|
||||
color: '#1f2937',
|
||||
'& .MuiSelect-select': {
|
||||
color: '#1f2937',
|
||||
py: 0.5,
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.entries(DELIVERABLE_DISPLAY).map(([key, label]) => (
|
||||
<MenuItem
|
||||
key={key}
|
||||
value={key}
|
||||
sx={{ fontSize: '0.8125rem', color: '#1f2937' }}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
|
||||
{/* Priority Input with Tooltip */}
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
|
||||
Priority (1-5)
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
Higher priority queries are executed first. Use 5 for most important, 1 for optional.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={query.priority}
|
||||
onChange={(e) => onEdit('priority', parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1, max: 5 }}
|
||||
sx={{
|
||||
width: 75,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.8125rem',
|
||||
height: '32px',
|
||||
color: '#1f2937',
|
||||
'& input': {
|
||||
color: '#1f2937',
|
||||
textAlign: 'center',
|
||||
py: 0.5,
|
||||
},
|
||||
'& fieldset': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
},
|
||||
}}
|
||||
placeholder="1-5"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Expected Results - Compact Display */}
|
||||
{query.expected_results && (
|
||||
<Chip
|
||||
label={query.expected_results}
|
||||
size="small"
|
||||
sx={{
|
||||
height: '24px',
|
||||
fontSize: '0.75rem',
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#4b5563',
|
||||
border: '1px solid #e5e7eb',
|
||||
maxWidth: '200px',
|
||||
'& .MuiChip-label': {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Expected Results Input - Collapsible/Compact */}
|
||||
<Box mt={1}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={query.expected_results || ''}
|
||||
onChange={(e) => onEdit('expected_results', e.target.value)}
|
||||
placeholder="What we expect to find (optional)"
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={2}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1f2937',
|
||||
fontSize: '0.8125rem',
|
||||
'& textarea': {
|
||||
color: '#1f2937',
|
||||
py: 0.5,
|
||||
},
|
||||
'& fieldset': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#0ea5e9',
|
||||
},
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Tooltip
|
||||
title="Describe what type of information this query should find. This helps the AI understand the research goal."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
color: '#9ca3af',
|
||||
cursor: 'help',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
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>
|
||||
<Tooltip title="Remove this query" arrow>
|
||||
<IconButton
|
||||
edge="end"
|
||||
size="small"
|
||||
onClick={onDelete}
|
||||
sx={{
|
||||
color: '#dc2626',
|
||||
'&:hover': {
|
||||
backgroundColor: '#fee2e2',
|
||||
color: '#b91c1c',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* ResearchQueriesSection Component
|
||||
*
|
||||
* Accordion section for managing research queries (add, edit, delete, select).
|
||||
* Enhanced with expand/collapse functionality and cost breakdown.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -15,10 +16,12 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Chip,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Search as SearchIcon,
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
import {
|
||||
ResearchQuery,
|
||||
@@ -32,6 +35,13 @@ interface ResearchQueriesSectionProps {
|
||||
onSelectionChange: (selected: Set<number>) => void;
|
||||
}
|
||||
|
||||
// Cost estimation per provider (approximate)
|
||||
const PROVIDER_COST_ESTIMATE = {
|
||||
exa: 0.02, // ~$0.02 per query
|
||||
tavily: 0.015, // ~$0.015 per query
|
||||
google: 0.001, // ~$0.001 per query (Gemini grounding)
|
||||
};
|
||||
|
||||
export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
|
||||
queries,
|
||||
selectedQueries,
|
||||
@@ -39,6 +49,7 @@ export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
|
||||
onSelectionChange,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [expandedQueries, setExpandedQueries] = useState<Set<number>>(new Set([0])); // First query expanded by default
|
||||
|
||||
const handleQueryToggle = (index: number) => {
|
||||
const newSelected = new Set(selectedQueries);
|
||||
@@ -85,8 +96,43 @@ export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
|
||||
const newSelected = new Set(selectedQueries);
|
||||
newSelected.add(queries.length);
|
||||
onSelectionChange(newSelected);
|
||||
// Expand the new query
|
||||
setExpandedQueries(new Set([...expandedQueries, queries.length]));
|
||||
};
|
||||
|
||||
const handleToggleQueryExpansion = (index: number) => {
|
||||
const newExpanded = new Set(expandedQueries);
|
||||
if (newExpanded.has(index)) {
|
||||
newExpanded.delete(index);
|
||||
} else {
|
||||
newExpanded.add(index);
|
||||
}
|
||||
setExpandedQueries(newExpanded);
|
||||
};
|
||||
|
||||
// Calculate cost breakdown
|
||||
const costBreakdown = useMemo(() => {
|
||||
const selected = Array.from(selectedQueries);
|
||||
let totalCost = 0;
|
||||
const providerCosts: Record<string, number> = {};
|
||||
|
||||
selected.forEach(idx => {
|
||||
const query = queries[idx];
|
||||
if (query) {
|
||||
const cost = PROVIDER_COST_ESTIMATE[query.provider] || 0.01;
|
||||
totalCost += cost;
|
||||
providerCosts[query.provider] = (providerCosts[query.provider] || 0) + cost;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
total: totalCost,
|
||||
perQuery: selected.length > 0 ? totalCost / selected.length : 0,
|
||||
providerCosts,
|
||||
queryCount: selected.length,
|
||||
};
|
||||
}, [selectedQueries, queries]);
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
expanded={expanded}
|
||||
@@ -123,6 +169,48 @@ export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
|
||||
fontWeight: 500,
|
||||
}}
|
||||
/>
|
||||
{selectedQueries.size > 0 && (
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="caption" fontWeight={600} display="block" gutterBottom>
|
||||
Cost Breakdown
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ mb: 0.5 }}>
|
||||
Estimated cost: ${costBreakdown.total.toFixed(3)}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" sx={{ mb: 0.5 }}>
|
||||
Queries to fire: {costBreakdown.queryCount}
|
||||
</Typography>
|
||||
{Object.entries(costBreakdown.providerCosts).map(([provider, cost]) => (
|
||||
<Typography key={provider} variant="caption" display="block" sx={{ fontSize: '0.7rem' }}>
|
||||
{provider}: ${cost.toFixed(3)}
|
||||
</Typography>
|
||||
))}
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1, fontStyle: 'italic', fontSize: '0.7rem' }}>
|
||||
* Estimates may vary based on actual API usage
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`~$${costBreakdown.total.toFixed(3)}`}
|
||||
sx={{
|
||||
ml: 1,
|
||||
backgroundColor: '#fef3c7',
|
||||
color: '#92400e',
|
||||
fontSize: '0.7rem',
|
||||
height: 20,
|
||||
fontWeight: 500,
|
||||
cursor: 'help',
|
||||
}}
|
||||
icon={<InfoIcon sx={{ fontSize: 12 }} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, backgroundColor: '#ffffff' }}>
|
||||
@@ -133,9 +221,14 @@ export const ResearchQueriesSection: React.FC<ResearchQueriesSectionProps> = ({
|
||||
query={query}
|
||||
index={idx}
|
||||
isSelected={selectedQueries.has(idx)}
|
||||
isExpanded={expandedQueries.has(idx)}
|
||||
onToggle={() => handleQueryToggle(idx)}
|
||||
onEdit={(field, value) => handleQueryEdit(idx, field, value)}
|
||||
onDelete={() => handleDeleteQuery(idx)}
|
||||
onToggleExpansion={() => handleToggleQueryExpansion(idx)}
|
||||
totalQueries={queries.length}
|
||||
selectedCount={selectedQueries.size}
|
||||
estimatedCost={PROVIDER_COST_ESTIMATE[query.provider] || 0.01}
|
||||
/>
|
||||
{idx < queries.length - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* TrendsConfigSection Component
|
||||
*
|
||||
* Google Trends configuration section with keywords, expected insights, and settings.
|
||||
* Enhanced with editing capabilities and educational modal.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
TextField,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
@@ -19,26 +19,125 @@ import {
|
||||
Grid,
|
||||
Chip,
|
||||
Tooltip,
|
||||
TextField,
|
||||
IconButton,
|
||||
FormControl,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
TrendingUp as TrendIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
Info as InfoIcon,
|
||||
Edit as EditIcon,
|
||||
Save as SaveIcon,
|
||||
Cancel as CancelIcon,
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
HelpOutline as HelpIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { TrendsConfig } from '../../../types/intent.types';
|
||||
import { TrendsKnowMoreModal } from './TrendsKnowMoreModal';
|
||||
|
||||
interface TrendsConfigSectionProps {
|
||||
trendsConfig: TrendsConfig;
|
||||
onUpdate?: (updatedConfig: TrendsConfig) => void;
|
||||
}
|
||||
|
||||
// Common timeframe options
|
||||
const TIMEFRAME_OPTIONS = [
|
||||
{ value: 'today 12-m', label: 'Past 12 months' },
|
||||
{ value: 'today 3-m', label: 'Past 3 months' },
|
||||
{ value: 'today 1-m', label: 'Past month' },
|
||||
{ value: 'today 7-d', label: 'Past 7 days' },
|
||||
{ value: 'today 5-y', label: 'Past 5 years' },
|
||||
];
|
||||
|
||||
// Common region options (top regions)
|
||||
const REGION_OPTIONS = [
|
||||
{ value: 'US', label: 'United States' },
|
||||
{ value: 'GB', label: 'United Kingdom' },
|
||||
{ value: 'CA', label: 'Canada' },
|
||||
{ value: 'AU', label: 'Australia' },
|
||||
{ value: 'DE', label: 'Germany' },
|
||||
{ value: 'FR', label: 'France' },
|
||||
{ value: 'IN', label: 'India' },
|
||||
{ value: 'JP', label: 'Japan' },
|
||||
{ value: 'BR', label: 'Brazil' },
|
||||
{ value: 'MX', label: 'Mexico' },
|
||||
{ value: 'ES', label: 'Spain' },
|
||||
{ value: 'IT', label: 'Italy' },
|
||||
{ value: 'NL', label: 'Netherlands' },
|
||||
{ value: 'SE', label: 'Sweden' },
|
||||
{ value: 'NO', label: 'Norway' },
|
||||
];
|
||||
|
||||
export const TrendsConfigSection: React.FC<TrendsConfigSectionProps> = ({
|
||||
trendsConfig,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const [showKnowMoreModal, setShowKnowMoreModal] = useState(false);
|
||||
const [editingKeywords, setEditingKeywords] = useState(false);
|
||||
const [editingTimeframe, setEditingTimeframe] = useState(false);
|
||||
const [editingRegion, setEditingRegion] = useState(false);
|
||||
const [editedKeywords, setEditedKeywords] = useState<string[]>(trendsConfig.keywords);
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
const [editedTimeframe, setEditedTimeframe] = useState(trendsConfig.timeframe);
|
||||
const [editedRegion, setEditedRegion] = useState(trendsConfig.geo);
|
||||
|
||||
if (!trendsConfig.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSaveKeywords = () => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
...trendsConfig,
|
||||
keywords: editedKeywords.filter(k => k.trim().length > 0),
|
||||
});
|
||||
}
|
||||
setEditingKeywords(false);
|
||||
};
|
||||
|
||||
const handleCancelKeywords = () => {
|
||||
setEditedKeywords(trendsConfig.keywords);
|
||||
setEditingKeywords(false);
|
||||
setNewKeyword('');
|
||||
};
|
||||
|
||||
const handleAddKeyword = () => {
|
||||
if (newKeyword.trim()) {
|
||||
setEditedKeywords([...editedKeywords, newKeyword.trim()]);
|
||||
setNewKeyword('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKeyword = (index: number) => {
|
||||
setEditedKeywords(editedKeywords.filter((_, idx) => idx !== index));
|
||||
};
|
||||
|
||||
const handleSaveTimeframe = () => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
...trendsConfig,
|
||||
timeframe: editedTimeframe,
|
||||
});
|
||||
}
|
||||
setEditingTimeframe(false);
|
||||
};
|
||||
|
||||
const handleSaveRegion = () => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
...trendsConfig,
|
||||
geo: editedRegion,
|
||||
});
|
||||
}
|
||||
setEditingRegion(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
@@ -77,50 +176,189 @@ export const TrendsConfigSection: React.FC<TrendsConfigSectionProps> = ({
|
||||
</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' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/* Trends Keywords - Editable */}
|
||||
<Box
|
||||
mb={2}
|
||||
sx={{
|
||||
padding: '12px',
|
||||
background: 'rgba(241, 245, 249, 0.5)',
|
||||
border: '1px solid rgba(203, 213, 225, 0.3)',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1.5}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#475569',
|
||||
}}
|
||||
>
|
||||
Trends Keywords ({editingKeywords ? editedKeywords.length : trendsConfig.keywords.length})
|
||||
</Typography>
|
||||
<Chip
|
||||
label="Know More"
|
||||
size="small"
|
||||
onClick={() => setShowKnowMoreModal(true)}
|
||||
icon={<HelpIcon sx={{ fontSize: 14 }} />}
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.7rem',
|
||||
backgroundColor: '#e0f2fe',
|
||||
color: '#0369a1',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: '#bae6fd',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!editingKeywords && onUpdate && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingKeywords(true)}
|
||||
sx={{
|
||||
color: '#64748b',
|
||||
'&:hover': { color: '#0ea5e9', backgroundColor: '#f0f9ff' },
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{editingKeywords ? (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
{editedKeywords.map((keyword, idx) => (
|
||||
<Chip
|
||||
key={idx}
|
||||
label={keyword}
|
||||
onDelete={() => handleDeleteKeyword(idx)}
|
||||
sx={{
|
||||
padding: '5px 10px',
|
||||
background: 'white',
|
||||
border: '1px solid rgba(203, 213, 225, 0.5)',
|
||||
fontSize: '12px',
|
||||
color: '#334155',
|
||||
fontWeight: 500,
|
||||
'& .MuiChip-deleteIcon': {
|
||||
color: '#dc2626',
|
||||
fontSize: '16px',
|
||||
'&:hover': { color: '#b91c1c' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" gap={1} mb={1}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Add keyword"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddKeyword();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
flex: 1,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '0.8125rem',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleAddKeyword}
|
||||
disabled={!newKeyword.trim()}
|
||||
sx={{
|
||||
backgroundColor: '#0ea5e9',
|
||||
color: 'white',
|
||||
'&:hover': { backgroundColor: '#0284c7' },
|
||||
'&:disabled': { backgroundColor: '#d1d5db' },
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box display="flex" gap={1} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleCancelKeywords}
|
||||
sx={{ color: '#64748b' }}
|
||||
>
|
||||
<CancelIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleSaveKeywords}
|
||||
sx={{
|
||||
color: '#10b981',
|
||||
'&:hover': { backgroundColor: '#dcfce7' },
|
||||
}}
|
||||
>
|
||||
<SaveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
{trendsConfig.keywords.map((keyword, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{
|
||||
padding: '5px 10px',
|
||||
background: 'white',
|
||||
border: '1px solid rgba(203, 213, 225, 0.5)',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#334155',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{keyword}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{trendsConfig.keywords_justification && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
marginTop: '8px',
|
||||
color: '#64748b',
|
||||
fontSize: '11px',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
{trendsConfig.keywords_justification}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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 */}
|
||||
{/* Settings with Justifications - Editable */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
@@ -129,35 +367,156 @@ export const TrendsConfigSection: React.FC<TrendsConfigSectionProps> = ({
|
||||
border: '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={1}>
|
||||
<Grid container spacing={2}>
|
||||
<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}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={0.5}>
|
||||
<Typography variant="caption" color="#666" fontWeight={500}>
|
||||
Timeframe
|
||||
</Typography>
|
||||
<Tooltip title={trendsConfig.timeframe_justification} arrow>
|
||||
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
{!editingTimeframe && onUpdate && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingTimeframe(true)}
|
||||
sx={{
|
||||
color: '#64748b',
|
||||
'&:hover': { color: '#0ea5e9', backgroundColor: '#f0f9ff' },
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
{editingTimeframe ? (
|
||||
<Box>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={editedTimeframe}
|
||||
onChange={(e) => setEditedTimeframe(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.8125rem',
|
||||
}}
|
||||
>
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box display="flex" gap={0.5} mt={1} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditedTimeframe(trendsConfig.timeframe);
|
||||
setEditingTimeframe(false);
|
||||
}}
|
||||
sx={{ color: '#64748b' }}
|
||||
>
|
||||
<CancelIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleSaveTimeframe}
|
||||
sx={{
|
||||
color: '#10b981',
|
||||
'&:hover': { backgroundColor: '#dcfce7' },
|
||||
}}
|
||||
>
|
||||
<SaveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<Typography variant="body2" fontWeight={500} color="#333">
|
||||
{TIMEFRAME_OPTIONS.find(opt => opt.value === trendsConfig.timeframe)?.label || trendsConfig.timeframe}
|
||||
</Typography>
|
||||
<Tooltip title={trendsConfig.timeframe_justification || 'Time period for trends analysis'} 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}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={0.5}>
|
||||
<Typography variant="caption" color="#666" fontWeight={500}>
|
||||
Region
|
||||
</Typography>
|
||||
<Tooltip title={trendsConfig.geo_justification} arrow>
|
||||
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
{!editingRegion && onUpdate && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingRegion(true)}
|
||||
sx={{
|
||||
color: '#64748b',
|
||||
'&:hover': { color: '#0ea5e9', backgroundColor: '#f0f9ff' },
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
{editingRegion ? (
|
||||
<Box>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={editedRegion}
|
||||
onChange={(e) => setEditedRegion(e.target.value)}
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: '0.8125rem',
|
||||
}}
|
||||
>
|
||||
{REGION_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box display="flex" gap={0.5} mt={1} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditedRegion(trendsConfig.geo);
|
||||
setEditingRegion(false);
|
||||
}}
|
||||
sx={{ color: '#64748b' }}
|
||||
>
|
||||
<CancelIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleSaveRegion}
|
||||
sx={{
|
||||
color: '#10b981',
|
||||
'&:hover': { backgroundColor: '#dcfce7' },
|
||||
}}
|
||||
>
|
||||
<SaveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<Typography variant="body2" fontWeight={500} color="#333">
|
||||
{REGION_OPTIONS.find(opt => opt.value === trendsConfig.geo)?.label || trendsConfig.geo}
|
||||
</Typography>
|
||||
<Tooltip title={trendsConfig.geo_justification || 'Geographic region for trends analysis'} arrow>
|
||||
<InfoIcon sx={{ fontSize: 14, color: '#9ca3af', cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Know More Modal */}
|
||||
<TrendsKnowMoreModal
|
||||
open={showKnowMoreModal}
|
||||
onClose={() => setShowKnowMoreModal(false)}
|
||||
trendsConfig={trendsConfig}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
TrendingUp as TrendIcon,
|
||||
Info as InfoIcon,
|
||||
AutoAwesome as AIIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { TrendsConfig } from '../../../types/intent.types';
|
||||
|
||||
interface TrendsKnowMoreModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
trendsConfig: TrendsConfig;
|
||||
}
|
||||
|
||||
export const TrendsKnowMoreModal: React.FC<TrendsKnowMoreModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
trendsConfig,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
pb: 2,
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<TrendIcon sx={{ color: '#10b981', fontSize: 28 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={700} color="#166534">
|
||||
Google Trends Analysis
|
||||
</Typography>
|
||||
<Typography variant="caption" color="#166534" sx={{ opacity: 0.8 }}>
|
||||
Understanding search interest and market trends
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
p: 0.5,
|
||||
color: '#64748b',
|
||||
'&:hover': { backgroundColor: 'rgba(0,0,0,0.05)' },
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 3 }}>
|
||||
{/* What is Google Trends? */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" fontWeight={700} color="#0c4a6e" gutterBottom>
|
||||
What is Google Trends?
|
||||
</Typography>
|
||||
<Typography variant="body2" color="#475569" paragraph>
|
||||
Google Trends is a free tool that shows how often specific search terms are entered into Google's search engine
|
||||
relative to the total search volume. It provides insights into search interest over time, regional popularity,
|
||||
and related topics/queries.
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
p: 2,
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderRadius: 1,
|
||||
border: '1px solid #bae6fd',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="start" gap={1}>
|
||||
<InfoIcon sx={{ color: '#0ea5e9', fontSize: 20, mt: 0.5 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" fontWeight={600} color="#0c4a6e" display="block" gutterBottom>
|
||||
Real-World Example
|
||||
</Typography>
|
||||
<Typography variant="caption" color="#0369a1">
|
||||
If you're researching "AI content generation", Google Trends shows you when interest peaked, which regions
|
||||
show the most interest, and what related topics people are searching for. This helps you understand market
|
||||
timing and content opportunities.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* What Happens in the Backend? */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" fontWeight={700} color="#0c4a6e" gutterBottom>
|
||||
What Happens in the Backend?
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e0f2fe',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
color: '#0ea5e9',
|
||||
}}
|
||||
>
|
||||
1
|
||||
</Box>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="API Request"
|
||||
secondary="ALwrity sends your keywords to Google Trends API with the specified timeframe and region"
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 600, color: '#1e293b' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#64748b' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e0f2fe',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
color: '#0ea5e9',
|
||||
}}
|
||||
>
|
||||
2
|
||||
</Box>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Data Retrieval"
|
||||
secondary="Google Trends returns interest over time, regional distribution, related topics, and related queries"
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 600, color: '#1e293b' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#64748b' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e0f2fe',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
color: '#0ea5e9',
|
||||
}}
|
||||
>
|
||||
3
|
||||
</Box>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="AI Analysis"
|
||||
secondary="ALwrity's AI analyzes the trends data to identify patterns, opportunities, and optimal timing for content publication"
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 600, color: '#1e293b' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#64748b' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e0f2fe',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
color: '#0ea5e9',
|
||||
}}
|
||||
>
|
||||
4
|
||||
</Box>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Integration"
|
||||
secondary="Trends insights are integrated into your research results, providing context for content timing, regional targeting, and topic expansion"
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 600, color: '#1e293b' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#64748b' }}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Why is it Important? */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" fontWeight={700} color="#0c4a6e" gutterBottom>
|
||||
Why is Google Trends Important?
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: '#fef3c7',
|
||||
borderRadius: 1,
|
||||
border: '1px solid #fde68a',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="#92400e" fontWeight={600} gutterBottom>
|
||||
🎯 Strategic Benefits:
|
||||
</Typography>
|
||||
<List dense sx={{ mt: 1 }}>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckIcon sx={{ color: '#10b981', fontSize: 18 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Timing Optimization"
|
||||
secondary="Publish content when search interest is highest for maximum visibility"
|
||||
primaryTypographyProps={{ variant: 'caption', fontWeight: 600, color: '#92400e' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#78350f' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckIcon sx={{ color: '#10b981', fontSize: 18 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Regional Targeting"
|
||||
secondary="Understand which regions show the most interest to tailor content accordingly"
|
||||
primaryTypographyProps={{ variant: 'caption', fontWeight: 600, color: '#92400e' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#78350f' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckIcon sx={{ color: '#10b981', fontSize: 18 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Content Expansion"
|
||||
secondary="Discover related topics and queries to expand your content strategy"
|
||||
primaryTypographyProps={{ variant: 'caption', fontWeight: 600, color: '#92400e' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#78350f' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ px: 0, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckIcon sx={{ color: '#10b981', fontSize: 18 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Market Intelligence"
|
||||
secondary="Gain insights into market trends, emerging topics, and competitor interest"
|
||||
primaryTypographyProps={{ variant: 'caption', fontWeight: 600, color: '#92400e' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#78350f' }}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* What Trends Will Uncover */}
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
||||
<AIIcon sx={{ color: '#0ea5e9', fontSize: 24 }} />
|
||||
<Typography variant="subtitle1" fontWeight={700} color="#0c4a6e">
|
||||
What Trends Will Uncover
|
||||
</Typography>
|
||||
</Box>
|
||||
{trendsConfig.expected_insights.length > 0 ? (
|
||||
<List dense sx={{ backgroundColor: '#f9fafb', borderRadius: 1, p: 1 }}>
|
||||
{trendsConfig.expected_insights.map((insight, idx) => (
|
||||
<ListItem key={idx} sx={{ py: 0.75, px: 1 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
<CheckIcon color="success" fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={insight}
|
||||
primaryTypographyProps={{ variant: 'body2', color: '#374151', fontWeight: 500 }}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Typography variant="body2" color="#64748b" sx={{ fontStyle: 'italic' }}>
|
||||
No specific insights configured for this research.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, py: 2, borderTop: '1px solid #e5e7eb', backgroundColor: '#f9fafb' }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: '#10b981',
|
||||
'&:hover': { backgroundColor: '#059669' },
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Got it!
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Tooltip, CircularProgress } from '@mui/material';
|
||||
import { Tooltip, CircularProgress, Select, MenuItem, FormControl, InputLabel } from '@mui/material';
|
||||
import { Psychology as BrainIcon, Settings as SettingsIcon, Info as InfoIcon } from '@mui/icons-material';
|
||||
import { ResearchPurpose, ContentOutput, ResearchDepthLevel } from '../../types/intent.types';
|
||||
|
||||
interface ResearchInputContainerProps {
|
||||
keywords: string[];
|
||||
@@ -11,8 +12,46 @@ interface ResearchInputContainerProps {
|
||||
isAnalyzingIntent?: boolean;
|
||||
hasIntentAnalysis?: boolean;
|
||||
intentConfidence?: number;
|
||||
// User-provided intent settings
|
||||
userPurpose?: ResearchPurpose;
|
||||
userContentOutput?: ContentOutput;
|
||||
userDepth?: ResearchDepthLevel;
|
||||
onPurposeChange?: (purpose: ResearchPurpose) => void;
|
||||
onContentOutputChange?: (output: ContentOutput) => void;
|
||||
onDepthChange?: (depth: ResearchDepthLevel) => void;
|
||||
}
|
||||
|
||||
const PURPOSE_OPTIONS: { value: ResearchPurpose; label: string }[] = [
|
||||
{ value: 'learn', label: 'Learn' },
|
||||
{ value: 'create_content', label: 'Create Content' },
|
||||
{ value: 'make_decision', label: 'Make Decision' },
|
||||
{ value: 'compare', label: 'Compare' },
|
||||
{ value: 'solve_problem', label: 'Solve Problem' },
|
||||
{ value: 'find_data', label: 'Find Data' },
|
||||
{ value: 'explore_trends', label: 'Explore Trends' },
|
||||
{ value: 'validate', label: 'Validate' },
|
||||
{ value: 'generate_ideas', label: 'Generate Ideas' },
|
||||
];
|
||||
|
||||
const CONTENT_OUTPUT_OPTIONS: { value: ContentOutput; label: string }[] = [
|
||||
{ value: 'blog', label: 'Blog Post' },
|
||||
{ value: 'podcast', label: 'Podcast' },
|
||||
{ value: 'video', label: 'Video' },
|
||||
{ value: 'social_post', label: 'Social Post' },
|
||||
{ value: 'newsletter', label: 'Newsletter' },
|
||||
{ value: 'presentation', label: 'Presentation' },
|
||||
{ value: 'report', label: 'Report' },
|
||||
{ value: 'whitepaper', label: 'Whitepaper' },
|
||||
{ value: 'email', label: 'Email' },
|
||||
{ value: 'general', label: 'General' },
|
||||
];
|
||||
|
||||
const DEPTH_OPTIONS: { value: ResearchDepthLevel; label: string }[] = [
|
||||
{ value: 'overview', label: 'Overview' },
|
||||
{ value: 'detailed', label: 'Detailed' },
|
||||
{ value: 'expert', label: 'Expert-Level' },
|
||||
];
|
||||
|
||||
export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
|
||||
keywords,
|
||||
placeholder,
|
||||
@@ -21,6 +60,12 @@ export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
|
||||
isAnalyzingIntent = false,
|
||||
hasIntentAnalysis = false,
|
||||
intentConfidence = 0,
|
||||
userPurpose,
|
||||
userContentOutput,
|
||||
userDepth,
|
||||
onPurposeChange,
|
||||
onContentOutputChange,
|
||||
onDepthChange,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [wordCount, setWordCount] = useState(0);
|
||||
@@ -106,7 +151,7 @@ export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
|
||||
style={{
|
||||
width: '100%',
|
||||
flex: '1',
|
||||
minHeight: '195px', // Reduced by 35% from 300px
|
||||
minHeight: '150px', // Reduced to make room for controls
|
||||
padding: '12px',
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.7',
|
||||
@@ -124,6 +169,187 @@ export const ResearchInputContainer: React.FC<ResearchInputContainerProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Compact Select Controls - Purpose, Creating, Depth */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
padding: '8px 0',
|
||||
borderTop: '1px solid rgba(14, 165, 233, 0.1)',
|
||||
borderBottom: '1px solid rgba(14, 165, 233, 0.1)',
|
||||
marginTop: '8px',
|
||||
}}>
|
||||
{/* Purpose */}
|
||||
<FormControl size="small" sx={{ flex: 1, minWidth: 100 }}>
|
||||
<Select
|
||||
value={userPurpose || ''}
|
||||
onChange={(e) => onPurposeChange?.(e.target.value as ResearchPurpose)}
|
||||
displayEmpty
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
height: '32px',
|
||||
'& .MuiSelect-select': {
|
||||
padding: '6px 10px',
|
||||
color: '#1e293b',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#9ca3af',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
borderWidth: '1.5px',
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: '#64748b',
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiMenuItem-root': {
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
color: '#0284c7',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
<em style={{ color: '#9ca3af', fontSize: '0.75rem' }}>Purpose</em>
|
||||
</MenuItem>
|
||||
{PURPOSE_OPTIONS.map(opt => (
|
||||
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Creating (Content Output) */}
|
||||
<FormControl size="small" sx={{ flex: 1, minWidth: 100 }}>
|
||||
<Select
|
||||
value={userContentOutput || ''}
|
||||
onChange={(e) => onContentOutputChange?.(e.target.value as ContentOutput)}
|
||||
displayEmpty
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
height: '32px',
|
||||
'& .MuiSelect-select': {
|
||||
padding: '6px 10px',
|
||||
color: '#1e293b',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#9ca3af',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
borderWidth: '1.5px',
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: '#64748b',
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiMenuItem-root': {
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
color: '#0284c7',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
<em style={{ color: '#9ca3af', fontSize: '0.75rem' }}>Creating</em>
|
||||
</MenuItem>
|
||||
{CONTENT_OUTPUT_OPTIONS.map(opt => (
|
||||
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Depth */}
|
||||
<FormControl size="small" sx={{ flex: 1, minWidth: 100 }}>
|
||||
<Select
|
||||
value={userDepth || ''}
|
||||
onChange={(e) => onDepthChange?.(e.target.value as ResearchDepthLevel)}
|
||||
displayEmpty
|
||||
sx={{
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
height: '32px',
|
||||
'& .MuiSelect-select': {
|
||||
padding: '6px 10px',
|
||||
color: '#1e293b',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#d1d5db',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#9ca3af',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: '#0ea5e9',
|
||||
borderWidth: '1.5px',
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: '#64748b',
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: '#ffffff',
|
||||
'& .MuiMenuItem-root': {
|
||||
color: '#1e293b',
|
||||
fontSize: '0.75rem',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
color: '#0284c7',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
<em style={{ color: '#9ca3af', fontSize: '0.75rem' }}>Depth</em>
|
||||
</MenuItem>
|
||||
{DEPTH_OPTIONS.map(opt => (
|
||||
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar with word count and Intent & Options button */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ResearchConfig } from '../../../../services/blogWriterApi';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { ResearchConfig } from '../../../../services/researchApi';
|
||||
import {
|
||||
tavilyTopics,
|
||||
tavilySearchDepths,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
tavilyAnswerOptions,
|
||||
tavilyRawContentOptions
|
||||
} from '../utils/constants';
|
||||
import { getTavilyTooltipContent } from './utils/tavilyTooltips';
|
||||
|
||||
interface TavilyOptionsProps {
|
||||
config: ResearchConfig;
|
||||
@@ -130,13 +132,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Tavily Topic */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Search Topic
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('topic', config.tavily_topic)}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.tavily_topic || 'general'}
|
||||
@@ -161,13 +172,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Tavily Search Depth */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Search Depth
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('searchDepth', config.tavily_search_depth)}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.tavily_search_depth || 'basic'}
|
||||
@@ -192,13 +212,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Tavily Include Answer */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
AI Answer
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeAnswer', typeof config.tavily_include_answer === 'string' ? config.tavily_include_answer : config.tavily_include_answer ? 'true' : 'false')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.tavily_include_answer === true ? 'true' : typeof config.tavily_include_answer === 'string' ? config.tavily_include_answer : 'false'}
|
||||
@@ -223,13 +252,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Tavily Time Range */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Time Range
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('timeRange', config.tavily_time_range)}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.tavily_time_range || ''}
|
||||
@@ -260,13 +298,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Include Domains (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeDomains')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -287,13 +334,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Exclude Domains (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('excludeDomains')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -323,13 +379,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Include Raw Content */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Raw Content Format
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeRawContent', typeof config.tavily_include_raw_content === 'string' ? config.tavily_include_raw_content : config.tavily_include_raw_content ? 'true' : 'false')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={config.tavily_include_raw_content === true ? 'true' : typeof config.tavily_include_raw_content === 'string' ? config.tavily_include_raw_content : 'false'}
|
||||
@@ -354,13 +419,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
{/* Country */}
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Country Code (optional)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('country')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -379,17 +453,26 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chunks Per Source (only for advanced) */}
|
||||
{config.tavily_search_depth === 'advanced' && (
|
||||
{/* Chunks Per Source (only for advanced or fast) */}
|
||||
{(config.tavily_search_depth === 'advanced' || (config.tavily_search_depth as string) === 'fast') && (
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Chunks Per Source (1-3)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('chunksPerSource')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -420,13 +503,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
}}>
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
Start Date (YYYY-MM-DD)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('startDate')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -446,13 +538,22 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
|
||||
<div>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
marginBottom: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0ea5e9',
|
||||
}}>
|
||||
End Date (YYYY-MM-DD)
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('endDate')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -496,7 +597,16 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<span>Include Images</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
Include Images
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeImages')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label style={{
|
||||
@@ -519,7 +629,16 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
opacity: config.tavily_include_images ? 1 : 0.5,
|
||||
}}
|
||||
/>
|
||||
<span>Include Image Descriptions</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
Include Image Descriptions
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeImageDescriptions')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label style={{
|
||||
@@ -540,7 +659,16 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<span>Include Favicon URLs</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
Include Favicon URLs
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('includeFavicon')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label style={{
|
||||
@@ -561,7 +689,16 @@ export const TavilyOptions: React.FC<TavilyOptionsProps> = ({ config, onConfigUp
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<span>Auto-Configure Parameters</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
Auto-Configure Parameters
|
||||
<Tooltip
|
||||
title={<div style={{ whiteSpace: 'pre-line', fontSize: '12px', lineHeight: '1.5' }}>{getTavilyTooltipContent('autoParameters')}</div>}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ fontSize: '10px', color: '#0ea5e9', cursor: 'help' }}>ℹ️</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Detailed tooltips for Exa search options
|
||||
* These help educate end users about each Exa option
|
||||
*/
|
||||
|
||||
export const exaOptionTooltips = {
|
||||
category: {
|
||||
title: "Content Category",
|
||||
description: "Filter results by content type. Choose a specific category to focus your search on particular content formats like research papers, news articles, or company profiles. Leave empty to search across all categories.",
|
||||
examples: {
|
||||
"research paper": "Best for academic papers, scientific publications, and scholarly content from sources like arXiv, Nature, IEEE.",
|
||||
"news": "Recent news articles and current events from news websites.",
|
||||
"company": "Company profiles, business information, and corporate websites.",
|
||||
"pdf": "PDF documents and downloadable files.",
|
||||
"github": "GitHub repositories, code, and technical documentation.",
|
||||
"tweet": "Twitter/X posts and social media content.",
|
||||
"personal site": "Personal blogs, portfolios, and individual websites.",
|
||||
"linkedin profile": "LinkedIn profiles and professional networking content.",
|
||||
"financial report": "Financial statements, earnings reports, and economic data.",
|
||||
}
|
||||
},
|
||||
|
||||
searchType: {
|
||||
title: "Search Algorithm",
|
||||
description: "Choose how Exa searches the web. Each algorithm is optimized for different use cases and latency requirements. Exa uses embeddings-based 'next-link prediction' to understand semantic meaning, not just keyword matches.",
|
||||
types: {
|
||||
auto: {
|
||||
title: "Auto (Default) - Best of all worlds",
|
||||
description: "Intelligently combines multiple search methods (neural, keyword, and others) using a reranker model that adapts to your query type. Provides the best balance of quality and versatility without manual tuning.",
|
||||
whenToUse: "Recommended for most use cases. Use for general research, production workloads, or when query types vary significantly. Best when you want versatility without choosing a specific method.",
|
||||
latency: "Median latency: ~1000ms",
|
||||
quality: "High quality, versatile across query types",
|
||||
},
|
||||
fast: {
|
||||
title: "Fast - World's fastest search API",
|
||||
description: "Streamlined versions of neural and reranker models optimized for speed. Trades a small amount of performance for significant speed improvements. Best for applications where milliseconds matter.",
|
||||
whenToUse: "Use for real-time applications (voice agents, autocomplete), low-latency QA, or high-volume agent workflows where latency accumulates. Perfect when speed is critical.",
|
||||
latency: "Median latency: <500ms (excluding network)",
|
||||
quality: "Good factual accuracy, optimized for speed",
|
||||
note: "Best for single-step factual queries",
|
||||
},
|
||||
deep: {
|
||||
title: "Deep - Comprehensive research",
|
||||
description: "Comprehensive search with automatic query expansion or custom query variations. Runs parallel searches across multiple query formulations to find comprehensive results. Returns rich contextual summaries for each result.",
|
||||
whenToUse: "Use for agentic workflows, complex research tasks, multi-hop queries, or when comprehensive coverage matters more than speed. Ideal for research assistants and deep analysis.",
|
||||
latency: "Median latency: ~5000ms",
|
||||
quality: "Highest quality, comprehensive coverage",
|
||||
note: "Requires context=true for detailed summaries. Best for multi-step reasoning workflows.",
|
||||
},
|
||||
neural: {
|
||||
title: "Neural - Embeddings-based semantic search",
|
||||
description: "Uses AI embeddings and 'next-link prediction' to understand semantic meaning. Finds results that are conceptually similar even without exact keyword matches. Incorporated into Fast and Auto search types.",
|
||||
whenToUse: "Use for exploratory searches, finding semantically related content, or when you want to discover related concepts beyond exact keyword matches. Best for thematic and conceptual relationships.",
|
||||
latency: "Variable (incorporated into Fast/Auto)",
|
||||
quality: "Excellent semantic understanding",
|
||||
note: "Also available as part of Auto and Fast search types",
|
||||
},
|
||||
keyword: {
|
||||
title: "Keyword - Traditional search",
|
||||
description: "Traditional keyword-based search similar to Google. Uses exact keyword matching and ranking algorithms. Faster and more cost-effective than neural search.",
|
||||
whenToUse: "Use when you need precise keyword matching, want faster results, or are searching for specific terms, brands, or exact phrases.",
|
||||
limits: "Maximum 10 results with keyword search.",
|
||||
latency: "Fastest (traditional search)",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
numResults: {
|
||||
title: "Number of Results",
|
||||
description: "How many search results to return. More results provide comprehensive coverage but take longer and cost more. Fewer results are faster and more focused.",
|
||||
recommendations: {
|
||||
"1-10": "Quick overview, fast results, lower cost. Good for simple queries or when you need just a few high-quality sources.",
|
||||
"11-25": "Balanced coverage. Recommended for most research tasks. Provides good depth without excessive cost.",
|
||||
"26-50": "Comprehensive research. Use for in-depth analysis, expert-level research, or when you need extensive source coverage.",
|
||||
"51-100": "Maximum coverage. Use for exhaustive research, literature reviews, or when you need to find every relevant source. Note: Only available with neural search.",
|
||||
},
|
||||
limits: "Keyword search: max 10 results. Neural search: max 100 results.",
|
||||
},
|
||||
|
||||
highlights: {
|
||||
title: "Extract Highlights",
|
||||
description: "Enable AI-powered highlight extraction. Exa's AI identifies and extracts the most relevant text snippets from each result that match your query. These highlights are perfect for quick scanning and understanding key points.",
|
||||
benefits: [
|
||||
"Quick overview of each source without reading full content",
|
||||
"AI-identified most relevant passages",
|
||||
"Great for research summaries and citations",
|
||||
"Helps you quickly assess source relevance",
|
||||
],
|
||||
whenToUse: "Enable for research, content creation, or when you want to quickly identify key information from sources. Recommended for most use cases.",
|
||||
cost: "Additional cost per page for highlight generation.",
|
||||
},
|
||||
|
||||
context: {
|
||||
title: "Return Context String",
|
||||
description: "Combine all result contents into a single context string optimized for LLM processing. This is ideal for RAG (Retrieval-Augmented Generation) applications, AI analysis, or when you need to process all content together.",
|
||||
benefits: [
|
||||
"Single unified text string from all results",
|
||||
"Optimized for AI/LLM processing",
|
||||
"Better for RAG applications than highlights",
|
||||
"Recommended 10,000+ characters for best results",
|
||||
],
|
||||
whenToUse: "Enable when you're using results with AI/LLM tools, need full content for analysis, or building RAG applications. Context strings often perform better than highlights for AI processing.",
|
||||
recommendation: "We recommend using 10,000+ characters for best performance, though no limit works best.",
|
||||
},
|
||||
|
||||
includeDomains: {
|
||||
title: "Include Domains",
|
||||
description: "Restrict search results to specific domains only. If specified, results will ONLY come from these domains. Useful for searching within trusted sources, specific websites, or curated domain lists.",
|
||||
whenToUse: [
|
||||
"Searching within trusted sources (e.g., academic journals, reputable news sites)",
|
||||
"Focusing on specific websites or organizations",
|
||||
"Building curated research from known high-quality sources",
|
||||
"Ensuring all results come from verified domains",
|
||||
],
|
||||
format: "Enter domains separated by commas. Example: arxiv.org, nature.com, ieee.org",
|
||||
example: "arxiv.org, paperswithcode.com, openai.com, deepmind.com",
|
||||
limit: "Maximum 1200 domains can be specified.",
|
||||
},
|
||||
|
||||
excludeDomains: {
|
||||
title: "Exclude Domains",
|
||||
description: "Exclude specific domains from search results. If specified, no results will be returned from these domains. Useful for filtering out unwanted sources, spam sites, or low-quality content.",
|
||||
whenToUse: [
|
||||
"Filtering out spam or low-quality websites",
|
||||
"Excluding competitor sites from results",
|
||||
"Removing unwanted content sources",
|
||||
"Focusing on higher-quality domains",
|
||||
],
|
||||
format: "Enter domains separated by commas. Example: spam.com, ads.com, low-quality-site.com",
|
||||
example: "spam.com, ads.com, clickbait-site.com",
|
||||
limit: "Maximum 1200 domains can be excluded.",
|
||||
},
|
||||
|
||||
dateFilter: {
|
||||
title: "Start Published Date",
|
||||
description: "Filter results to only include content published after this date. This helps you find recent content or content from a specific time period onwards.",
|
||||
whenToUse: [
|
||||
"Finding recent content (e.g., last year, last month)",
|
||||
"Filtering by publication date",
|
||||
"Ensuring content freshness",
|
||||
"Time-sensitive research",
|
||||
],
|
||||
format: "ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ (e.g., 2025-01-01T00:00:00.000Z)",
|
||||
example: "2025-01-01T00:00:00.000Z (content from January 1, 2025 onwards)",
|
||||
note: "Only links with a published date after this date will be returned.",
|
||||
},
|
||||
|
||||
endPublishedDate: {
|
||||
title: "End Published Date",
|
||||
description: "Filter results to only include content published before this date. Use with Start Published Date to create a precise date range.",
|
||||
whenToUse: [
|
||||
"Creating a date range for published content",
|
||||
"Finding content from a specific time period",
|
||||
"Historical research within a timeframe",
|
||||
"Combining with Start Published Date for precise filtering",
|
||||
],
|
||||
format: "ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ (e.g., 2025-12-31T23:59:59.999Z)",
|
||||
example: "2025-12-31T23:59:59.999Z (content up to December 31, 2025)",
|
||||
note: "Only links with a published date before this date will be returned. Use with Start Published Date to create a range.",
|
||||
},
|
||||
|
||||
startCrawlDate: {
|
||||
title: "Start Crawl Date",
|
||||
description: "Filter results to only include links that Exa discovered (crawled) after this date. Crawl date refers to when Exa first found the link, not when it was published.",
|
||||
whenToUse: [
|
||||
"Finding recently discovered content",
|
||||
"Filtering by when Exa indexed the content",
|
||||
"Ensuring content is in Exa's index after a certain date",
|
||||
"Time-sensitive indexing requirements",
|
||||
],
|
||||
format: "ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ (e.g., 2025-01-01T00:00:00.000Z)",
|
||||
example: "2025-01-01T00:00:00.000Z (links crawled after January 1, 2025)",
|
||||
note: "Crawl date is different from published date. This filters by when Exa discovered the link, not when it was originally published.",
|
||||
},
|
||||
|
||||
endCrawlDate: {
|
||||
title: "End Crawl Date",
|
||||
description: "Filter results to only include links that Exa discovered (crawled) before this date. Use with Start Crawl Date to create a crawl date range.",
|
||||
whenToUse: [
|
||||
"Creating a crawl date range",
|
||||
"Finding content indexed within a specific period",
|
||||
"Historical indexing research",
|
||||
"Combining with Start Crawl Date for precise filtering",
|
||||
],
|
||||
format: "ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ (e.g., 2025-12-31T23:59:59.999Z)",
|
||||
example: "2025-12-31T23:59:59.999Z (links crawled before December 31, 2025)",
|
||||
note: "Crawl date is different from published date. This filters by when Exa discovered the link. Use with Start Crawl Date to create a range.",
|
||||
},
|
||||
|
||||
includeText: {
|
||||
title: "Include Text Filter",
|
||||
description: "Filter results to only include webpages that contain specific text. This helps narrow down results to pages mentioning specific terms or phrases.",
|
||||
whenToUse: [
|
||||
"Finding pages that mention specific terms",
|
||||
"Filtering by content keywords",
|
||||
"Ensuring results contain specific phrases",
|
||||
"Narrowing search to relevant content",
|
||||
],
|
||||
format: "Enter up to 5 words. Example: 'large language model'",
|
||||
example: "large language model, artificial intelligence, machine learning",
|
||||
limit: "Currently only 1 string is supported, of up to 5 words. Checks webpage text.",
|
||||
note: "This filters results based on text content, not just metadata.",
|
||||
},
|
||||
|
||||
excludeText: {
|
||||
title: "Exclude Text Filter",
|
||||
description: "Filter results to exclude webpages that contain specific text. This helps filter out unwanted content or pages mentioning terms you want to avoid.",
|
||||
whenToUse: [
|
||||
"Excluding pages with specific terms",
|
||||
"Filtering out unwanted content",
|
||||
"Avoiding certain topics or phrases",
|
||||
"Removing irrelevant results",
|
||||
],
|
||||
format: "Enter up to 5 words. Example: 'course tutorial'",
|
||||
example: "course, tutorial, advertisement",
|
||||
limit: "Currently only 1 string is supported, of up to 5 words. Checks the first 1000 words of webpage text.",
|
||||
note: "This filters results based on text content, helping avoid unwanted pages.",
|
||||
},
|
||||
|
||||
highlightsNumSentences: {
|
||||
title: "Highlights: Sentences Per Snippet",
|
||||
description: "Number of sentences to return for each highlight snippet. More sentences provide more context but increase response size.",
|
||||
whenToUse: [
|
||||
"Need more context in highlights (use 2-3 sentences)",
|
||||
"Want concise highlights (use 1 sentence)",
|
||||
"Balancing detail vs response size",
|
||||
],
|
||||
format: "Integer, minimum 1",
|
||||
example: "2 sentences per highlight (default)",
|
||||
recommendation: "Use 1-2 sentences for concise highlights, 3+ for more context.",
|
||||
},
|
||||
|
||||
highlightsPerUrl: {
|
||||
title: "Highlights: Snippets Per URL",
|
||||
description: "Number of highlight snippets to return for each search result. More highlights provide better coverage of the content but increase response size.",
|
||||
whenToUse: [
|
||||
"Need comprehensive coverage (use 3-5 highlights)",
|
||||
"Want quick overview (use 1-2 highlights)",
|
||||
"Balancing coverage vs response size",
|
||||
],
|
||||
format: "Integer, minimum 1",
|
||||
example: "3 highlights per URL (default)",
|
||||
recommendation: "Use 3-5 highlights for comprehensive research, 1-2 for quick overviews.",
|
||||
},
|
||||
|
||||
contextMaxCharacters: {
|
||||
title: "Context: Max Characters",
|
||||
description: "Maximum character limit for the context string. When context is enabled, all result contents are combined into one string. Higher limits provide more content but increase response size.",
|
||||
whenToUse: [
|
||||
"RAG applications (use 10,000+ characters)",
|
||||
"Comprehensive analysis (use 10,000+ characters)",
|
||||
"Limited response size (use 5,000-10,000 characters)",
|
||||
"Quick summaries (use 1,000-5,000 characters)",
|
||||
],
|
||||
format: "Integer, recommended 10,000+ for best results",
|
||||
example: "10,000 characters (recommended for RAG)",
|
||||
recommendation: "We recommend using 10,000+ characters for best results, though no limit works best. Context strings often perform better than highlights for RAG applications.",
|
||||
note: "If you have 5 results and set 1000 characters, each result gets about 200 characters.",
|
||||
},
|
||||
|
||||
textMaxCharacters: {
|
||||
title: "Text: Max Characters",
|
||||
description: "Maximum character limit for the full page text. This controls how much text content is retrieved from each webpage. Useful for controlling response size and API costs.",
|
||||
whenToUse: [
|
||||
"Controlling response size",
|
||||
"Limiting API costs",
|
||||
"Quick content preview (use 500-1000 characters)",
|
||||
"Full content analysis (use 5000+ characters)",
|
||||
],
|
||||
format: "Integer, no strict limit",
|
||||
example: "1000 characters (default)",
|
||||
recommendation: "Use 1000-2000 for quick previews, 5000+ for comprehensive content analysis.",
|
||||
},
|
||||
|
||||
summaryQuery: {
|
||||
title: "Summary: Custom Query",
|
||||
description: "Custom query to direct the LLM's generation of summaries. This helps get summaries focused on specific aspects of the content.",
|
||||
whenToUse: [
|
||||
"Need summaries focused on specific topics",
|
||||
"Customizing summary content",
|
||||
"Directing LLM attention to key aspects",
|
||||
"Getting targeted insights",
|
||||
],
|
||||
format: "String query, e.g., 'Key advancements' or 'Main developments'",
|
||||
example: "Key insights about artificial intelligence",
|
||||
recommendation: "Use specific queries to get summaries focused on what you need. Leave empty for general summaries.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Detailed tooltips for Tavily search options
|
||||
* These help educate end users about each Tavily option
|
||||
*/
|
||||
|
||||
export const tavilyOptionTooltips = {
|
||||
topic: {
|
||||
title: "Search Topic",
|
||||
description: "Choose the category of content you want to search. This helps Tavily optimize its search algorithm for your specific use case.",
|
||||
options: {
|
||||
general: {
|
||||
title: "General - Broad searches",
|
||||
description: "Best for general web searches, informational queries, and diverse content types. Use when you need a wide range of sources and perspectives.",
|
||||
whenToUse: "Use for general research, educational content, or when you're not sure which topic category fits best.",
|
||||
},
|
||||
news: {
|
||||
title: "News - Real-time updates",
|
||||
description: "Optimized for recent news articles, current events, and time-sensitive information. Provides access to the latest developments and breaking news.",
|
||||
whenToUse: "Use for current events, recent developments, breaking news, or when you need the most up-to-date information.",
|
||||
},
|
||||
finance: {
|
||||
title: "Finance - Financial data",
|
||||
description: "Focused on financial markets, economic data, company financials, stock information, and investment-related content.",
|
||||
whenToUse: "Use for financial research, market analysis, company financials, economic trends, or investment information.",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
searchDepth: {
|
||||
title: "Search Depth",
|
||||
description: "Controls the depth and quality of search results. Higher depth means better relevance but longer latency and higher cost.",
|
||||
options: {
|
||||
basic: {
|
||||
title: "Basic - Balanced (1 credit)",
|
||||
description: "Provides one NLP summary per URL. Good balance between relevance, speed, and cost. Recommended for most use cases.",
|
||||
whenToUse: "Use for general research, quick searches, or when you need a good balance of quality and speed.",
|
||||
latency: "Fast",
|
||||
quality: "Good relevance",
|
||||
cost: "1 credit per search",
|
||||
},
|
||||
advanced: {
|
||||
title: "Advanced - Highest quality (2 credits)",
|
||||
description: "Provides multiple semantic snippets per URL for the highest relevance. Best for comprehensive research and expert-level analysis.",
|
||||
whenToUse: "Use for comprehensive research, expert analysis, or when you need the highest quality results. Enables chunks_per_source parameter.",
|
||||
latency: "Slower",
|
||||
quality: "Highest relevance",
|
||||
cost: "2 credits per search",
|
||||
note: "Allows chunks_per_source up to 3 for more detailed content per source",
|
||||
},
|
||||
fast: {
|
||||
title: "Fast - Good quality, lower latency (1 credit)",
|
||||
description: "Provides multiple semantic snippets per URL with optimized latency. Good quality with faster response times than advanced.",
|
||||
whenToUse: "Use when you need good relevance with lower latency than advanced mode. Enables chunks_per_source parameter.",
|
||||
latency: "Faster than advanced",
|
||||
quality: "Good relevance",
|
||||
cost: "1 credit per search",
|
||||
note: "Allows chunks_per_source up to 3",
|
||||
},
|
||||
"ultra-fast": {
|
||||
title: "Ultra-Fast - Minimal latency (1 credit)",
|
||||
description: "Minimizes latency while providing one NLP summary per URL. Best for time-critical queries where speed is essential.",
|
||||
whenToUse: "Use for time-critical queries, real-time applications, or when minimal latency is more important than maximum relevance.",
|
||||
latency: "Minimal",
|
||||
quality: "Good relevance",
|
||||
cost: "1 credit per search",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
includeAnswer: {
|
||||
title: "AI Answer",
|
||||
description: "Get an AI-generated answer summarizing the search results. This provides a quick overview of the findings without reading all sources.",
|
||||
options: {
|
||||
false: {
|
||||
title: "Disabled",
|
||||
description: "No AI-generated answer. You'll only get the raw search results.",
|
||||
whenToUse: "Use when you want to analyze results yourself or when you don't need a summary.",
|
||||
},
|
||||
true: {
|
||||
title: "Basic Answer",
|
||||
description: "Quick AI-generated summary of the search results. Provides a concise overview.",
|
||||
whenToUse: "Use for quick summaries or when you need a brief overview of findings.",
|
||||
},
|
||||
basic: {
|
||||
title: "Basic Answer",
|
||||
description: "Quick AI-generated summary of the search results. Provides a concise overview.",
|
||||
whenToUse: "Use for quick summaries or when you need a brief overview of findings.",
|
||||
},
|
||||
advanced: {
|
||||
title: "Advanced Answer",
|
||||
description: "Detailed AI-generated answer with comprehensive insights from all search results. Best for in-depth analysis.",
|
||||
whenToUse: "Use for comprehensive research, detailed analysis, or when you need thorough insights from all sources.",
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
timeRange: {
|
||||
title: "Time Range",
|
||||
description: "Filter results by recency. Use this to find recent content within a specific time period. Shorthand options (d, w, m, y) are supported.",
|
||||
options: {
|
||||
day: {
|
||||
title: "Past Day",
|
||||
description: "Results from the last 24 hours. Best for breaking news and very recent developments.",
|
||||
whenToUse: "Use for breaking news, real-time updates, or very recent events.",
|
||||
},
|
||||
week: {
|
||||
title: "Past Week",
|
||||
description: "Results from the last 7 days. Good for recent news and current events.",
|
||||
whenToUse: "Use for recent news, current events, or weekly updates.",
|
||||
},
|
||||
month: {
|
||||
title: "Past Month",
|
||||
description: "Results from the last 30 days. Good for recent trends and monthly updates.",
|
||||
whenToUse: "Use for recent trends, monthly updates, or when you need relatively fresh content.",
|
||||
},
|
||||
year: {
|
||||
title: "Past Year",
|
||||
description: "Results from the last 12 months. Good for annual trends and yearly analysis.",
|
||||
whenToUse: "Use for annual trends, yearly analysis, or when you need content from the past year.",
|
||||
},
|
||||
},
|
||||
note: "For more precise date ranges, use Start Date and End Date fields instead.",
|
||||
},
|
||||
|
||||
includeRawContent: {
|
||||
title: "Raw Content Format",
|
||||
description: "Include the raw HTML content from web pages. This provides full text content but may increase latency and response size.",
|
||||
options: {
|
||||
false: {
|
||||
title: "Disabled",
|
||||
description: "No raw content. You'll only get summaries and metadata.",
|
||||
whenToUse: "Use when you only need summaries and don't need full page content.",
|
||||
},
|
||||
true: {
|
||||
title: "Markdown Format",
|
||||
description: "Full page content formatted as Markdown. Preserves structure and formatting.",
|
||||
whenToUse: "Use when you need full page content with formatting preserved.",
|
||||
},
|
||||
markdown: {
|
||||
title: "Markdown Format",
|
||||
description: "Full page content formatted as Markdown. Preserves structure and formatting.",
|
||||
whenToUse: "Use when you need full page content with formatting preserved.",
|
||||
},
|
||||
text: {
|
||||
title: "Plain Text",
|
||||
description: "Full page content as plain text. No formatting, just raw text content.",
|
||||
whenToUse: "Use when you need full page content but don't need formatting.",
|
||||
},
|
||||
},
|
||||
note: "Including raw content may increase latency and response size. Use only when necessary.",
|
||||
},
|
||||
|
||||
includeDomains: {
|
||||
title: "Include Domains",
|
||||
description: "Restrict search results to specific domains. Only results from these domains will be returned. Useful for trusted sources or specific websites.",
|
||||
examples: [
|
||||
"nature.com - For scientific articles from Nature",
|
||||
"arxiv.org - For academic papers from arXiv",
|
||||
"github.com - For code repositories and technical content",
|
||||
"news.ycombinator.com - For Hacker News discussions",
|
||||
],
|
||||
whenToUse: "Use when you want to search only specific trusted sources or websites. Maximum 300 domains.",
|
||||
note: "Separate multiple domains with commas. Maximum 300 domains allowed.",
|
||||
},
|
||||
|
||||
excludeDomains: {
|
||||
title: "Exclude Domains",
|
||||
description: "Exclude specific domains from search results. Results from these domains will be filtered out. Useful for avoiding spam or unwanted sources.",
|
||||
examples: [
|
||||
"spam.com - Exclude spam websites",
|
||||
"ads.com - Exclude ad-heavy sites",
|
||||
"example.com - Exclude specific unwanted sources",
|
||||
],
|
||||
whenToUse: "Use when you want to filter out specific domains or avoid unwanted sources. Maximum 150 domains.",
|
||||
note: "Separate multiple domains with commas. Maximum 150 domains allowed.",
|
||||
},
|
||||
|
||||
country: {
|
||||
title: "Country Code",
|
||||
description: "Boost results from a specific country. This helps prioritize content from a particular geographic region. Use lowercase full country name.",
|
||||
examples: [
|
||||
"united states - For US-focused results",
|
||||
"united kingdom - For UK-focused results",
|
||||
"india - For India-focused results",
|
||||
"canada - For Canada-focused results",
|
||||
],
|
||||
whenToUse: "Use when you need region-specific content or want to prioritize results from a particular country.",
|
||||
note: "Use lowercase full country name (e.g., 'united states' not 'US'). Works best with 'general' topic.",
|
||||
},
|
||||
|
||||
startDate: {
|
||||
title: "Start Date",
|
||||
description: "Return only results published after this date. Provides precise date filtering for more accurate time-based searches.",
|
||||
whenToUse: "Use when you need results from a specific date onwards. More precise than time_range.",
|
||||
note: "Format: YYYY-MM-DD. Use with End Date to create a date range.",
|
||||
},
|
||||
|
||||
endDate: {
|
||||
title: "End Date",
|
||||
description: "Return only results published before this date. Use with Start Date to create a precise date range.",
|
||||
whenToUse: "Use when you need results up to a specific date. More precise than time_range.",
|
||||
note: "Format: YYYY-MM-DD. Use with Start Date to create a date range.",
|
||||
},
|
||||
|
||||
chunksPerSource: {
|
||||
title: "Chunks Per Source",
|
||||
description: "Number of content chunks to return per source. Higher values provide more detailed content from each source but may increase response size.",
|
||||
whenToUse: "Use with 'advanced' or 'fast' search_depth to get more detailed content per source. Maximum is 3.",
|
||||
note: "Only available with 'advanced' or 'fast' search_depth. Maximum is 3 chunks per source.",
|
||||
range: "1-3 chunks per source",
|
||||
},
|
||||
|
||||
includeImages: {
|
||||
title: "Include Images",
|
||||
description: "Include query-related images in the search results. Images are selected based on relevance to your search query.",
|
||||
whenToUse: "Use when you need visual content related to your search query, such as charts, diagrams, or illustrations.",
|
||||
note: "Enabling this may increase response size. Use include_image_descriptions for AI-generated image descriptions.",
|
||||
},
|
||||
|
||||
includeImageDescriptions: {
|
||||
title: "Include Image Descriptions",
|
||||
description: "Include AI-generated descriptions of images. Provides context about what each image contains. Requires include_images to be enabled.",
|
||||
whenToUse: "Use when you need to understand image content without viewing the images directly.",
|
||||
note: "Requires 'Include Images' to be enabled. Provides AI-generated descriptions of image content.",
|
||||
},
|
||||
|
||||
includeFavicon: {
|
||||
title: "Include Favicon URLs",
|
||||
description: "Include the favicon (website icon) URL for each search result. Useful for visual identification of sources.",
|
||||
whenToUse: "Use when you want to display source icons or visually identify different sources in your UI.",
|
||||
note: "Provides the favicon URL for each result, useful for UI display purposes.",
|
||||
},
|
||||
|
||||
autoParameters: {
|
||||
title: "Auto-Configure Parameters",
|
||||
description: "Let Tavily automatically optimize search parameters based on your query. Uses AI to select the best settings for your specific search.",
|
||||
whenToUse: "Use when you're unsure about optimal settings or want Tavily to automatically optimize for your query.",
|
||||
note: "Costs 2 credits per search. Use sparingly as it's more expensive than manual configuration.",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tooltip content for a specific Tavily option
|
||||
*/
|
||||
export const getTavilyTooltipContent = (optionKey: keyof typeof tavilyOptionTooltips, value?: string): string => {
|
||||
const tooltip = tavilyOptionTooltips[optionKey];
|
||||
|
||||
if (!tooltip) {
|
||||
return `Information about ${optionKey}`;
|
||||
}
|
||||
|
||||
// Handle options with nested value-specific tooltips
|
||||
if ('options' in tooltip && value) {
|
||||
const valueTooltip = (tooltip.options as any)[value];
|
||||
if (valueTooltip) {
|
||||
let content = `**${valueTooltip.title}**\n\n${valueTooltip.description}\n\n`;
|
||||
if (valueTooltip.whenToUse) {
|
||||
content += `**When to use:** ${valueTooltip.whenToUse}\n\n`;
|
||||
}
|
||||
if (valueTooltip.latency) {
|
||||
content += `**Latency:** ${valueTooltip.latency}\n\n`;
|
||||
}
|
||||
if (valueTooltip.quality) {
|
||||
content += `**Quality:** ${valueTooltip.quality}\n\n`;
|
||||
}
|
||||
if (valueTooltip.cost) {
|
||||
content += `**Cost:** ${valueTooltip.cost}\n\n`;
|
||||
}
|
||||
if (valueTooltip.note) {
|
||||
content += `**Note:** ${valueTooltip.note}`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tooltips with examples
|
||||
if ('examples' in tooltip && Array.isArray(tooltip.examples)) {
|
||||
let content = `**${tooltip.title}**\n\n${tooltip.description}\n\n`;
|
||||
if (tooltip.whenToUse) {
|
||||
content += `**When to use:** ${tooltip.whenToUse}\n\n`;
|
||||
}
|
||||
content += `**Examples:**\n${tooltip.examples.map(ex => `• ${ex}`).join('\n')}\n\n`;
|
||||
if (tooltip.note) {
|
||||
content += `**Note:** ${tooltip.note}`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Default tooltip format
|
||||
let content = `**${tooltip.title}**\n\n${tooltip.description}\n\n`;
|
||||
if ('whenToUse' in tooltip && tooltip.whenToUse) {
|
||||
content += `**When to use:** ${tooltip.whenToUse}\n\n`;
|
||||
}
|
||||
if ('note' in tooltip && tooltip.note) {
|
||||
content += `**Note:** ${tooltip.note}`;
|
||||
}
|
||||
if ('range' in tooltip && tooltip.range) {
|
||||
content += `**Range:** ${tooltip.range}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getResearchConfig, ProviderAvailability } from '../../../../api/researchConfig';
|
||||
import { WizardState } from '../../types/research.types';
|
||||
import { ResearchProvider } from '../../../../services/blogWriterApi';
|
||||
import { ResearchProvider } from '../../../../services/researchApi';
|
||||
|
||||
interface ResearchPersona {
|
||||
research_angles?: string[];
|
||||
|
||||
@@ -34,20 +34,20 @@ export const exaCategories = [
|
||||
{ value: 'company', label: 'Company Profiles' },
|
||||
{ value: 'research paper', label: 'Research Papers' },
|
||||
{ value: 'news', label: 'News Articles' },
|
||||
{ value: 'linkedin profile', label: 'LinkedIn Profiles' },
|
||||
{ value: 'pdf', label: 'PDF Documents' },
|
||||
{ value: 'github', label: 'GitHub Repos' },
|
||||
{ value: 'tweet', label: 'Tweets' },
|
||||
{ value: 'movie', label: 'Movies' },
|
||||
{ value: 'song', label: 'Songs' },
|
||||
{ value: 'personal site', label: 'Personal Sites' },
|
||||
{ value: 'pdf', label: 'PDF Documents' },
|
||||
{ value: 'linkedin profile', label: 'LinkedIn Profiles' },
|
||||
{ value: 'financial report', label: 'Financial Reports' },
|
||||
];
|
||||
|
||||
export const exaSearchTypes = [
|
||||
{ value: 'auto', label: 'Auto - Let AI decide' },
|
||||
{ value: 'keyword', label: 'Keyword - Precise matching' },
|
||||
{ value: 'neural', label: 'Neural - Semantic search' },
|
||||
{ value: 'auto', label: 'Auto (Default) - Best of all worlds' },
|
||||
{ value: 'fast', label: 'Fast - <500ms, speed-critical' },
|
||||
{ value: 'deep', label: 'Deep - ~5000ms, comprehensive research' },
|
||||
{ value: 'neural', label: 'Neural - Embeddings-based semantic' },
|
||||
{ value: 'keyword', label: 'Keyword - Traditional search' },
|
||||
];
|
||||
|
||||
export const tavilyTopics = [
|
||||
|
||||
@@ -79,40 +79,52 @@ export const getIndustryPlaceholders = (
|
||||
const getIndustryDefaults = (industry: string): string[] => {
|
||||
const industryExamples: Record<string, string[]> = {
|
||||
Healthcare: [
|
||||
"AI diagnostic tools and clinical applications",
|
||||
"Telemedicine adoption and patient outcomes",
|
||||
"Personalized medicine and genomic testing",
|
||||
"Healthcare automation and workflow optimization"
|
||||
"AI diagnostic tools: accuracy rates and clinical implementation",
|
||||
"Telemedicine adoption statistics and patient satisfaction outcomes",
|
||||
"Personalized medicine: genomic testing costs and benefits",
|
||||
"Healthcare automation: workflow optimization case studies",
|
||||
"Compare telehealth platforms: features, pricing, and ROI",
|
||||
"Future of healthcare AI: predictions for 2025-2026"
|
||||
],
|
||||
Technology: [
|
||||
"Edge computing and IoT deployment strategies",
|
||||
"Cloud provider comparison and cost optimization",
|
||||
"Quantum computing breakthroughs and applications",
|
||||
"AI and machine learning industry trends"
|
||||
"Latest AI advancements in multimodal content generation 2026",
|
||||
"Compare cloud providers: AWS vs Azure vs GCP pricing and features",
|
||||
"Edge computing deployment strategies and IoT best practices",
|
||||
"Quantum computing breakthroughs and real-world applications",
|
||||
"How to implement AI automation in small businesses",
|
||||
"Future of AI: predictions and emerging opportunities 2025-2030"
|
||||
],
|
||||
Finance: [
|
||||
"DeFi regulations and compliance strategies",
|
||||
"Digital banking and customer retention",
|
||||
"ESG investing trends and performance",
|
||||
"Fintech innovations and market analysis"
|
||||
"DeFi regulations: compliance strategies and risk management",
|
||||
"Digital banking: customer retention tactics and ROI",
|
||||
"ESG investing trends: performance metrics and market analysis",
|
||||
"Fintech innovations: comparison of top payment platforms",
|
||||
"Cryptocurrency adoption: statistics and future outlook 2025",
|
||||
"How to choose the right financial software for small businesses"
|
||||
],
|
||||
Marketing: [
|
||||
"AI marketing automation and personalization",
|
||||
"Influencer marketing ROI and best practices",
|
||||
"Privacy-first marketing in cookieless world",
|
||||
"Content marketing strategies and trends"
|
||||
"AI marketing automation tools: comparison and ROI analysis",
|
||||
"Influencer marketing ROI: statistics and best practices 2025",
|
||||
"Privacy-first marketing strategies in cookieless world",
|
||||
"Content marketing trends: what works in 2025",
|
||||
"How to measure marketing attribution and conversion rates",
|
||||
"Social media marketing: platform comparison and audience insights"
|
||||
],
|
||||
Business: [
|
||||
"Remote work policies and hybrid models",
|
||||
"Supply chain resilience and diversification",
|
||||
"Sustainability initiatives and ESG programs",
|
||||
"Business automation and efficiency"
|
||||
"Remote work policies: best practices and productivity metrics",
|
||||
"Supply chain resilience: diversification strategies and case studies",
|
||||
"Sustainability initiatives: ESG programs and ROI analysis",
|
||||
"Business automation: tools comparison and implementation guides",
|
||||
"How to scale a startup: funding strategies and growth tactics",
|
||||
"Customer retention: strategies that work in 2025"
|
||||
],
|
||||
Education: [
|
||||
"EdTech tools and personalized learning",
|
||||
"Microlearning and skill-based education",
|
||||
"AI tutoring systems and student support",
|
||||
"Online learning platforms and outcomes"
|
||||
"EdTech tools: comparison of top learning platforms",
|
||||
"Microlearning: effectiveness statistics and best practices",
|
||||
"AI tutoring systems: student outcomes and implementation",
|
||||
"Online learning platforms: ROI and engagement metrics",
|
||||
"How to create effective online courses: step-by-step guide",
|
||||
"Future of education: predictions and emerging technologies"
|
||||
],
|
||||
'Real Estate': [
|
||||
"PropTech innovations and property management",
|
||||
@@ -128,12 +140,14 @@ const getIndustryDefaults = (industry: string): string[] => {
|
||||
]
|
||||
};
|
||||
|
||||
// Default placeholders - concise and actionable
|
||||
// Default placeholders - diverse, actionable examples that inspire research
|
||||
return industryExamples[industry] || [
|
||||
"Latest AI trends and innovations",
|
||||
"Best practices and case studies",
|
||||
"Market analysis and competitor insights",
|
||||
"Emerging technologies and future predictions"
|
||||
"What are the latest trends in [your industry] for 2025-2026?",
|
||||
"Compare top solutions: [solution A] vs [solution B]",
|
||||
"Best practices and real-world case studies",
|
||||
"Expert insights and statistics on [topic]",
|
||||
"How to [achieve goal] - step-by-step guide",
|
||||
"Future predictions and emerging opportunities"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResearchMode } from '../../../../services/blogWriterApi';
|
||||
import { ResearchMode } from '../../../../services/researchApi';
|
||||
|
||||
/**
|
||||
* Smart mode suggestion based on query complexity
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface ResearchIntent {
|
||||
expected_deliverables: ExpectedDeliverable[];
|
||||
depth: ResearchDepthLevel;
|
||||
focus_areas: string[];
|
||||
also_answering: string[];
|
||||
perspective: string | null;
|
||||
time_sensitivity: string | null;
|
||||
input_type: InputType;
|
||||
@@ -210,9 +211,13 @@ export interface SourceWithRelevance {
|
||||
|
||||
export interface AnalyzeIntentRequest {
|
||||
user_input: string;
|
||||
keywords: string[];
|
||||
use_persona: boolean;
|
||||
use_competitor_data: boolean;
|
||||
keywords?: string[];
|
||||
use_persona?: boolean;
|
||||
use_competitor_data?: boolean;
|
||||
// User-provided intent settings (optional - if provided, use these instead of inferring)
|
||||
user_provided_purpose?: ResearchPurpose;
|
||||
user_provided_content_output?: ContentOutput;
|
||||
user_provided_depth?: ResearchDepthLevel;
|
||||
}
|
||||
|
||||
// Optimized provider configuration with AI-driven justifications
|
||||
@@ -231,10 +236,33 @@ export interface OptimizedConfig {
|
||||
exa_num_results_justification?: string;
|
||||
exa_date_filter?: string;
|
||||
exa_date_justification?: string;
|
||||
exa_end_published_date?: string;
|
||||
exa_end_published_date_justification?: string;
|
||||
exa_start_crawl_date?: string;
|
||||
exa_start_crawl_date_justification?: string;
|
||||
exa_end_crawl_date?: string;
|
||||
exa_end_crawl_date_justification?: string;
|
||||
exa_include_text?: string[];
|
||||
exa_include_text_justification?: string;
|
||||
exa_exclude_text?: string[];
|
||||
exa_exclude_text_justification?: string;
|
||||
exa_highlights?: boolean;
|
||||
exa_highlights_justification?: string;
|
||||
exa_context?: boolean;
|
||||
exa_highlights_num_sentences?: number;
|
||||
exa_highlights_num_sentences_justification?: string;
|
||||
exa_highlights_per_url?: number;
|
||||
exa_highlights_per_url_justification?: string;
|
||||
exa_context?: boolean | { maxCharacters?: number };
|
||||
exa_context_justification?: string;
|
||||
exa_context_max_characters?: number;
|
||||
exa_context_max_characters_justification?: string;
|
||||
exa_text_max_characters?: number;
|
||||
exa_text_max_characters_justification?: string;
|
||||
exa_summary_query?: string;
|
||||
exa_summary_query_justification?: string;
|
||||
exa_additional_queries?: string[];
|
||||
exa_additional_queries_justification?: string;
|
||||
// Note: exa_search_type is mapped from exa_type in the backend
|
||||
|
||||
// Tavily settings with justifications
|
||||
tavily_topic?: string;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { BlogResearchResponse, ResearchMode, ResearchConfig } from '../../../services/blogWriterApi';
|
||||
import { ResearchResponse, ResearchMode, ResearchConfig } from '../../../services/researchApi';
|
||||
import {
|
||||
ResearchIntent,
|
||||
AnalyzeIntentResponse,
|
||||
IntentDrivenResearchResponse,
|
||||
ResearchQuery,
|
||||
ResearchPurpose,
|
||||
ContentOutput,
|
||||
ResearchDepthLevel,
|
||||
} from './intent.types';
|
||||
|
||||
export interface WizardState {
|
||||
@@ -13,7 +16,11 @@ export interface WizardState {
|
||||
targetAudience: string;
|
||||
researchMode: ResearchMode;
|
||||
config: ResearchConfig;
|
||||
results: BlogResearchResponse | null;
|
||||
results: ResearchResponse | null;
|
||||
// User-provided intent settings (optional, if not provided, AI will infer)
|
||||
userPurpose?: ResearchPurpose;
|
||||
userContentOutput?: ContentOutput;
|
||||
userDepth?: ResearchDepthLevel;
|
||||
}
|
||||
|
||||
export interface ResearchExecution {
|
||||
@@ -34,7 +41,7 @@ export interface ResearchExecution {
|
||||
confirmedIntent: ResearchIntent | null;
|
||||
intentResult: IntentDrivenResearchResponse | null;
|
||||
analyzeIntent: (state: WizardState) => Promise<AnalyzeIntentResponse | null>;
|
||||
confirmIntent: (intent: ResearchIntent) => void;
|
||||
confirmIntent: (intent: ResearchIntent, state?: WizardState) => void;
|
||||
updateIntentField: <K extends keyof ResearchIntent>(field: K, value: ResearchIntent[K]) => void;
|
||||
executeIntentResearch: (state: WizardState, selectedQueries?: ResearchQuery[]) => Promise<IntentDrivenResearchResponse | null>;
|
||||
clearIntent: () => void;
|
||||
@@ -49,13 +56,14 @@ export interface WizardStepProps {
|
||||
}
|
||||
|
||||
export interface ResearchWizardProps {
|
||||
onComplete?: (results: BlogResearchResponse) => void;
|
||||
onComplete?: (results: ResearchResponse) => void;
|
||||
onCancel?: () => void;
|
||||
initialKeywords?: string[];
|
||||
initialIndustry?: string;
|
||||
initialTargetAudience?: string;
|
||||
initialResearchMode?: ResearchMode;
|
||||
initialConfig?: ResearchConfig;
|
||||
initialResults?: ResearchResponse | null; // For restoring saved projects
|
||||
}
|
||||
|
||||
export interface ModeCardInfo {
|
||||
|
||||
Reference in New Issue
Block a user