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:
ajaysi
2026-05-20 22:34:37 +05:30
parent 68190dedb3
commit 644e72d289
98 changed files with 16137 additions and 2501 deletions

View File

@@ -0,0 +1,102 @@
import { useState, useCallback } from 'react';
import { useAuth } from '@clerk/clerk-react';
import {
gscBrainstormAPI,
BrainstormResult,
ContentOpportunity,
KeywordGap,
AIRecommendations,
BrainstormSummary,
} from '../api/gscBrainstorm';
import { useGSCBrainstormConnection } from './useGSCBrainstormConnection';
interface UseGSCBrainstormReturn {
gscConnected: boolean;
gscSites: { siteUrl: string; permissionLevel: string }[] | null;
isConnecting: boolean;
connectError: string | null;
isBrainstorming: boolean;
brainstormError: string | null;
brainstormResult: BrainstormResult | null;
contentOpportunities: ContentOpportunity[];
keywordGaps: KeywordGap[];
aiRecommendations: AIRecommendations | null;
summary: BrainstormSummary | null;
connectGSC: () => Promise<void>;
brainstorm: (keywords: string, siteUrl?: string) => Promise<BrainstormResult | null>;
reset: () => void;
}
export const useGSCBrainstorm = (): UseGSCBrainstormReturn => {
const { getToken } = useAuth();
const {
gscConnected,
gscSites,
isConnecting,
connectError,
checkConnection,
connectGSC,
} = useGSCBrainstormConnection();
const [isBrainstorming, setIsBrainstorming] = useState(false);
const [brainstormError, setBrainstormError] = useState<string | null>(null);
const [brainstormResult, setBrainstormResult] = useState<BrainstormResult | null>(null);
const brainstorm = useCallback(
async (keywords: string, siteUrl?: string): Promise<BrainstormResult | null> => {
setIsBrainstorming(true);
setBrainstormError(null);
try {
gscBrainstormAPI.setAuthTokenGetter(async () => {
try {
return await getToken();
} catch {
return null;
}
});
const result = await gscBrainstormAPI.brainstorm(keywords, siteUrl);
setBrainstormResult(result);
return result;
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to brainstorm topics. Please try again.';
setBrainstormError(message);
return null;
} finally {
setIsBrainstorming(false);
}
},
[getToken],
);
const reset = useCallback(() => {
setBrainstormResult(null);
setBrainstormError(null);
setIsBrainstorming(false);
}, []);
return {
gscConnected,
gscSites,
isConnecting,
connectError,
isBrainstorming,
brainstormError,
brainstormResult,
contentOpportunities: brainstormResult?.content_opportunities ?? [],
keywordGaps: brainstormResult?.keyword_gaps ?? [],
aiRecommendations: brainstormResult?.ai_recommendations
&& Object.keys(brainstormResult.ai_recommendations).length > 0
? (brainstormResult.ai_recommendations as AIRecommendations)
: null,
summary: brainstormResult?.summary
&& Object.keys(brainstormResult.summary).length > 0
? (brainstormResult.summary as BrainstormSummary)
: null,
connectGSC,
brainstorm,
reset,
};
};