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,79 @@
import { apiClient } from './client';
export interface ContentOpportunity {
type: 'Content Optimization' | 'Content Enhancement';
keyword: string;
opportunity: string;
potential_impact: 'High' | 'Medium';
current_position: number;
impressions: number;
priority: 'High' | 'Medium';
}
export interface KeywordGap {
keyword: string;
position: number;
impressions: number;
}
export interface AIRecommendations {
immediate_opportunities: string[];
content_strategy: string[];
long_term_strategy: string[];
}
export interface BrainstormSummary {
site_url: string;
date_range: { start: string; end: string };
total_keywords_analyzed: number;
total_impressions: number;
total_clicks: number;
avg_ctr: number;
avg_position: number;
keyword_distribution: {
positions_1_3: number;
positions_4_10: number;
positions_11_20: number;
positions_21_plus: number;
};
top_keywords: Array<{ keyword: string; impressions: number; position: number }>;
top_pages: Array<{ page: string; clicks: number; impressions: number }>;
}
export interface BrainstormResult {
error?: string;
content_opportunities: ContentOpportunity[];
keyword_gaps: KeywordGap[];
ai_recommendations: AIRecommendations | Record<string, never>;
summary: BrainstormSummary | Record<string, never>;
}
class GSCBrainstormAPI {
private baseUrl = '/gsc';
private getAuthToken: (() => Promise<string | null>) | null = null;
setAuthTokenGetter(getToken: () => Promise<string | null>) {
this.getAuthToken = getToken;
}
private async getAuthenticatedClient() {
const token = this.getAuthToken ? await this.getAuthToken() : null;
if (!token) {
throw new Error('No authentication token available');
}
return apiClient.create({
headers: { Authorization: `Bearer ${token}` },
});
}
async brainstorm(keywords: string, siteUrl?: string): Promise<BrainstormResult> {
const client = await this.getAuthenticatedClient();
const response = await client.post(`${this.baseUrl}/brainstorm`, {
keywords,
site_url: siteUrl || undefined,
});
return response.data;
}
}
export const gscBrainstormAPI = new GSCBrainstormAPI();

View File

@@ -1,83 +0,0 @@
/**
* Wix API Client
* Handles Wix connection status and operations
*/
import { apiClient } from './client';
export interface WixStatus {
connected: boolean;
sites: Array<{
id: string;
blog_url: string;
blog_id: string;
created_at: string;
scope: string;
}>;
total_sites: number;
error?: string;
}
class WixAPI {
private baseUrl = '/api/wix';
private getAuthToken: (() => Promise<string | null>) | null = null;
/**
* Set the auth token getter function
*/
setAuthTokenGetter(getToken: () => Promise<string | null>) {
this.getAuthToken = getToken;
}
/**
* Get authenticated API client with auth token
*/
private async getAuthenticatedClient() {
const token = this.getAuthToken ? await this.getAuthToken() : null;
if (!token) {
throw new Error('No authentication token available');
}
return apiClient.create({
headers: {
'Authorization': `Bearer ${token}`
}
});
}
/**
* Get Wix connection status
*/
async getStatus(): Promise<WixStatus> {
try {
const client = await this.getAuthenticatedClient();
const response = await client.get(`${this.baseUrl}/status`);
return response.data;
} catch (error: any) {
console.error('Wix API: Error getting status:', error);
return {
connected: false,
sites: [],
total_sites: 0,
error: error.response?.data?.detail || error.message
};
}
}
/**
* Health check for Wix service
*/
async healthCheck(): Promise<boolean> {
try {
const client = await this.getAuthenticatedClient();
await client.get(`${this.baseUrl}/connection/status`);
return true;
} catch (error) {
console.error('Wix API: Health check failed:', error);
return false;
}
}
}
export const wixAPI = new WixAPI();