Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements

This commit is contained in:
ajaysi
2026-02-28 20:06:26 +05:30
parent 08a1f4a1d8
commit 4828274cbf
162 changed files with 19489 additions and 4300 deletions

View File

@@ -0,0 +1,212 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
import SEOCopilotActions from './SEOCopilotActions';
const SEOCopilot: React.FC = () => {
const {
loadPersonalizationData,
error,
clearError,
isLoading
} = useSEOCopilotStore();
const { analysisData } = useSEOCopilotStore();
// Handle data loading and error states
useEffect(() => {
const initializeCopilot = async () => {
try {
await loadPersonalizationData();
} catch (error) {
console.error('Failed to initialize SEO Copilot:', error);
}
};
initializeCopilot();
}, [loadPersonalizationData]);
// Auto-clear errors after 5 seconds
useEffect(() => {
if (error) {
const timer = setTimeout(() => {
clearError();
}, 5000);
return () => clearTimeout(timer);
}
}, [error, clearError]);
// Get the CopilotKit API key from the same sources as App.tsx
// Check localStorage first, then fall back to environment variable
const publicApiKey = useMemo(() => {
const savedKey = typeof window !== 'undefined'
? localStorage.getItem('copilotkit_api_key')
: null;
const envKey = process.env.REACT_APP_COPILOTKIT_API_KEY || '';
const key = (savedKey || envKey).trim();
// Validate key format if present
if (key && !key.startsWith('ck_pub_')) {
console.warn('SEOCopilot: CopilotKit API key format invalid - must start with ck_pub_');
}
return key;
}, []);
// Derive a friendly site/brand name from the URL for personalization
const domainRootName = useMemo(() => {
const url = analysisData?.url;
if (!url) return '';
try {
const withProto = url.startsWith('http') ? url : `https://${url}`;
const host = new URL(withProto).hostname;
const parts = host.split('.').filter(Boolean);
const root = parts.length >= 2 ? parts[parts.length - 2] : parts[0] || '';
if (!root) return '';
return root.charAt(0).toUpperCase() + root.slice(1);
} catch {
return '';
}
}, [analysisData?.url]);
// Suggestions model: progressive disclosure
const topLevelGroups = useMemo(() => ([
{ title: 'Content analysis', message: 'Content analysis' },
{ title: 'Website/URL analysis', message: 'Web URL analysis' },
{ title: 'Technical SEO', message: 'Technical SEO' },
{ title: 'Strategy & planning', message: 'Strategy and planning' },
{ title: 'Monitoring & health', message: 'Monitoring and health' }
]), []);
const subSuggestionsByGroup = useMemo(() => ({
'Content analysis': [
{ title: 'Comprehensive content analysis', message: 'Analyze content comprehensively for my site' },
{ title: 'Optimize page content', message: 'Optimize page content for SEO' },
{ title: 'Generate meta descriptions', message: 'Generate meta descriptions for key pages' }
],
'Web URL analysis': [
{ title: 'Comprehensive SEO analysis', message: 'Run comprehensive SEO analysis for a URL' },
{ title: 'Analyze page speed', message: 'Analyze page speed for a URL' },
{ title: 'Analyze sitemap', message: 'Analyze sitemap for my site' },
{ title: 'Generate OpenGraph tags', message: 'Generate OpenGraph tags for a URL' }
],
'Technical SEO': [
{ title: 'Technical SEO audit', message: 'Run a technical SEO audit' },
{ title: 'Check SEO health', message: 'Check overall SEO health' },
{ title: 'Image alt text', message: 'Generate image alt text for pages' }
],
'Strategy and planning': [
{ title: 'Enterprise SEO analysis', message: 'Run enterprise SEO analysis' },
{ title: 'Content strategy', message: 'Analyze content strategy and recommendations' },
{ title: 'Customize SEO dashboard', message: 'Customize the SEO dashboard' }
],
'Monitoring and health': [
{ title: 'Website audit', message: 'Perform a website audit' },
{ title: 'Update SEO charts', message: 'Update SEO charts and visualizations' },
{ title: 'Explain an SEO concept', message: 'Explain an SEO concept in simple terms' }
]
}), []);
const [chatSuggestions, setChatSuggestions] = useState(topLevelGroups);
useEffect(() => {
loadPersonalizationData();
}, [loadPersonalizationData]);
return (
<>
{/* Loading indicator */}
{isLoading && (
<div className="seo-copilot-loading">
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading SEO Assistant...</p>
</div>
</div>
)}
{/* Error display */}
{error && (
<div className="seo-copilot-error">
<div className="error-message">
<span className="error-icon"></span>
<span className="error-text">{error}</span>
<button
className="error-dismiss"
onClick={clearError}
aria-label="Dismiss error"
>
×
</button>
</div>
</div>
)}
<SEOCopilotActions />
<style>{`
.seo-copilot-loading {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 16px;
border-radius: 8px;
z-index: 1300;
display: flex;
align-items: center;
gap: 8px;
}
.seo-copilot-error {
position: fixed;
top: 20px;
right: 20px;
background: #f44336;
color: white;
padding: 12px 16px;
border-radius: 8px;
z-index: 1300;
display: flex;
align-items: center;
gap: 8px;
max-width: 300px;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.error-dismiss {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 0;
margin-left: 8px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #ffffff40;
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</>
);
};
export default SEOCopilot;

View File

@@ -18,12 +18,6 @@ const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
children,
enableDebugMode = false
}) => {
const {
loadPersonalizationData,
error,
clearError,
isLoading
} = useSEOCopilotStore();
const { analysisData } = useSEOCopilotStore();
// Get the CopilotKit API key from the same sources as App.tsx
@@ -107,40 +101,11 @@ const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
// Initialize the provider
useEffect(() => {
const initializeProvider = async () => {
try {
// Load personalization data on mount
await loadPersonalizationData();
if (enableDebugMode) {
console.log('🔧 SEO CopilotKit Provider initialized successfully');
console.log('🔑 CopilotKit API Key:', publicApiKey ? 'Configured' : 'Missing');
}
} catch (error) {
console.error('❌ Failed to initialize SEO CopilotKit Provider:', error);
}
};
initializeProvider();
}, [loadPersonalizationData, enableDebugMode, publicApiKey]);
// Error handling
useEffect(() => {
if (error && enableDebugMode) {
console.error('🚨 SEO CopilotKit Error:', error);
if (enableDebugMode) {
console.log('🔧 SEO CopilotKit Provider initialized successfully');
console.log('🔑 CopilotKit API Key:', publicApiKey ? 'Configured' : 'Missing');
}
}, [error, enableDebugMode]);
// Auto-clear errors after 5 seconds
useEffect(() => {
if (error) {
const timer = setTimeout(() => {
clearError();
}, 5000);
return () => clearTimeout(timer);
}
}, [error, clearError]);
}, [enableDebugMode, publicApiKey]);
return (
<CopilotKit publicApiKey={publicApiKey}>
@@ -194,33 +159,6 @@ Focus on actionable recommendations and use the registered tools.
{/* SEO CopilotKit Actions - Defines available actions */}
<SEOCopilotActions />
{/* Loading indicator */}
{isLoading && (
<div className="seo-copilotkit-loading">
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading SEO Assistant...</p>
</div>
</div>
)}
{/* Error display */}
{error && (
<div className="seo-copilotkit-error">
<div className="error-message">
<span className="error-icon"></span>
<span className="error-text">{error}</span>
<button
className="error-dismiss"
onClick={clearError}
aria-label="Dismiss error"
>
×
</button>
</div>
</div>
)}
{/* Main content */}
<div className="seo-copilotkit-content">
{children}

View File

@@ -17,10 +17,11 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
CircularProgress
CircularProgress,
Drawer
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth, useUser, SignInButton, SignOutButton, useClerk } from '@clerk/clerk-react';
import { useAuth, useUser, SignOutButton, useClerk } from '@clerk/clerk-react';
import { apiClient } from '../../api/client';
import {
Refresh as RefreshIcon,
@@ -32,13 +33,15 @@ import {
Schedule as ScheduleIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
Close as CloseIcon,
AutoAwesome as AIIcon
} from '@mui/icons-material';
// Shared components
import { DashboardContainer, GlassCard } from '../shared/styled';
import SEOAnalyzerPanel from './components/SEOAnalyzerPanel';
import { SEOCopilotKitProvider, SEOCopilotSuggestions } from './index';
import { SEOCopilotSuggestions } from './index';
import SEOCopilot from './SEOCopilot';
// Removed SEOCopilotTest
import useSEOCopilotStore from '../../stores/seoCopilotStore';
@@ -47,6 +50,8 @@ import { useSEODashboardStore } from '../../stores/seoDashboardStore';
// API
import { userDataAPI } from '../../api/userData';
import { SIFIndexingHealth } from '../../api/seoDashboard';
import { getSchedulerDashboard, SchedulerJob } from '../../api/schedulerDashboard';
// Shared components
import PlatformAnalytics from '../shared/PlatformAnalytics';
@@ -81,9 +86,7 @@ const SEODashboard: React.FC = () => {
analysisError,
setData,
setLoading,
setError,
runSEOAnalysis,
checkAndRunInitialAnalysis,
refreshSEOAnalysis,
getAnalysisFreshness,
} = useSEODashboardStore();
@@ -109,7 +112,6 @@ const SEODashboard: React.FC = () => {
// 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);
@@ -119,6 +121,11 @@ const SEODashboard: React.FC = () => {
const [competitiveSitemapBenchmarkingReport, setCompetitiveSitemapBenchmarkingReport] = useState<any>(null);
const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false);
const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState<string | null>(null);
const [sifHealth, setSifHealth] = useState<SIFIndexingHealth | null>(null);
const [sifDetailsOpen, setSifDetailsOpen] = useState(false);
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJob[] | null>(null);
const [schedulerJobsLoading, setSchedulerJobsLoading] = useState(false);
const [schedulerJobsError, setSchedulerJobsError] = useState<string | null>(null);
// PlatformAnalytics refresh handle
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
@@ -140,6 +147,27 @@ const SEODashboard: React.FC = () => {
fetchStrategicInsightsHistory();
}, []);
useEffect(() => {
if (!sifDetailsOpen || schedulerJobs || schedulerJobsLoading) return;
setSchedulerJobsLoading(true);
(async () => {
try {
const dashboard = await getSchedulerDashboard();
const currentUserId = dashboard.user_isolation?.current_user_id || null;
const filtered = dashboard.jobs.filter((job) =>
currentUserId ? job.user_id === currentUserId : Boolean(job.user_id)
);
setSchedulerJobs(filtered);
setSchedulerJobsError(null);
} catch (e) {
console.error('Failed to load scheduler jobs for SEO dashboard:', e);
setSchedulerJobsError('Failed to load scheduler jobs');
} finally {
setSchedulerJobsLoading(false);
}
})();
}, [sifDetailsOpen, schedulerJobs, schedulerJobsLoading]);
const fetchStrategicInsightsHistory = async () => {
setStrategicInsightsLoading(true);
try {
@@ -232,14 +260,16 @@ const SEODashboard: React.FC = () => {
setLoading(true);
// Fetch platform status and user data in parallel
const [platformResponse, userData] = await Promise.all([
const [platformResponse, userData, sifHealthResponse] = await Promise.all([
apiClient.get('/api/seo-dashboard/platforms'),
userDataAPI.getUserData()
userDataAPI.getUserData(),
apiClient.get('/api/seo-dashboard/sif-health')
]);
console.log('Platform status response:', platformResponse.status, platformResponse.statusText);
console.log('Platform status data:', platformResponse.data);
setPlatformStatus(platformResponse.data);
setSifHealth(sifHealthResponse.data);
websiteUrl = userData?.website_url || 'https://alwrity.com';
@@ -514,16 +544,15 @@ const SEODashboard: React.FC = () => {
}
return (
<SEOCopilotKitProvider enableDebugMode={false}>
<DashboardContainer>
<Container maxWidth="xl">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Professional Compact Header */}
<DashboardContainer>
<Container maxWidth="xl">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Professional Compact Header */}
<Box sx={{
mb: 4,
display: 'flex',
@@ -593,6 +622,69 @@ const SEODashboard: React.FC = () => {
}}
/>
</Tooltip>
{sifHealth && (
<Tooltip title="Semantic Indexing Status (SIF)">
<Box
onClick={() => setSifDetailsOpen(true)}
sx={{
ml: 2,
px: 2,
py: 0.75,
borderRadius: 999,
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
bgcolor:
sifHealth.status === 'healthy'
? 'rgba(76, 175, 80, 0.15)'
: sifHealth.status === 'warning'
? 'rgba(255, 152, 0, 0.15)'
: sifHealth.status === 'critical'
? 'rgba(244, 67, 54, 0.15)'
: 'rgba(255, 255, 255, 0.05)',
border:
sifHealth.status === 'healthy'
? '1px solid rgba(76, 175, 80, 0.5)'
: sifHealth.status === 'warning'
? '1px solid rgba(255, 152, 0, 0.5)'
: sifHealth.status === 'critical'
? '1px solid rgba(244, 67, 54, 0.5)'
: '1px solid rgba(255, 255, 255, 0.15)',
}}
>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor:
sifHealth.status === 'healthy'
? '#4CAF50'
: sifHealth.status === 'warning'
? '#FF9800'
: sifHealth.status === 'critical'
? '#F44336'
: 'rgba(255, 255, 255, 0.5)',
}}
/>
<Typography
variant="body2"
sx={{ color: 'rgba(255, 255, 255, 0.9)', fontWeight: 500 }}
>
Semantic Indexing:{' '}
{sifHealth.status === 'not_scheduled'
? 'Not scheduled yet'
: sifHealth.status === 'healthy'
? 'Up to date'
: sifHealth.status === 'warning'
? 'Issues detected'
: 'Needs intervention'}
</Typography>
</Box>
</Tooltip>
)}
</Box>
{/* Right Section - User Menu */}
@@ -1444,11 +1536,244 @@ const SEODashboard: React.FC = () => {
<Box sx={{ mt: 4 }}>
<SEOCopilotSuggestions />
</Box>
{/* SEO Copilot Component for data loading and error handling */}
<SEOCopilot />
</motion.div>
</AnimatePresence>
</Container>
<Drawer
anchor="right"
open={sifDetailsOpen}
onClose={() => setSifDetailsOpen(false)}
PaperProps={{
sx: {
width: { xs: '100%', sm: 360 },
maxWidth: '100vw',
bgcolor: 'rgba(15, 23, 42, 0.98)',
color: 'white'
}
}}
>
<Box
sx={{
p: 2,
borderBottom: 1,
borderColor: 'rgba(148, 163, 184, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<Typography variant="h6">Semantic Indexing Details</Typography>
<IconButton onClick={() => setSifDetailsOpen(false)} sx={{ color: 'rgba(148,163,184,0.9)' }}>
<CloseIcon />
</IconButton>
</Box>
<Box sx={{ p: 2 }}>
{sifHealth ? (
<>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Overall Status
</Typography>
<Typography variant="body1">
{sifHealth.status === 'not_scheduled'
? 'Not scheduled'
: sifHealth.status === 'healthy'
? 'Healthy'
: sifHealth.status === 'warning'
? 'Warning'
: 'Critical'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Next Scheduled Run
</Typography>
<Typography variant="body1">
{sifHealth.task?.next_execution
? new Date(sifHealth.task.next_execution).toLocaleString()
: 'Not scheduled'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Success
</Typography>
<Typography variant="body1">
{sifHealth.task?.last_success
? new Date(sifHealth.task.last_success).toLocaleString()
: 'No successful runs yet'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Failure
</Typography>
<Typography variant="body1">
{sifHealth.task?.last_failure
? new Date(sifHealth.task.last_failure).toLocaleString()
: 'No failures recorded'}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Consecutive Failures
</Typography>
<Typography variant="body1">
{sifHealth.task?.consecutive_failures ?? 0}
</Typography>
</Box>
{sifHealth.last_run?.status && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)', mb: 0.5 }}>
Last Run Status
</Typography>
<Typography variant="body1">
{sifHealth.last_run.status}
{sifHealth.last_run.time
? `${new Date(sifHealth.last_run.time).toLocaleString()}`
: ''}
</Typography>
</Box>
)}
{sifHealth.last_run?.error_message && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(248,113,113,0.9)', mb: 0.5 }}>
Last Error (snippet)
</Typography>
<Typography
variant="body2"
sx={{
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
fontSize: 12,
bgcolor: 'rgba(15,23,42,0.9)',
borderRadius: 1,
p: 1.5,
border: '1px solid rgba(148,163,184,0.3)'
}}
>
{sifHealth.last_run.error_message}
</Typography>
</Box>
)}
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" sx={{ color: 'white', mb: 1 }}>
Scheduled Jobs For Your Account
</Typography>
{schedulerJobsLoading && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} sx={{ color: 'rgba(148,163,184,0.9)' }} />
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
Loading scheduler jobs
</Typography>
</Box>
)}
{schedulerJobsError && !schedulerJobsLoading && (
<Typography variant="body2" sx={{ color: 'rgba(248,113,113,0.9)' }}>
{schedulerJobsError}
</Typography>
)}
{!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length === 0 && (
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
No scheduled jobs found for this account.
</Typography>
)}
{!schedulerJobsLoading && !schedulerJobsError && schedulerJobs && schedulerJobs.length > 0 && (
<Box
sx={{
mt: 1,
maxHeight: 220,
overflowY: 'auto',
borderRadius: 1,
border: '1px solid rgba(51,65,85,0.9)',
bgcolor: 'rgba(15,23,42,0.9)'
}}
>
{schedulerJobs
.slice()
.sort((a, b) => {
const aTime = a.next_run_time ? new Date(a.next_run_time).getTime() : Number.MAX_SAFE_INTEGER;
const bTime = b.next_run_time ? new Date(b.next_run_time).getTime() : Number.MAX_SAFE_INTEGER;
return aTime - bTime;
})
.map((job) => {
const label =
job.task_category === 'website_analysis'
? 'Website Analysis'
: job.task_category === 'platform_insights'
? 'Platform Insights'
: job.task_category === 'deep_website_crawl'
? 'Deep Website Crawl'
: job.function_name || job.id;
const subtitle =
job.website_url ||
(job.platform ? `${job.platform.toUpperCase()} insights` : job.job_store);
const nextRun = job.next_run_time
? new Date(job.next_run_time).toLocaleString()
: 'Not scheduled';
return (
<Box
key={job.id}
sx={{
px: 1.5,
py: 1,
borderBottom: '1px solid rgba(30,41,59,0.9)',
'&:last-of-type': { borderBottom: 'none' }
}}
>
<Typography
variant="body2"
sx={{ color: 'rgba(248,250,252,0.95)', fontWeight: 500 }}
>
{label}
</Typography>
<Typography
variant="caption"
sx={{ display: 'block', color: 'rgba(148,163,184,0.9)' }}
>
{subtitle}
</Typography>
<Typography
variant="caption"
sx={{ display: 'block', color: 'rgba(148,163,184,0.7)', mt: 0.25 }}
>
Next run: {nextRun}
{job.frequency ? `${job.frequency}` : ''}
</Typography>
</Box>
);
})}
</Box>
)}
</Box>
</>
) : (
<Typography variant="body2" sx={{ color: 'rgba(148,163,184,0.9)' }}>
No semantic indexing information available.
</Typography>
)}
</Box>
</Drawer>
</DashboardContainer>
</SEOCopilotKitProvider>
);
};

View File

@@ -7,6 +7,7 @@ export { default as SEOCopilotContext } from './SEOCopilotContext';
export { default as SEOCopilotActions } from './SEOCopilotActions';
export { default as SEOCopilotSuggestions } from './SEOCopilotSuggestions';
export { default as SEOCopilotTest } from './SEOCopilotTest';
export { default as SEOCopilot } from './SEOCopilot';
// Store and Services
export { useSEOCopilotStore, useSEOCopilotAnalysis, useSEOCopilotSuggestions, useSEOCopilotDashboard } from '../../stores/seoCopilotStore';