import React, { useState, useMemo, useCallback } from 'react'; import { BlogResearchResponse } from '../../../services/blogWriterApi'; import { BrainstormResult } from '../../../api/gscBrainstorm'; import { useGSCBrainstorm } from '../../../hooks/useGSCBrainstorm'; import { GSCBrainstormModal } from '../GSCBrainstormModal'; import { TextToSpeechButton } from '../../shared/TextToSpeechButton'; interface ResearchSourcesProps { research: BlogResearchResponse; brainstormResult?: BrainstormResult; onResearchWithKeywords?: (keywords: string) => void; selectedContentAngle?: string; onAngleSelect?: (angle: string) => void; selectedCompetitiveAdvantage?: string; onCompetitiveAdvantageSelect?: (advantage: string) => void; } interface KeywordChipGroupProps { title: string; keywords: string[]; color: string; backgroundColor: string; icon: string; showCount: number; tooltip: string; lockedCount?: number; } const KeywordChipGroup: React.FC = ({ title, keywords, color, backgroundColor, icon, showCount, tooltip, lockedCount = 0 }) => { const [isExpanded, setIsExpanded] = useState(false); const [showTooltip, setShowTooltip] = useState(false); const visibleKeywords = isExpanded ? keywords : keywords.slice(0, showCount); const hasMore = keywords.length > showCount; return (
{ if (hasMore) { setIsExpanded(true); e.currentTarget.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'; e.currentTarget.style.borderColor = color; e.currentTarget.style.transform = 'translateY(-1px)'; } }} onMouseLeave={(e) => { if (hasMore) { setIsExpanded(false); e.currentTarget.style.boxShadow = '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)'; e.currentTarget.style.borderColor = '#e5e7eb'; e.currentTarget.style.transform = 'translateY(0)'; } }} >
{icon} {title} {keywords.length} {/* Help Icon */} setShowTooltip(!showTooltip)} style={{ fontSize: '12px', color: '#9ca3af', cursor: 'pointer', padding: '4px', borderRadius: '50%', transition: 'all 0.2s ease', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: '20px', minHeight: '20px' }} onMouseEnter={(e) => { e.currentTarget.style.color = '#6b7280'; e.currentTarget.style.backgroundColor = '#f3f4f6'; }} onMouseLeave={(e) => { e.currentTarget.style.color = '#9ca3af'; e.currentTarget.style.backgroundColor = 'transparent'; }} > โ“
{visibleKeywords.map((keyword: string, index: number) => { const isLocked = index < lockedCount; return ( {isLocked && ๐Ÿ”’} {keyword} ); })} {hasMore && !isExpanded && ( +{keywords.length - showCount} more )}
{/* Professional Tooltip - Only show when clicked */} {showTooltip && (
{title} Keywords
{tooltip}
{/* Tooltip arrow */}
)}
); }; export const ResearchSources: React.FC = ({ research, brainstormResult: propBrainstormResult, onResearchWithKeywords, selectedContentAngle, onAngleSelect, selectedCompetitiveAdvantage, onCompetitiveAdvantageSelect }) => { const { gscConnected, isBrainstorming, brainstormError, contentOpportunities, keywordGaps, quickWins, pageOpportunities, aiRecommendations, summary, progressMessage, brainstorm: localBrainstorm, reset: resetBrainstorm, } = useGSCBrainstorm(); const [showGSCModal, setShowGSCModal] = useState(false); const [localBrainstormResult, setLocalBrainstormResult] = useState(null); const brainstormResult = propBrainstormResult || localBrainstormResult; const handleLocalBrainstorm = useCallback(async () => { const kw = (research as any).original_keywords ? (Array.isArray((research as any).original_keywords) ? (research as any).original_keywords.join(', ') : String((research as any).original_keywords)) : (research.keywords?.join(', ') || (research as any).topic || ''); if (!kw) { console.warn('[GSC] No keywords available for brainstorming'); return; } setShowGSCModal(true); const result = await localBrainstorm(kw); if (result) { setLocalBrainstormResult(result); } }, [research, localBrainstorm]); const renderCredibilityScore = (score: number | undefined) => { const safeScore = score ?? 0.8; // Default to 0.8 if undefined const percentage = Math.round(safeScore * 100); const color = safeScore >= 0.8 ? '#4CAF50' : safeScore >= 0.6 ? '#FF9800' : '#F44336'; const radius = 20; const circumference = 2 * Math.PI * radius; const strokeDasharray = circumference; const strokeDashoffset = circumference - (safeScore * circumference); return (
{percentage}%
); }; return (
{/* Keywords Sidebar - Moved to Left */}

๐ŸŽฏ Keywords

{/* Curation Summary Bar */}
๐Ÿงน Smart Curation Active: ~{(() => { const primary = research.keyword_analysis?.primary?.length || 0; const secondary = research.keyword_analysis?.secondary?.length || 0; const longTail = research.keyword_analysis?.long_tail?.length || 0; const semantic = research.keyword_analysis?.semantic_keywords?.length || 0; const trending = research.keyword_analysis?.trending_terms?.length || 0; const gaps = research.keyword_analysis?.content_gaps?.length || 0; return primary + secondary + longTail + semantic + trending + gaps; })()} raw โ†’ 13 locked ๐Ÿ”’ H1/H2/H3 assigned
{/* Progressive Disclosure Keyword Chips */}
{/* Primary Keywords โ€” lock top 2 for H1 */} {research.keyword_analysis?.primary && research.keyword_analysis.primary.length > 0 && ( )} {/* Secondary Keywords โ€” lock top 2 for H2 */} {research.keyword_analysis?.secondary && research.keyword_analysis.secondary.length > 0 && ( )} {/* Long-tail Keywords โ€” lock top 2 for H3 */} {research.keyword_analysis?.long_tail && research.keyword_analysis.long_tail.length > 0 && ( )} {/* Semantic Keywords โ€” lock top 4 for body-level signals */} {research.keyword_analysis?.semantic_keywords && research.keyword_analysis.semantic_keywords.length > 0 && ( )} {/* Trending Terms โ€” lock top 2 for contextual mentions */} {research.keyword_analysis?.trending_terms && research.keyword_analysis.trending_terms.length > 0 && ( )} {/* Content Gaps โ€” lock top 1 for competitive edge */} {research.keyword_analysis?.content_gaps && research.keyword_analysis.content_gaps.length > 0 && ( )} {/* Top Competitors โ€” no lock (display only, not used for keyword placement) */} {research.competitor_analysis?.top_competitors && research.competitor_analysis.top_competitors.length > 0 && ( )} {/* GSC โ€” inside keywords card, below chips */} {brainstormResult ? ( ) : (
๐Ÿ“Š Unlock GSC Insights

Find keyword gaps, quick wins, and AI recommendations from your GSC data.

{isBrainstorming ? 'Analyzing...' : gscConnected ? '๐Ÿ“Š Brainstorm Topics' : '๐Ÿ”— Connect GSC'} {brainstormError && (

{brainstormError}

)}
)}
{/* GSC Brainstorm Modal */} { setShowGSCModal(false); resetBrainstorm(); }} contentOpportunities={contentOpportunities} keywordGaps={keywordGaps} quickWins={quickWins} pageOpportunities={pageOpportunities} aiRecommendations={aiRecommendations} summary={summary} error={brainstormError} isBrainstorming={isBrainstorming} progressMessage={progressMessage} onSelectSuggestion={() => {}} initialKeywords={typeof research.original_keywords === 'string' ? research.original_keywords : research.keywords?.join(', ') || ''} onReRun={async (newKw) => { const result = await localBrainstorm(newKw, undefined, true); if (result) setLocalBrainstormResult(result); }} />
{/* Main Sources Content */}

๐Ÿ” Research Sources ({research.sources.length})

{/* Research Insights Section */} {research.keyword_analysis?.analysis_insights && (
๐Ÿ’ก

Research Insights

{/* Key Metrics in Research Insights - Moved to right corner */}
{research.keyword_analysis?.search_intent && (
๐ŸŽฏ Search Intent: {research.keyword_analysis.search_intent}
)} {research.keyword_analysis?.difficulty && (
๐Ÿ“Š Difficulty: {research.keyword_analysis.difficulty}/10
)}

{research.keyword_analysis.analysis_insights}

)} {/* Market Positioning Section */} {research.competitor_analysis?.market_positioning && (
๐ŸŽฏ

Market Positioning

{research.competitor_analysis.market_positioning}

)} {/* Content Angles Section */} {research.suggested_angles && research.suggested_angles.length > 0 && (
๐Ÿ’ก

Select Content Angle For Blog Outline

{research.suggested_angles.length} angles
{research.suggested_angles.map((angle, index) => (
onAngleSelect?.(angle)} onMouseEnter={(e) => { if (selectedContentAngle !== angle) { e.currentTarget.style.borderColor = '#86efac'; e.currentTarget.style.backgroundColor = '#f0fdf4'; } }} onMouseLeave={(e) => { if (selectedContentAngle !== angle) { e.currentTarget.style.borderColor = '#bbf7d0'; e.currentTarget.style.backgroundColor = '#fff'; } }} >
{selectedContentAngle === angle ? 'โœ“' : index + 1} {angle}
))}
)} {/* Competitive Advantages Section */} {research.competitor_analysis?.competitive_advantages?.length > 0 && (
โœ…

Select Competitive Advantage For Blog Outline

{research.competitor_analysis.competitive_advantages.length} advantages
{(research.competitor_analysis.competitive_advantages as string[]).map((advantage, index) => (
onCompetitiveAdvantageSelect?.(advantage)} onMouseEnter={(e) => { if (selectedCompetitiveAdvantage !== advantage) { e.currentTarget.style.borderColor = '#fcd34d'; e.currentTarget.style.backgroundColor = '#fffbeb'; } }} onMouseLeave={(e) => { if (selectedCompetitiveAdvantage !== advantage) { e.currentTarget.style.borderColor = '#fde68a'; e.currentTarget.style.backgroundColor = '#fff'; } }} >
{selectedCompetitiveAdvantage === advantage ? 'โœ“' : index + 1} {advantage}
))}
)} {/* Note: Google Search Widget is shown in GoogleSearchModal instead */}
{research.sources.map((source, index) => ( ))}
); }; // ============================================================================ // Source Card Component โ€” each research source with expandable content // ============================================================================ interface SourceCardProps { source: BlogResearchResponse['sources'][0] & { highlights?: string[]; summary?: string; image?: string; author?: string; content?: string }; } const SourceCard: React.FC = ({ source }) => { const [showExtra, setShowExtra] = useState(false); const [showFullText, setShowFullText] = useState(false); const allHighlights = source.highlights || []; const remainingHighlights = allHighlights.length > 1 ? allHighlights.slice(1) : []; const hasSummary = !!source.summary; const hasContent = !!source.content; const renderCredibilityScore = (score: number | undefined | null): React.ReactNode => { if (score === undefined || score === null) return null; const percentage = Math.round(score * 100); const radius = 18; const circumference = 2 * Math.PI * radius; const strokeDasharray = `${circumference} ${circumference}`; const strokeDashoffset = circumference - (percentage / 100) * circumference; const color = percentage >= 80 ? '#22c55e' : percentage >= 60 ? '#f59e0b' : '#ef4444'; return (
{percentage}%
); }; return (
SERP Ranking {source.index !== undefined ? source.index + 1 : '?'} Research Type: {source.source_type || 'web'} {source.published_at ? ( {source.published_at} ) : ( No date )} {source.author && ( โœ๏ธ {source.author} )}
{source.image && ( { (e.target as HTMLImageElement).style.display = 'none'; }} /> )}

{source.title}

{renderCredibilityScore(source.credibility_score)}

{source.excerpt}

{/* Show more toggle for highlights + summary */} {(remainingHighlights.length > 0 || hasSummary) && (
{showExtra && (
{hasSummary && (
Summary: {source.summary}
)} {remainingHighlights.map((h, i) => (
{h}
))}
)}
)} {/* Full-text preview toggle */} {hasContent && (
{showFullText && source.content && (
{source.content.length > 2000 ? source.content.substring(0, 2000) + '...' : source.content}
)}
)}
); }; interface GSCInsightsCardProps { brainstormResult: BrainstormResult; research: BlogResearchResponse; } const GSCInsightsCard: React.FC = ({ brainstormResult, research }) => { const keywordGaps = brainstormResult.keyword_gaps || []; const quickWins = brainstormResult.quick_wins || []; const contentGaps = research.keyword_analysis?.content_gaps || []; const intersectingGaps = useMemo(() => { return keywordGaps.filter((g) => contentGaps.some((cg: string) => cg.toLowerCase().includes(g.keyword.toLowerCase()) || g.keyword.toLowerCase().includes(cg.toLowerCase())) ); }, [keywordGaps, contentGaps]); const gapOnly = useMemo(() => { return keywordGaps.filter((g) => !contentGaps.some((cg: string) => cg.toLowerCase().includes(g.keyword.toLowerCase()) || g.keyword.toLowerCase().includes(cg.toLowerCase())) ); }, [keywordGaps, contentGaps]); const aiRecs = brainstormResult.ai_recommendations && 'immediate_opportunities' in brainstormResult.ai_recommendations ? (brainstormResult.ai_recommendations as import('../../../api/gscBrainstorm').AIRecommendations) : null; const allAiRecs = aiRecs ? [...(aiRecs.immediate_opportunities || []), ...(aiRecs.content_strategy || []), ...(aiRecs.long_term_strategy || [])] : []; return (

๐Ÿ“Š GSC Insights

{/* 1. Search Intent + CTR overlay */} {brainstormResult.summary && 'site_url' in brainstormResult.summary && (
๐ŸŽฏ GSC Performance
{research.keyword_analysis?.search_intent && (
Intent: {research.keyword_analysis.search_intent}
)}
Avg CTR: {(brainstormResult.summary as any).avg_ctr?.toFixed(1)}%
Avg Pos: {(brainstormResult.summary as any).avg_position?.toFixed(1)}
Health: {(brainstormResult.summary as any).health_score ?? 'N/A'}
)} {/* 2. Intersecting Keyword Gaps */} {intersectingGaps.length > 0 && (
๐Ÿ”„ Gap Overlap ({intersectingGaps.length})
{intersectingGaps.map((g, i) => ( {g.keyword} ))}
)} {/* 3. New Keyword Gaps (not in research) */} {gapOnly.length > 0 && (
๐Ÿ•ณ๏ธ New Gaps ({gapOnly.length})
{gapOnly.map((g, i) => ( {g.keyword} (pos {g.position}) ))}
)} {/* 4. Quick Wins anchor keywords */} {quickWins.length > 0 && (
โšก Quick Wins ({quickWins.length})
{quickWins.slice(0, 5).map((w, i) => (
{w.keyword} โ€” pos {w.position}, CTR {w.current_ctr?.toFixed(1)}%
))}
)} {/* 5. AI Recommendations */} {allAiRecs.length > 0 && (
๐Ÿค– AI Subheading Ideas ({allAiRecs.length})
{allAiRecs.slice(0, 5).map((r, i) => (
{r.title}
{r.reason}
))}
)}
); }; export default ResearchSources;