SEO Dashboard Fixes and content planning refactoring

This commit is contained in:
ajaysi
2025-10-29 17:10:48 +05:30
parent 5866f49325
commit 4431cd9848
92 changed files with 7046 additions and 1940 deletions

View File

@@ -74,15 +74,15 @@ class CachedAnalyticsAPI {
* Get analytics data with caching
*/
async getAnalyticsData(platforms?: string[], bypassCache: boolean = false): Promise<AnalyticsResponse> {
const params = platforms ? { platforms: platforms.join(',') } : undefined;
const baseParams: any = platforms ? { platforms: platforms.join(',') } : {};
const endpoint = '/api/analytics/data';
// If bypassing cache, add timestamp to force fresh request
const requestParams = bypassCache ? { ...params, _t: Date.now() } : params;
const requestParams = bypassCache ? { ...baseParams, _t: Date.now() } : baseParams;
// Try to get from cache first (unless bypassing)
if (!bypassCache) {
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, params);
const cached = analyticsCache.get<AnalyticsResponse>(endpoint, baseParams);
if (cached) {
console.log('📦 Analytics Cache HIT: Analytics data (cached for 60 minutes)');
return cached;
@@ -95,7 +95,7 @@ class CachedAnalyticsAPI {
// Cache the result with extended TTL (unless bypassing)
if (!bypassCache) {
analyticsCache.set(endpoint, params, response.data, this.CACHE_TTL.ANALYTICS_DATA);
analyticsCache.set(endpoint, baseParams, response.data, this.CACHE_TTL.ANALYTICS_DATA);
}
return response.data;

View File

@@ -199,4 +199,4 @@ class GSCAPI {
}
}
export const gscAPI = new GSCAPI();
export const gscAPI = new GSCAPI();

View File

@@ -21,6 +21,10 @@ export interface PlatformStatus {
connected: boolean;
last_sync?: string;
data_points?: number;
// Additional Bing-specific properties
has_expired_tokens?: boolean;
last_token_date?: string;
total_tokens?: number;
}
export interface AIInsight {
@@ -40,6 +44,19 @@ export interface SEODashboardData {
ai_insights: AIInsight[];
last_updated: string;
website_url?: string; // User's website URL from onboarding
// Real data from backend
summary?: {
clicks: number;
impressions: number;
ctr: number;
position: number;
};
timeseries?: any[];
competitor_insights?: {
competitor_keywords: any[];
content_gaps: any[];
opportunity_score: number;
};
}
// SEO Dashboard API functions

View File

@@ -1,7 +1,7 @@
// SEO CopilotKit Context Component
// Provides real-time context and instructions to CopilotKit
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useMemo } from 'react';
import { useCopilotReadable } from '@copilotkit/react-core';
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
@@ -27,25 +27,29 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
}
}, [personalizationData]);
// Memoize values to prevent unnecessary re-renders
const websiteUrl = useMemo(() => analysisData?.url || '', [analysisData?.url]);
const statusData = useMemo(() => ({
isLoading,
isAnalyzing,
isGenerating,
error
}), [isLoading, isAnalyzing, isGenerating, error]);
const suggestionsCount = useMemo(() => Array.isArray(suggestions) ? suggestions.length : 0, [suggestions]);
// Register SEO analysis data with CopilotKit
useCopilotReadable({
description: "Current SEO analysis data and insights",
value: analysisData,
categories: ["seo", "analysis"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered analysis data', !!analysisData);
}
// Provide a flat, explicit website URL for the LLM
useCopilotReadable({
description: "Current website URL the user is working on",
value: analysisData?.url || '',
value: websiteUrl,
categories: ["seo", "context"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered website URL', analysisData?.url);
}
// Register personalization data with CopilotKit
useCopilotReadable({
@@ -53,9 +57,6 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
value: personalizationData,
categories: ["user", "preferences"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered personalization', !!personalizationData);
}
// Register dashboard layout with CopilotKit
useCopilotReadable({
@@ -63,9 +64,6 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
value: dashboardLayout,
categories: ["ui", "layout"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered layout', !!dashboardLayout);
}
// Register suggestions with CopilotKit
useCopilotReadable({
@@ -73,24 +71,25 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
value: suggestions,
categories: ["actions", "suggestions"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered suggestions', Array.isArray(suggestions) ? suggestions.length : 0);
}
// Register loading states with CopilotKit
useCopilotReadable({
description: "Current loading and processing states",
value: {
isLoading,
isAnalyzing,
isGenerating,
error
},
value: statusData,
categories: ["status", "loading"]
});
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered status', { isLoading, isAnalyzing, isGenerating, hasError: !!error });
}
// Debug logging only in development and only when values actually change
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log('[CopilotContext] Registered analysis data', !!analysisData);
console.log('[CopilotContext] Registered website URL', websiteUrl);
console.log('[CopilotContext] Registered personalization', !!personalizationData);
console.log('[CopilotContext] Registered layout', !!dashboardLayout);
console.log('[CopilotContext] Registered suggestions', suggestionsCount);
console.log('[CopilotContext] Registered status', { isLoading, isAnalyzing, isGenerating, hasError: !!error });
}
}, [analysisData, websiteUrl, personalizationData, dashboardLayout, suggestionsCount, statusData]);
return <>{children}</>;
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Box,
Container,
@@ -7,10 +7,28 @@ import {
Alert,
Skeleton,
Chip,
Button
Button,
IconButton,
Tooltip,
Menu,
MenuItem,
Divider,
Avatar
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth, useUser, SignInButton, SignOutButton } from '@clerk/clerk-react';
import { apiClient } from '../../api/client';
import {
Search as SearchIcon,
Refresh as RefreshIcon,
Person as PersonIcon,
ExitToApp as ExitIcon,
ArrowBack as ArrowBackIcon,
MoreVert as MoreVertIcon,
CheckCircle as CheckCircleIcon,
Schedule as ScheduleIcon,
Info as InfoIcon
} from '@mui/icons-material';
// Shared components
import { DashboardContainer, GlassCard } from '../shared/styled';
@@ -28,6 +46,14 @@ import { useSEODashboardStore } from '../../stores/seoDashboardStore';
// API
import { userDataAPI } from '../../api/userData';
// Shared components
import PlatformAnalytics from '../shared/PlatformAnalytics';
import { cachedAnalyticsAPI } from '../../api/cachedAnalytics';
// OAuth hooks
import { useBingOAuth } from '../../hooks/useBingOAuth';
import { useGSCConnection } from '../OnboardingWizard/common/useGSCConnection';
// SEO Dashboard component
const SEODashboard: React.FC = () => {
// Clerk authentication hooks
@@ -51,6 +77,35 @@ const SEODashboard: React.FC = () => {
getAnalysisFreshness,
} = useSEODashboardStore();
// OAuth hooks
const { connect: connectBing } = useBingOAuth();
const { handleGSCConnect } = useGSCConnection();
// Platform status state
const [platformStatus, setPlatformStatus] = useState({
gsc: { connected: false, sites: [], last_sync: null, status: 'disconnected' },
bing: {
connected: false,
sites: [],
last_sync: null,
status: 'disconnected',
has_expired_tokens: false,
last_token_date: undefined,
total_tokens: 0
}
});
// Menu state
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
// Competitor analysis data from onboarding step 3
const [competitorAnalysisData, setCompetitorAnalysisData] = useState<any>(null);
// PlatformAnalytics refresh handle
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
// Sync dashboard analysis to Copilot store so readables have URL/context
const setCopilotAnalysisData = useSEOCopilotStore(state => state.setAnalysisData);
useEffect(() => {
@@ -62,17 +117,112 @@ const SEODashboard: React.FC = () => {
}
}, [analysisData, setCopilotAnalysisData]);
// Load competitor analysis data on component mount
useEffect(() => {
// Simulate fetching dashboard data
const fetchData = async () => {
loadCompetitorAnalysisData();
}, []);
// Reconnect handlers using existing OAuth hooks
const handleGSCReconnect = async () => {
try {
console.log('Initiating GSC reconnect...');
await handleGSCConnect();
} catch (error) {
console.error('Error reconnecting GSC:', error);
}
};
const handleBingReconnect = async () => {
try {
console.log('Initiating Bing reconnect...');
// Purge expired tokens before reconnecting to avoid refresh loops
try {
await apiClient.post('/bing/purge-expired');
console.log('Purged expired Bing tokens before reconnect');
} catch (purgeError) {
console.warn('Failed to purge expired tokens (non-critical):', purgeError);
}
await connectBing();
// After successful reconnect, refresh platform status and run analysis
try {
// Invalidate backend analytics cache for Bing
try {
await apiClient.post('/api/analytics/cache/clear', null, { params: { platform: 'bing' } });
console.log('Cleared backend analytics cache for Bing');
} catch (cacheErr) {
console.warn('Failed to clear backend analytics cache (non-critical):', cacheErr);
}
// Invalidate frontend cached analytics
try {
cachedAnalyticsAPI.invalidatePlatformStatus();
// Optional: clear all analytics cache if available
// @ts-ignore - method may not exist in older builds
cachedAnalyticsAPI.clearCache?.();
console.log('Cleared frontend analytics cache');
} catch (feCacheErr) {
console.warn('Failed to clear frontend analytics cache (non-critical):', feCacheErr);
}
await fetchPlatformStatus();
} catch (e) {
console.warn('Post-reconnect platform status refresh failed:', e);
}
try {
await useSEODashboardStore.getState().refreshSEOAnalysis();
} catch (e) {
console.warn('Post-reconnect analysis refresh failed:', e);
}
// Force PlatformAnalytics to refresh (bypass cache)
try {
await platformRefreshRef.current?.();
} catch (e) {
console.warn('Platform analytics forced refresh failed (non-critical):', e);
}
} catch (error) {
console.error('Error reconnecting Bing:', error);
}
};
// One-run guard to avoid duplicate fetches under StrictMode
const dataFetchedRef = useRef(false);
// Consolidated data fetching effect
useEffect(() => {
if (dataFetchedRef.current || !isSignedIn) return;
dataFetchedRef.current = true;
const fetchAllData = async () => {
let websiteUrl = 'https://alwrity.com'; // Default fallback
try {
setLoading(true);
// Get user's website URL from user data
const userData = await userDataAPI.getUserData();
const websiteUrl = userData?.website_url || 'https://alwrity.com';
// Fetch platform status and user data in parallel
const [platformResponse, userData] = await Promise.all([
apiClient.get('/api/seo-dashboard/platforms'),
userDataAPI.getUserData()
]);
// Mock data for demonstration
console.log('Platform status response:', platformResponse.status, platformResponse.statusText);
console.log('Platform status data:', platformResponse.data);
setPlatformStatus(platformResponse.data);
websiteUrl = userData?.website_url || 'https://alwrity.com';
// Fetch real data from backend using authenticated API client
console.log('Fetching SEO dashboard overview...');
const response = await apiClient.get('/api/seo-dashboard/overview', {
params: { site_url: websiteUrl }
});
console.log('SEO overview response:', response.status, response.statusText);
console.log('Real SEO data received:', response.data);
setData(response.data);
} catch (error) {
console.error('Error fetching SEO dashboard data:', error);
// Fallback to mock data on error
const mockData = {
health_score: {
score: 84,
@@ -118,26 +268,107 @@ const SEODashboard: React.FC = () => {
last_updated: new Date().toISOString(),
website_url: websiteUrl || undefined // Convert null to undefined for TypeScript
};
setData(mockData);
setLoading(false);
} catch (err) {
setError('Failed to load dashboard data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
fetchAllData();
}, [isSignedIn, setLoading, setData]);
useEffect(() => {
// Run initial SEO analysis if no data exists
if (!loading && !error && data) {
// Call via store to avoid changing function identity in deps
useSEODashboardStore.getState().checkAndRunInitialAnalysis();
// Check if we have cached analysis data first
const store = useSEODashboardStore.getState();
store.checkAndRunInitialAnalysis();
// If no cached analysis data and we have a website URL, run initial analysis
if (!store.analysisData && data.website_url) {
console.log('No cached analysis data found, running initial SEO analysis...');
store.runSEOAnalysis();
}
}
}, [loading, error, data]);
// Menu handlers
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchor(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchor(null);
};
const handleStatusMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setStatusMenuAnchor(event.currentTarget);
};
const handleStatusMenuClose = () => {
setStatusMenuAnchor(null);
};
const handleBackToDashboard = () => {
window.location.href = '/seo-dashboard';
};
const handleRefreshData = async () => {
try {
setLoading(true);
await refreshSEOAnalysis();
await fetchPlatformStatus();
setLastRefresh(new Date());
} catch (error) {
console.error('Error refreshing data:', error);
} finally {
setLoading(false);
}
};
// Background jobs visibility (user-triggered)
const [showBackgroundJobs, setShowBackgroundJobs] = useState(false);
// Platform status fetching function
const fetchPlatformStatus = async () => {
try {
console.log('Fetching platform status...');
const response = await apiClient.get('/api/seo-dashboard/platforms');
console.log('Platform status response:', response.status, response.statusText);
console.log('Platform status data:', response.data);
setPlatformStatus(response.data);
} catch (error) {
console.error('Error fetching platform status:', error);
}
};
// Load competitor analysis data from onboarding step 3
const loadCompetitorAnalysisData = () => {
try {
const cachedData = localStorage.getItem('competitor_analysis_data');
const cachedUrl = localStorage.getItem('competitor_analysis_url');
const cachedTimestamp = localStorage.getItem('competitor_analysis_timestamp');
if (cachedData && cachedUrl && cachedTimestamp) {
const analysisData = JSON.parse(cachedData);
const timestamp = parseInt(cachedTimestamp);
const isRecent = (Date.now() - timestamp) < (7 * 24 * 60 * 60 * 1000); // 7 days
if (isRecent) {
console.log('Loading competitor analysis data from onboarding step 3:', analysisData);
setCompetitorAnalysisData(analysisData);
} else {
console.log('Competitor analysis data is too old, not loading');
}
} else {
console.log('No competitor analysis data found in localStorage');
}
} catch (error) {
console.error('Error loading competitor analysis data:', error);
}
};
if (loading) {
return <Skeleton variant="rectangular" height={200} />;
}
@@ -202,137 +433,445 @@ const SEODashboard: React.FC = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Header */}
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
🔍 SEO Dashboard
</Typography>
<Typography variant="subtitle1" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
AI-powered insights and actionable recommendations
</Typography>
</Box>
{/* Professional Compact Header */}
<Box sx={{
mb: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
py: 2,
px: 3,
bgcolor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 2,
border: '1px solid rgba(255, 255, 255, 0.1)'
}}>
{/* Left Section - Navigation & Title */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{/* User Info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`Signed in as ${user?.primaryEmailAddress?.emailAddress || 'User'}`}
size="small"
sx={{
bgcolor: 'rgba(76, 175, 80, 0.25)',
border: '1px solid rgba(76, 175, 80, 0.45)',
color: 'white',
fontWeight: 600
}}
/>
<SignOutButton>
<Button
variant="outlined"
size="small"
sx={{
borderColor: 'rgba(255, 255, 255, 0.3)',
color: 'white',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
bgcolor: 'rgba(255, 255, 255, 0.1)'
}
}}
>
Sign Out
</Button>
</SignOutButton>
</Box>
{/* Freshness Indicator */}
{(() => {
const freshness = getAnalysisFreshness();
const chipColor = freshness.isStale ? 'rgba(255, 193, 7, 0.25)' : 'rgba(76, 175, 80, 0.25)';
const chipBorder = freshness.isStale ? 'rgba(255, 193, 7, 0.45)' : 'rgba(76, 175, 80, 0.45)';
return (
<Chip
label={`Freshness: ${freshness.label}`}
size="small"
sx={{
bgcolor: chipColor,
border: `1px solid ${chipBorder}`,
color: 'white',
fontWeight: 600
}}
/>
);
})()}
<Button
onClick={refreshSEOAnalysis}
disabled={analysisLoading}
variant="outlined"
size="small"
sx={{
<IconButton
onClick={handleBackToDashboard}
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.6)',
'&:hover': { borderColor: 'rgba(255, 255, 255, 0.9)' }
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
>
{analysisLoading ? 'Refreshing…' : 'Refresh'}
</Button>
<ArrowBackIcon />
</IconButton>
<Box>
<Typography variant="h5" sx={{ color: 'white', fontWeight: 700, lineHeight: 1.2 }}>
SEO Dashboard
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
AI-powered insights and recommendations
</Typography>
</Box>
</Box>
{/* Center Section - Status Overview */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title="Platform Connection Status">
<IconButton
onClick={handleStatusMenuOpen}
sx={{
color: 'white',
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
>
<CheckCircleIcon sx={{
color: platformStatus.gsc.connected && platformStatus.bing.connected
? '#4CAF50'
: platformStatus.gsc.connected || platformStatus.bing.connected
? '#FF9800'
: '#f44336'
}} />
</IconButton>
</Tooltip>
<Tooltip title="Data Freshness">
<Chip
icon={<ScheduleIcon />}
label={(() => {
const freshness = getAnalysisFreshness();
return freshness.label;
})()}
size="small"
sx={{
bgcolor: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)',
fontSize: '0.75rem'
}}
/>
</Tooltip>
</Box>
{/* Right Section - User Menu */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: 'rgba(33, 150, 243, 0.8)' }}>
<PersonIcon fontSize="small" />
</Avatar>
<IconButton
onClick={handleUserMenuOpen}
sx={{
color: 'white',
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
>
<MoreVertIcon />
</IconButton>
</Box>
{/* Status Menu */}
<Menu
anchorEl={statusMenuAnchor}
open={Boolean(statusMenuAnchor)}
onClose={handleStatusMenuClose}
PaperProps={{
sx: {
bgcolor: 'rgba(30, 30, 30, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.1)',
color: 'white',
minWidth: 280
}
}}
>
<MenuItem disabled>
<Typography variant="subtitle2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Platform Status
</Typography>
</MenuItem>
{/* GSC Status */}
<MenuItem>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleIcon sx={{
color: platformStatus.gsc.connected ? '#4CAF50' : '#f44336',
fontSize: 16
}} />
<Typography variant="body2">
Google Search Console: {platformStatus.gsc.connected ? 'Connected' : 'Disconnected'}
</Typography>
</Box>
{!platformStatus.gsc.connected && (
<Button
size="small"
variant="outlined"
onClick={handleGSCReconnect}
sx={{
ml: 2,
borderColor: 'rgba(255, 255, 255, 0.3)',
color: 'white',
fontSize: '0.75rem',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
bgcolor: 'rgba(255, 255, 255, 0.1)'
}
}}
>
Reconnect
</Button>
)}
</Box>
</MenuItem>
{/* Bing Status */}
<MenuItem>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleIcon sx={{
color: platformStatus.bing.connected ? '#4CAF50' :
platformStatus.bing.status === 'expired' ? '#FF9800' : '#f44336',
fontSize: 16
}} />
<Box>
<Typography variant="body2">
Bing Webmaster: {platformStatus.bing.connected ? 'Connected' :
platformStatus.bing.status === 'expired' ? 'Expired' : 'Disconnected'}
</Typography>
{platformStatus.bing.status === 'expired' && platformStatus.bing.last_token_date && (
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '0.7rem' }}>
Last connected: {new Date(platformStatus.bing.last_token_date).toLocaleDateString()}
</Typography>
)}
</Box>
</Box>
{!platformStatus.bing.connected && (
<Button
size="small"
variant="outlined"
onClick={handleBingReconnect}
sx={{
ml: 2,
borderColor: platformStatus.bing.status === 'expired' ? '#FF9800' : 'rgba(255, 255, 255, 0.3)',
color: platformStatus.bing.status === 'expired' ? '#FF9800' : 'white',
fontSize: '0.75rem',
'&:hover': {
borderColor: platformStatus.bing.status === 'expired' ? '#FFB74D' : 'rgba(255, 255, 255, 0.5)',
bgcolor: platformStatus.bing.status === 'expired' ? 'rgba(255, 152, 0, 0.1)' : 'rgba(255, 255, 255, 0.1)'
}
}}
>
{platformStatus.bing.status === 'expired' ? 'Reconnect' : 'Connect'}
</Button>
)}
</Box>
</MenuItem>
</Menu>
{/* User Menu */}
<Menu
anchorEl={userMenuAnchor}
open={Boolean(userMenuAnchor)}
onClose={handleUserMenuClose}
PaperProps={{
sx: {
bgcolor: 'rgba(30, 30, 30, 0.95)',
border: '1px solid rgba(255, 255, 255, 0.1)',
color: 'white'
}
}}
>
<MenuItem disabled>
<Typography variant="subtitle2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
{user?.primaryEmailAddress?.emailAddress || 'User'}
</Typography>
</MenuItem>
<Divider sx={{ bgcolor: 'rgba(255, 255, 255, 0.1)' }} />
<MenuItem onClick={handleRefreshData}>
<RefreshIcon sx={{ mr: 1, fontSize: 16 }} />
<Typography variant="body2">Refresh Data</Typography>
</MenuItem>
<Divider sx={{ bgcolor: 'rgba(255, 255, 255, 0.1)' }} />
<SignOutButton>
<MenuItem>
<ExitIcon sx={{ mr: 1, fontSize: 16 }} />
<Typography variant="body2">Sign Out</Typography>
</MenuItem>
</SignOutButton>
</Menu>
</Box>
{/* GSC Connection Section */}
<Box sx={{ mb: 3 }}>
<GSCLoginButton />
</Box>
{/* CopilotKit Test Panel removed */}
{/* Executive Summary */}
{/* Search Performance Overview */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
📊 Performance Overview
</Typography>
<Grid container spacing={2}>
<Grid item xs={6} sm={3}>
<GlassCard sx={{ p: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Organic Traffic
</Typography>
<Typography variant="h5" sx={{ color: '#4CAF50' }}>
{data.metrics.traffic.value}
</Typography>
</GlassCard>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
📊 Search Performance Overview
</Typography>
<Tooltip title="Real-time analytics data from connected search platforms">
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
</Tooltip>
<Box sx={{ flexGrow: 1 }} />
<Button
variant="outlined"
size="small"
onClick={() => setShowBackgroundJobs((v) => !v)}
sx={{ textTransform: 'none' }}
>
{showBackgroundJobs ? 'Hide Background Jobs' : 'Run Background Jobs'}
</Button>
</Box>
<PlatformAnalytics
platforms={['gsc', 'bing']}
showSummary={true}
refreshInterval={0}
onDataLoaded={(analyticsData) => {
console.log('Real analytics data loaded:', analyticsData);
}}
onRefreshReady={(fn) => { platformRefreshRef.current = fn; }}
onReconnect={(platform) => {
if (platform === 'gsc') {
handleGSCReconnect();
} else if (platform === 'bing') {
handleBingReconnect();
}
}}
showBackgroundJobs={showBackgroundJobs}
/>
{/* Enhanced Metrics with Tooltips */}
<Box sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={6} sm={3}>
<Tooltip title="Number of search engine platforms (GSC, Bing) currently connected to your dashboard">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Connected Platforms
</Typography>
<Typography variant="h4" sx={{ color: '#4CAF50', fontWeight: 700 }}>
{(platformStatus.gsc.connected ? 1 : 0) + (platformStatus.bing.connected ? 1 : 0)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
of 2 platforms
</Typography>
</GlassCard>
</Tooltip>
</Grid>
<Grid item xs={6} sm={3}>
<Tooltip title="Total number of clicks from search results to your website within the selected time period">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Total Clicks
</Typography>
<Typography variant="h4" sx={{ color: '#2196F3', fontWeight: 700 }}>
{data.metrics?.traffic?.value || data.summary?.clicks || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
from search results
</Typography>
</GlassCard>
</Tooltip>
</Grid>
<Grid item xs={6} sm={3}>
<Tooltip title="Total number of times your website appeared in search results within the selected time period">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Total Impressions
</Typography>
<Typography variant="h4" sx={{ color: '#FF9800', fontWeight: 700 }}>
{data.metrics?.impressions?.value || data.summary?.impressions || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
search appearances
</Typography>
</GlassCard>
</Tooltip>
</Grid>
<Grid item xs={6} sm={3}>
<Tooltip title="Percentage of impressions that resulted in a click to your website (Clicks ÷ Impressions × 100)">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Overall CTR
</Typography>
<Typography variant="h4" sx={{ color: '#9C27B0', fontWeight: 700 }}>
{data.metrics?.ctr?.value || data.summary?.ctr || 0}%
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
click-through rate
</Typography>
</GlassCard>
</Tooltip>
</Grid>
</Grid>
<Grid item xs={6} sm={3}>
<GlassCard sx={{ p: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Average Ranking
</Typography>
<Typography variant="h5" sx={{ color: '#2196F3' }}>
{data.metrics.rankings.value}
</Typography>
</GlassCard>
</Grid>
<Grid item xs={6} sm={3}>
<GlassCard sx={{ p: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Mobile Speed
</Typography>
<Typography variant="h5" sx={{ color: '#FF9800' }}>
{data.metrics.mobile.value}
</Typography>
</GlassCard>
</Grid>
<Grid item xs={6} sm={3}>
<GlassCard sx={{ p: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Keywords Tracked
</Typography>
<Typography variant="h5" sx={{ color: '#9C27B0' }}>
{data.metrics.keywords.value}
</Typography>
</GlassCard>
</Grid>
</Grid>
</Box>
</Box>
{/* Competitive Analysis from Onboarding Step 3 */}
{competitorAnalysisData && (
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
🎯 Competitive Analysis
</Typography>
<Tooltip title="Real competitor analysis data from onboarding step 3">
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
</Tooltip>
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Tooltip title="Number of competitors discovered during onboarding analysis">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Competitors Found
</Typography>
<Typography variant="h4" sx={{ color: '#4CAF50', fontWeight: 700 }}>
{competitorAnalysisData.competitors?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
in your market
</Typography>
</GlassCard>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip title="Social media accounts discovered for competitors">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Social Media Accounts
</Typography>
<Typography variant="h4" sx={{ color: '#2196F3', fontWeight: 700 }}>
{Object.keys(competitorAnalysisData.social_media_accounts || {}).length}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
competitor accounts
</Typography>
</GlassCard>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip title="Social media citations and mentions found">
<GlassCard sx={{ p: 2, cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
Social Citations
</Typography>
<Typography variant="h4" sx={{ color: '#FF9800', fontWeight: 700 }}>
{competitorAnalysisData.social_media_citations?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
mentions found
</Typography>
</GlassCard>
</Tooltip>
</Grid>
</Grid>
{/* Competitor List */}
{competitorAnalysisData.competitors && competitorAnalysisData.competitors.length > 0 && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
Top Competitors
</Typography>
<Grid container spacing={2}>
{competitorAnalysisData.competitors.slice(0, 6).map((competitor: any, index: number) => (
<Grid item xs={12} sm={6} md={4} key={index}>
<GlassCard sx={{ p: 2 }}>
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 600, mb: 1 }}>
{competitor.name || competitor.domain || `Competitor ${index + 1}`}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
{competitor.domain || competitor.url || 'No domain available'}
</Typography>
{competitor.description && (
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
{competitor.description.length > 100
? `${competitor.description.substring(0, 100)}...`
: competitor.description}
</Typography>
)}
</GlassCard>
</Grid>
))}
</Grid>
</Box>
)}
{/* Research Summary */}
{competitorAnalysisData.research_summary && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
Research Summary
</Typography>
<GlassCard sx={{ p: 3 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.9)', lineHeight: 1.6 }}>
{competitorAnalysisData.research_summary}
</Typography>
</GlassCard>
</Box>
)}
</Box>
)}
{/* SEO Analyzer Panel */}
<SEOAnalyzerPanel
analysisData={analysisData}

View File

@@ -22,6 +22,7 @@ import {
} from '@mui/icons-material';
import { useAuth } from '@clerk/clerk-react';
import { gscAPI, GSCStatusResponse } from '../../../api/gsc';
import { apiClient } from '../../../api/client';
interface GSCLoginButtonProps {
onStatusChange?: (connected: boolean) => void;
@@ -69,17 +70,28 @@ const GSCLoginButton: React.FC<GSCLoginButtonProps> = ({ onStatusChange }) => {
setLoading(true);
setError(null);
const statusResponse = await gscAPI.getStatus();
setStatus(statusResponse);
// Use backend API to check GSC status
const response = await apiClient.get('/api/seo-dashboard/platforms');
const platformData = response.data;
const gscStatus = {
connected: platformData.gsc?.connected || false,
sites: platformData.gsc?.sites || [],
last_sync: platformData.gsc?.last_sync || undefined
};
setStatus(gscStatus);
if (onStatusChange) {
onStatusChange(statusResponse.connected);
onStatusChange(gscStatus.connected);
}
console.log('GSC Login Button: Status checked, connected:', statusResponse.connected);
console.log('GSC Login Button: Status checked, connected:', gscStatus.connected);
} catch (err) {
console.error('GSC Login Button: Error checking status:', err);
setError('Failed to check GSC connection status');
// Set disconnected status on error
setStatus({ connected: false, sites: [], last_sync: undefined });
} finally {
setLoading(false);
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box,
Button,
@@ -206,8 +206,14 @@ const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
}
};
// One-run guard to prevent duplicate calls in StrictMode
const jobsFetchedRef = useRef(false);
// Poll for job updates
useEffect(() => {
if (jobsFetchedRef.current) return;
jobsFetchedRef.current = true;
fetchJobs();
// Only start polling if there are running jobs

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box,
Card,
@@ -26,6 +26,7 @@ import {
Error as ErrorIcon,
Warning,
} from '@mui/icons-material';
import { Button } from '@mui/material';
import { PlatformAnalytics as PlatformAnalyticsType, AnalyticsSummary, PlatformConnectionStatus } from '../../api/analytics';
import { cachedAnalyticsAPI } from '../../api/cachedAnalytics';
import BingInsightsCard from './BingInsightsCard';
@@ -37,6 +38,8 @@ interface PlatformAnalyticsComponentProps {
refreshInterval?: number; // in milliseconds, 0 = no auto-refresh
onDataLoaded?: (data: any) => void;
onRefreshReady?: (refreshFn: () => Promise<void>) => void; // Expose refresh function to parent
onReconnect?: (platform: string) => void; // Reconnect handler for individual platforms
showBackgroundJobs?: boolean; // Only render background jobs when user triggers
}
const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
@@ -45,6 +48,8 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
refreshInterval = 0,
onDataLoaded,
onRefreshReady,
onReconnect,
showBackgroundJobs = false,
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -111,7 +116,13 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
}
}, [platforms, loadData]);
// One-run guard to prevent duplicate calls in StrictMode
const dataLoadedRef = useRef(false);
useEffect(() => {
if (dataLoadedRef.current) return;
dataLoadedRef.current = true;
loadData();
// Set up auto-refresh if interval is specified
@@ -300,9 +311,31 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
)}
{data.status === 'error' && (
<Alert severity="error" sx={{ mt: 1 }}>
{data.error_message || 'Failed to load analytics data'}
</Alert>
<Box sx={{ mt: 1 }}>
<Alert severity="error" sx={{ mb: 2 }}>
{data.error_message || 'Failed to load analytics data'}
</Alert>
{onReconnect && (
<Button
variant="outlined"
color="error"
size="small"
onClick={() => onReconnect(platform)}
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: '#f44336',
color: '#f44336',
'&:hover': {
borderColor: '#d32f2f',
backgroundColor: 'rgba(244, 67, 54, 0.04)'
}
}}
>
Reconnect {platform.toUpperCase()}
</Button>
)}
</Box>
)}
{data.status === 'partial' && (
@@ -423,18 +456,20 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
))}
</Grid>
{/* Background Job Manager */}
<Box sx={{ mt: 3 }}>
<BackgroundJobManager
siteUrl="https://www.alwrity.com/"
days={30}
onJobCompleted={(job) => {
console.log('🎉 Background job completed:', job);
// Refresh analytics data when job completes
forceRefresh();
}}
/>
</Box>
{/* Background Job Manager - render only when explicitly enabled */}
{showBackgroundJobs && (
<Box sx={{ mt: 3 }}>
<BackgroundJobManager
siteUrl="https://www.alwrity.com/"
days={30}
onJobCompleted={(job) => {
console.log('🎉 Background job completed:', job);
// Refresh analytics data when job completes
forceRefresh();
}}
/>
</Box>
)}
{/* Debug Section - Show data structure for all platforms */}
<Box sx={{ mt: 3 }}>

View File

@@ -111,6 +111,9 @@ export const useBingOAuth = (): UseBingOAuthReturn => {
throw new Error('Failed to open Bing OAuth popup. Please allow popups for this site.');
}
// Track if we've already handled success/error to avoid duplicate processing
let messageHandled = false;
// Listen for popup completion and messages
const messageHandler = (event: MessageEvent) => {
console.log('Bing OAuth: Message received from any source:', {
@@ -139,6 +142,7 @@ export const useBingOAuth = (): UseBingOAuthReturn => {
if (event.data?.type === 'BING_OAUTH_SUCCESS') {
console.log('Bing OAuth: Success message received:', event.data);
messageHandled = true;
popup.close();
window.removeEventListener('message', messageHandler);
@@ -148,6 +152,7 @@ export const useBingOAuth = (): UseBingOAuthReturn => {
}, 1000);
} else if (event.data?.type === 'BING_OAUTH_ERROR') {
console.error('Bing OAuth: Error message received:', event.data);
messageHandled = true;
popup.close();
window.removeEventListener('message', messageHandler);
setError(event.data.error || 'Bing OAuth connection failed');
@@ -170,7 +175,13 @@ export const useBingOAuth = (): UseBingOAuthReturn => {
clearInterval(checkClosed);
window.removeEventListener('message', messageHandler);
console.log('Bing OAuth: Popup closed, refreshing status...');
console.log('Bing OAuth: Popup closed without receiving success/error message');
if (!messageHandled) {
console.log('Bing OAuth: Popup closed without receiving success/error message');
} else {
console.log('Bing OAuth: Popup closed after successful message handling');
}
// Refresh status after OAuth completion
setTimeout(() => {
checkStatus();
@@ -217,10 +228,7 @@ export const useBingOAuth = (): UseBingOAuthReturn => {
setError(null);
}, []);
// Check status on mount
useEffect(() => {
checkStatus();
}, [checkStatus]);
// Note: Status check is now handled by the parent component to avoid duplicate API calls
return {
connected,