feat: Brainstorm Topics with GSC + Issue #518 fixes + Blog Editor enhancements
Issue #518 - Subscription not updating after checkout: - Fix stale closure in SubscriptionContext checkout polling (use subscriptionRef) - Move checkout success polling from InitialRouteHandler into SubscriptionContext - Remove redundant polling code from InitialRouteHandler - Fix plan label: 'Free' instead of 'No Plan', proper capitalization - Add plan refresh button in UserBadge - Add 'View Costing Details' to UserBadge dropdown - Rename 'ALwrity Podcast Maker' to 'Podcast Creator' across UI - Clean subscription=success URL param after verification Blog Writer WYSIWYG Editor enhancements: - Per-section preview toggle (view/edit icons) - Enhanced hover-based toolbar - Circular SVG progress stats bar with detailed tooltip - Research tool chips in stats bar footer - Per-section TTS with useTextToSpeech hook (browser native) - Full blog preview modal with print/PDF support - PlayAllTTSButton: sequential playback with progress bar - OnThisPageNav: floating sidebar with scroll tracking - Section data attributes for scroll anchoring GSC Brainstorm Topics feature: - Backend: gsc_brainstorm_service.py (rule-based + LLM recommendations) - Backend: POST /gsc/brainstorm endpoint with 3-word minimum validation - Frontend: gscBrainstorm.ts API client - Frontend: useGSCBrainstormConnection hook (popup OAuth, no /onboarding redirect) - Frontend: useGSCBrainstorm hook (connect check + brainstorm call) - Frontend: GSCBrainstormModal (3-tab results: Opportunities, Gaps, AI Recs) - Frontend: BrainstormButton (visible at 3+ words, GSC connect overlay) - Wire BrainstormButton into ManualResearchForm and ResearchAction - Add blog_writer to gsc_auth router features for ALWRITY_ENABLED_FEATURES
This commit is contained in:
280
frontend/src/components/BlogWriter/BrainstormButton.tsx
Normal file
280
frontend/src/components/BlogWriter/BrainstormButton.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useGSCBrainstorm } from '../../hooks/useGSCBrainstorm';
|
||||
import { GSCBrainstormModal } from './GSCBrainstormModal';
|
||||
|
||||
interface BrainstormButtonProps {
|
||||
keywords: string;
|
||||
onKeywordsChange: (val: string) => void;
|
||||
onBrainstormResult?: (result: import('../../api/gscBrainstorm').BrainstormResult) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const BrainstormButton: React.FC<BrainstormButtonProps> = ({
|
||||
keywords,
|
||||
onKeywordsChange,
|
||||
onBrainstormResult,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showConnectOverlay, setShowConnectOverlay] = useState(false);
|
||||
const pendingBrainstormRef = useRef(false);
|
||||
const {
|
||||
gscConnected,
|
||||
isConnecting,
|
||||
connectError,
|
||||
isBrainstorming,
|
||||
brainstormError,
|
||||
contentOpportunities,
|
||||
keywordGaps,
|
||||
aiRecommendations,
|
||||
summary,
|
||||
connectGSC,
|
||||
brainstorm,
|
||||
reset,
|
||||
} = useGSCBrainstorm();
|
||||
|
||||
const wordCount = keywords.trim().split(/\s+/).filter(Boolean).length;
|
||||
const isVisible = wordCount >= 3;
|
||||
|
||||
// Auto-trigger brainstorm after GSC connection succeeds
|
||||
useEffect(() => {
|
||||
if (gscConnected && pendingBrainstormRef.current && !isConnecting) {
|
||||
pendingBrainstormRef.current = false;
|
||||
brainstorm(keywords).then((result) => {
|
||||
if (result && onBrainstormResult) {
|
||||
onBrainstormResult(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [gscConnected, isConnecting]);
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!gscConnected) {
|
||||
setShowConnectOverlay(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowModal(true);
|
||||
const result = await brainstorm(keywords);
|
||||
if (result && onBrainstormResult) {
|
||||
onBrainstormResult(result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectSuggestion = (suggestion: string) => {
|
||||
onKeywordsChange(suggestion);
|
||||
setShowModal(false);
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleConnectGSC = async () => {
|
||||
pendingBrainstormRef.current = true;
|
||||
await connectGSC();
|
||||
};
|
||||
|
||||
const handleConnectSuccess = async () => {
|
||||
setShowConnectOverlay(false);
|
||||
setShowModal(true);
|
||||
const result = await brainstorm(keywords);
|
||||
if (result && onBrainstormResult) {
|
||||
onBrainstormResult(result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectCancel = () => {
|
||||
setShowConnectOverlay(false);
|
||||
pendingBrainstormRef.current = false;
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={disabled || isBrainstorming}
|
||||
title={
|
||||
wordCount < 3
|
||||
? 'Enter at least 3 words to enable brainstorming'
|
||||
: 'Brainstorm topics using your Google Search Console data'
|
||||
}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
backgroundColor: disabled || isBrainstorming ? '#ccc' : '#4caf50',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
cursor: disabled || isBrainstorming ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'background-color 0.15s',
|
||||
}}
|
||||
>
|
||||
{isBrainstorming ? (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
border: '2px solid #fff',
|
||||
borderTopColor: 'transparent',
|
||||
borderRadius: '50%',
|
||||
animation: 'brainstormSpin 0.8s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<style>{`@keyframes brainstormSpin { to { transform: rotate(360deg); } }`}</style>
|
||||
Analyzing...
|
||||
</>
|
||||
) : (
|
||||
'Brainstorm Topics'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<GSCBrainstormModal
|
||||
open={showModal}
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
reset();
|
||||
}}
|
||||
contentOpportunities={contentOpportunities}
|
||||
keywordGaps={keywordGaps}
|
||||
aiRecommendations={aiRecommendations}
|
||||
summary={summary}
|
||||
error={brainstormError}
|
||||
isBrainstorming={isBrainstorming}
|
||||
onSelectSuggestion={handleSelectSuggestion}
|
||||
/>
|
||||
|
||||
{showConnectOverlay && (
|
||||
<GSConnectOverlay
|
||||
isConnecting={isConnecting}
|
||||
connectError={connectError}
|
||||
gscConnected={gscConnected}
|
||||
onConnect={handleConnectGSC}
|
||||
onSuccess={handleConnectSuccess}
|
||||
onCancel={handleConnectCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* GSC Connection Overlay */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const GSConnectOverlay: React.FC<{
|
||||
isConnecting: boolean;
|
||||
connectError: string | null;
|
||||
gscConnected: boolean;
|
||||
onConnect: () => void;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ isConnecting, connectError, gscConnected, onConnect, onSuccess, onCancel }) => {
|
||||
// If connection just succeeded, auto-proceed
|
||||
if (gscConnected && !isConnecting) {
|
||||
onSuccess();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '12px',
|
||||
padding: '32px',
|
||||
maxWidth: '440px',
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📊</div>
|
||||
<h3 style={{ margin: '0 0 8px', fontSize: '18px', color: '#333' }}>
|
||||
Connect Google Search Console
|
||||
</h3>
|
||||
<p style={{ margin: '0 0 20px', fontSize: '14px', color: '#666', lineHeight: 1.5 }}>
|
||||
Brainstorm Topics uses your Google Search Console data to suggest blog topics
|
||||
based on what your audience is actually searching for.
|
||||
</p>
|
||||
|
||||
{connectError && (
|
||||
<p style={{ color: '#d32f2f', fontSize: '13px', margin: '0 0 16px' }}>{connectError}</p>
|
||||
)}
|
||||
|
||||
{isConnecting ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
border: '2px solid #e0e0e0',
|
||||
borderTopColor: '#4caf50',
|
||||
borderRadius: '50%',
|
||||
animation: 'gscSpin 0.8s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<style>{`@keyframes gscSpin { to { transform: rotate(360deg); } }`}</style>
|
||||
<span style={{ fontSize: '14px', color: '#666' }}>Opening Google sign-in...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<button
|
||||
onClick={onConnect}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#4caf50',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Connect Google Search Console
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '8px 24px',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#888',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<p style={{ fontSize: '12px', color: '#999', margin: '4px 0 0' }}>
|
||||
You'll be redirected to Google to authorize access. Your data stays private.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainstormButton;
|
||||
Reference in New Issue
Block a user