import React from 'react'; import { PieChart, Pie, Cell, Tooltip as ReTooltip, ResponsiveContainer } from 'recharts'; import { ContentOpportunity, KeywordGap, QuickWin, PageOpportunity, AIRecommendations, AIRecommendation, BrainstormSummary, } from '../../api/gscBrainstorm'; interface GSCBrainstormModalProps { open: boolean; onClose: () => void; contentOpportunities: ContentOpportunity[]; keywordGaps: KeywordGap[]; quickWins: QuickWin[]; pageOpportunities: PageOpportunity[]; aiRecommendations: AIRecommendations | null; summary: BrainstormSummary | null; error: string | null; isBrainstorming: boolean; progressMessage?: string; onSelectSuggestion: (keyword: string) => void; initialKeywords: string; onReRun: (keywords: string) => void; } const tabLabels = [ 'Quick Wins', 'Opportunities', 'Keyword Gaps', 'Pages', 'AI Recommendations', ] as const; type TabKey = typeof tabLabels[number]; export const GSCBrainstormModal: React.FC = ({ open, onClose, contentOpportunities, keywordGaps, quickWins, pageOpportunities, aiRecommendations, summary, error, isBrainstorming, progressMessage, onSelectSuggestion, initialKeywords, onReRun, }) => { const [activeTab, setActiveTab] = React.useState('Quick Wins'); const [topicInput, setTopicInput] = React.useState(initialKeywords); React.useEffect(() => setTopicInput(initialKeywords), [initialKeywords]); if (!open) return null; const hasData = contentOpportunities.length > 0 || keywordGaps.length > 0 || quickWins.length > 0 || pageOpportunities.length > 0 || aiRecommendations !== null; const getTabCount = (tab: TabKey): number => { switch (tab) { case 'Quick Wins': return quickWins.length; case 'Opportunities': return contentOpportunities.length; case 'Keyword Gaps': return keywordGaps.length; case 'Pages': return pageOpportunities.length; case 'AI Recommendations': return aiRecommendations ? (aiRecommendations.immediate_opportunities?.length ?? 0) + (aiRecommendations.content_strategy?.length ?? 0) + (aiRecommendations.long_term_strategy?.length ?? 0) : 0; } }; return (
e.stopPropagation()} > {/* Header */}

Brainstorm Topics

setTopicInput(e.target.value)} disabled={isBrainstorming} placeholder="Enter research topic or keywords..." style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: '6px', fontSize: '13px', color: '#333', backgroundColor: isBrainstorming ? '#f5f5f5' : '#fff', outline: 'none', minWidth: 0, }} /> {summary?.site_url && ( {summary.site_url.replace(/^https?:\/\//, '').slice(0, 30)} )}
{/* Summary dashboard */} {summary && summary.total_keywords_analyzed > 0 && ( )} {/* Loading with educational progress */} {isBrainstorming && (
{progressMessage ? ( <>

{progressMessage}

) : (

Analyzing your GSC data and generating topic suggestions...

)}

This usually takes 5-15 seconds

What's happening behind the scenes:

We fetch your real Google Search Console data, scan for high-ROI keywords, find pages that need optimization, and ask our AI to craft topic suggestions tailored to your site's analytics.

)} {/* Error */} {error && !isBrainstorming && (

{error}

Make sure your Google Search Console is connected and has data for the last 30 days.

)} {/* No data */} {!isBrainstorming && !error && !hasData && (
🔍

No brainstorming data available. Try different keywords or check your GSC connection.

)} {/* Results */} {!isBrainstorming && !error && hasData && ( <> {/* Tabs */}
{tabLabels.map((tab) => { const count = getTabCount(tab); const isActive = activeTab === tab; return ( ); })}
{/* Tab content */}
{activeTab === 'Quick Wins' && } {activeTab === 'Opportunities' && } {activeTab === 'Keyword Gaps' && } {activeTab === 'Pages' && } {activeTab === 'AI Recommendations' && }
)} {/* Footer */}
Click any keyword or title to use it as your research topic
); }; /* ------------------------------------------------------------------ */ /* Summary Dashboard */ /* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */ /* Metric tooltips */ /* ------------------------------------------------------------------ */ const METRIC_HELP: Record = { Impressions: "How many times your site appeared in Google search results over the last 30 days. More impressions means more visibility — but you also want clicks.", Clicks: "How many times people actually clicked on your site in search results. Low clicks with high impressions means your titles or descriptions need improvement.", 'Avg CTR': "Click-Through Rate — the percentage of people who saw your result and clicked it. Higher is better. The industry average is around 2-3%.", 'Avg Position': "Your average ranking across all keywords. Position 1 is the top result. Positions 1-3 get most clicks; anything below page 1 (position 10+) gets very few.", 'SEO Health': "A composite score from 0-100 based on your rankings, CTR, and keyword distribution. 70+ is good, 40-70 needs work, below 40 needs attention.", 'Top 3': "Keywords ranking in the top 3 positions. These are your strongest pages — already visible to most searchers. Small improvements here can bring big gains.", '4-10': "Keywords on page 1 of Google (positions 4-10). These have good visibility but room to climb higher. Optimizing these can push you into the top 3.", '11-20': "Keywords on page 2 of Google. Searchers rarely go past page 1, so writing targeted content for these keywords could dramatically increase traffic.", '21+': "Keywords deep in search results. Low visibility, but often easier to rank for with focused content. These represent untapped traffic potential.", 'Rank Distribution': "Shows where your keywords fall in Google's search results. A healthy profile has keywords spread across all ranges, with a focus on page 1.", }; const HelpIcon: React.FC<{ text: string }> = ({ text }) => { const [show, setShow] = React.useState(false); return ( setShow(true)} onMouseLeave={() => setShow(false)} onClick={() => setShow(!show)} >? {show && (
{text}
)} ); }; const PIE_COLORS = ['#2e7d32', '#1565c0', '#f57c00', '#999']; const SummaryDashboard: React.FC<{ summary: BrainstormSummary }> = ({ summary }) => { const dist = summary.keyword_distribution || {}; const total = dist.positions_1_3 + dist.positions_4_10 + dist.positions_11_20 + dist.positions_21_plus || 1; const healthColor = summary.health_score >= 70 ? '#2e7d32' : summary.health_score >= 40 ? '#f57c00' : '#d32f2f'; const ctrColor = summary.ctr_vs_benchmark >= 0 ? '#2e7d32' : '#d32f2f'; const pieData = [ { name: 'Top 3', value: dist.positions_1_3, pct: Math.round(dist.positions_1_3 / total * 100) }, { name: '4-10', value: dist.positions_4_10, pct: Math.round(dist.positions_4_10 / total * 100) }, { name: '11-20', value: dist.positions_11_20, pct: Math.round(dist.positions_11_20 / total * 100) }, { name: '21+', value: dist.positions_21_plus, pct: Math.round(dist.positions_21_plus / total * 100) }, ]; return (
{/* Metric boxes */}
{/* Rank distribution pie chart */} {total > 1 && (
{pieData.map((entry, idx) => ( ))} { if (!active || !payload?.length) return null; const d = payload[0].payload; return (
{d.name}: {d.value} keywords ({d.pct}%)
); }} />
{pieData.map((d, idx) => ( {d.name}: {d.value} ))}
)}
); }; const MetricBox: React.FC<{ label: string; value: string; valueColor?: string; sublabel?: string; sublabelColor?: string; driving?: boolean; tooltip?: string; }> = ({ label, value, valueColor, sublabel, sublabelColor, driving, tooltip }) => (
{value}
{label} {tooltip && }
{sublabel &&
{sublabel}
}
); /* ------------------------------------------------------------------ */ /* Quick Wins Tab */ /* ------------------------------------------------------------------ */ const QuickWinsTab: React.FC<{ wins: QuickWin[]; onSelect: (kw: string) => void }> = ({ wins, onSelect }) => { if (wins.length === 0) { return ; } return (

These keywords are already on page 1. A small optimization push could land them in the top 3 — the highest-ROI opportunities available.

{wins.map((win, i) => (
onSelect(win.keyword)} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#dcedc8'; e.currentTarget.style.borderLeftColor = '#2e7d32'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#f1f8e9'; e.currentTarget.style.borderLeftColor = '#4caf50'; }} >
{win.keyword}

{win.reason}

{(win.impressions.toLocaleString())} impressions · {win.current_ctr}% CTR
))}
); }; /* ------------------------------------------------------------------ */ /* Opportunities Tab */ /* ------------------------------------------------------------------ */ const OpportunitiesTab: React.FC<{ opportunities: ContentOpportunity[]; onSelect: (kw: string) => void }> = ({ opportunities, onSelect }) => { if (opportunities.length === 0) { return ; } return (
{opportunities.map((opp, i) => (
onSelect(opp.keyword)} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f7ff'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fff'} >
{opp.keyword}
{opp.suggested_format && }

{opp.opportunity}

{opp.impressions.toLocaleString()} impressions Position {opp.current_position} {opp.current_ctr}% CTR +{opp.estimated_traffic_gain} clicks/mo potential
))}
); }; /* ------------------------------------------------------------------ */ /* Keyword Gaps Tab */ /* ------------------------------------------------------------------ */ const GapsTab: React.FC<{ gaps: KeywordGap[]; onSelect: (kw: string) => void }> = ({ gaps, onSelect }) => { if (gaps.length === 0) { return ; } return (

These keywords rank between positions 4-20. Writing targeted content could push them to page 1 where CTR increases dramatically.

{gaps.map((gap, i) => (
onSelect(gap.keyword)} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f7ff'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fff'} >
{gap.keyword}
{gap.current_ctr}% CTR · {gap.clicks} clicks
Position #{gap.position.toFixed(0)}
+{gap.estimated_traffic_if_page1} clicks/mo if page 1
))}
); }; /* ------------------------------------------------------------------ */ /* Pages Tab */ /* ------------------------------------------------------------------ */ const PagesTab: React.FC<{ pages: PageOpportunity[] }> = ({ pages }) => { if (pages.length === 0) { return ; } return (

These pages get significant impressions but low click-through rates. Improving their titles and meta descriptions can boost clicks.

{pages.map((pg, i) => (
{pg.page_title}

{pg.reason}

{pg.impressions.toLocaleString()} impressions · {pg.clicks} clicks · Position {pg.current_position}
{pg.page}
))}
); }; /* ------------------------------------------------------------------ */ /* AI Recommendations Tab */ /* ------------------------------------------------------------------ */ const AIRecommendationsTab: React.FC<{ recommendations: AIRecommendations | null; onSelect: (kw: string) => void }> = ({ recommendations, onSelect }) => { if (!recommendations) { return ; } return (
); }; const RecommendationSection: React.FC<{ title: string; items: AIRecommendation[]; onSelect: (kw: string) => void; color: string }> = ({ title, items, onSelect, color }) => { if (!items || items.length === 0) return null; return (

{title}

{items.map((item, i) => (
{ const kw = item.keyword || item.title.split(/[:(]/)[0].replace(/^[-\s]+/, '').trim(); if (kw && kw.length > 2) onSelect(kw); }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#f8faff'; e.currentTarget.style.borderColor = '#c8d8e8'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#fff'; e.currentTarget.style.borderColor = '#e8e8e8'; }} >
{item.title}
{item.keyword &&
Target: {item.keyword}
} {item.reason &&
{item.reason}
}
{item.format && {item.format}} {item.estimated_impact && {item.estimated_impact}}
))}
); }; /* ------------------------------------------------------------------ */ /* Shared */ /* ------------------------------------------------------------------ */ const InlineHelp: React.FC<{ text: string; children: React.ReactNode }> = ({ text, children }) => { const [show, setShow] = React.useState(false); return ( setShow(true)} onMouseLeave={() => setShow(false)} onClick={() => setShow(!show)} style={{ cursor: 'help', borderBottom: '1px dashed #bbb' }} >{children} {show && ( {text} )} ); }; const Badge: React.FC<{ label: string; color: string }> = ({ label, color }) => ( {label} ); const EmptyMessage: React.FC<{ message: string }> = ({ message }) => (

{message}

); export default GSCBrainstormModal;